diff --git a/Cargo.lock b/Cargo.lock index 97bfa15994..8e9e976efd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -62,8 +62,7 @@ dependencies = [ [[package]] name = "alacritty_config_derive" version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77044c45bdb871e501b5789ad16293ecb619e5733b60f4bb01d1cb31c463c336" +source = "git+https://github.com/zed-industries/alacritty?rev=e9b864860ec79cc1b70042aafce100cdd6985a0a#e9b864860ec79cc1b70042aafce100cdd6985a0a" dependencies = [ "proc-macro2", "quote", @@ -72,14 +71,13 @@ dependencies = [ [[package]] name = "alacritty_terminal" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02fb5d4af84e39f9754d039ff6de2233c8996dbae0af74910156e559e5766e2f" +version = "0.17.0-dev" +source = "git+https://github.com/zed-industries/alacritty?rev=e9b864860ec79cc1b70042aafce100cdd6985a0a#e9b864860ec79cc1b70042aafce100cdd6985a0a" dependencies = [ "alacritty_config_derive", "base64 0.13.0", "bitflags", - "dirs 3.0.2", + "dirs 4.0.0", "libc", "log", "mio 0.6.23", @@ -5355,12 +5353,14 @@ name = "terminal" version = "0.1.0" dependencies = [ "alacritty_terminal", + "anyhow", "client", "dirs 4.0.0", "editor", "futures", "gpui", "itertools", + "libc", "mio-extras", "ordered-float", "project", @@ -5368,6 +5368,7 @@ dependencies = [ "shellexpand", "smallvec", "theme", + "thiserror", "util", "workspace", ] diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 21c45acafe..72252edd71 100644 --- a/assets/keymaps/default.json +++ b/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", diff --git a/assets/settings/default.json b/assets/settings/default.json index 73c73636f6..6c34d6be70 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -102,10 +102,10 @@ // "working_directory": "current_project_directory", //Any key-value pairs added to this list will be added to the terminal's - //enviroment. Use `:` to seperate multiple values, not multiple list items - "env": [ - //["KEY", "value1:value2"] - ] + //enviroment. Use `:` to seperate multiple values. + "env": { + //"KEY": "value1:value2" + } //Set the terminal's font size. If this option is not included, //the terminal will default to matching the buffer's font size. //"font_size": "15" diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index 467a45b93e..5f438057ee 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -362,12 +362,7 @@ mod tests { }); let palette = workspace.read_with(cx, |workspace, _| { - workspace - .modal() - .unwrap() - .clone() - .downcast::() - .unwrap() + workspace.modal::().unwrap() }); palette @@ -398,12 +393,7 @@ mod tests { // Assert editor command not present let palette = workspace.read_with(cx, |workspace, _| { - workspace - .modal() - .unwrap() - .clone() - .downcast::() - .unwrap() + workspace.modal::().unwrap() }); palette diff --git a/crates/diagnostics/src/items.rs b/crates/diagnostics/src/items.rs index 81ec57fe54..cc5932a781 100644 --- a/crates/diagnostics/src/items.rs +++ b/crates/diagnostics/src/items.rs @@ -1,5 +1,5 @@ use collections::HashSet; -use editor::{Editor, GoToNextDiagnostic}; +use editor::{Editor, GoToDiagnostic}; use gpui::{ elements::*, platform::CursorStyle, serde_json, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext, Subscription, View, ViewContext, ViewHandle, WeakViewHandle, @@ -48,10 +48,10 @@ impl DiagnosticIndicator { } } - fn go_to_next_diagnostic(&mut self, _: &GoToNextDiagnostic, cx: &mut ViewContext) { + fn go_to_next_diagnostic(&mut self, _: &GoToDiagnostic, cx: &mut ViewContext) { if let Some(editor) = self.active_editor.as_ref().and_then(|e| e.upgrade(cx)) { editor.update(cx, |editor, cx| { - editor.go_to_diagnostic(editor::Direction::Next, cx); + editor.go_to_diagnostic_impl(editor::Direction::Next, cx); }) } } @@ -202,7 +202,7 @@ impl View for DiagnosticIndicator { }) .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, |_, cx| { - cx.dispatch_action(GoToNextDiagnostic) + cx.dispatch_action(GoToDiagnostic) }) .boxed(), ); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index bf3dfc220e..2a187c58b2 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -82,9 +82,6 @@ pub struct SelectNext { pub replace_newest: bool, } -#[derive(Clone, PartialEq)] -pub struct GoToDiagnostic(pub Direction); - #[derive(Clone, PartialEq)] pub struct Scroll(pub Vector2F); @@ -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.go_to_diagnostic(Direction::Next, cx) + fn go_to_diagnostic(&mut self, _: &GoToDiagnostic, cx: &mut ViewContext) { + self.go_to_diagnostic_impl(Direction::Next, cx) } fn go_to_prev_diagnostic(&mut self, _: &GoToPrevDiagnostic, cx: &mut ViewContext) { - self.go_to_diagnostic(Direction::Prev, cx) + self.go_to_diagnostic_impl(Direction::Prev, cx) } - pub fn go_to_diagnostic(&mut self, direction: Direction, cx: &mut ViewContext) { + pub fn go_to_diagnostic_impl(&mut self, direction: Direction, cx: &mut ViewContext) { let buffer = self.buffer.read(cx).snapshot(cx); let selection = self.selections.newest::(cx); + + // If there is an active Diagnostic Popover. Jump to it's diagnostic instead. + if direction == Direction::Next { + if let Some(popover) = self.hover_state.diagnostic_popover.as_ref() { + let (group_id, jump_to) = popover.activation_info(); + self.activate_diagnostics(group_id, cx); + self.change_selections(Some(Autoscroll::Center), cx, |s| { + let mut new_selection = s.newest_anchor().clone(); + new_selection.collapse_to(jump_to, SelectionGoal::None); + s.select_anchors(vec![new_selection.clone()]); + }); + return; + } + } + let mut active_primary_range = self.active_diagnostics.as_ref().map(|active_diagnostics| { active_diagnostics .primary_range diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index b8ae7c8b0a..b8bee49d8d 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -41,6 +41,10 @@ use std::{ ops::Range, }; +const MIN_POPOVER_CHARACTER_WIDTH: f32 = 20.; +const MIN_POPOVER_LINE_HEIGHT: f32 = 4.; +const HOVER_POPOVER_GAP: f32 = 10.; + struct SelectionLayout { head: DisplayPoint, range: Range, @@ -268,8 +272,9 @@ impl EditorElement { } if paint - .hover_bounds - .map_or(false, |hover_bounds| hover_bounds.contains_point(position)) + .hover_popover_bounds + .iter() + .any(|hover_bounds| hover_bounds.contains_point(position)) { return false; } @@ -585,35 +590,78 @@ impl EditorElement { cx.scene.pop_stacking_context(); } - if let Some((position, hover_popover)) = layout.hover.as_mut() { + if let Some((position, hover_popovers)) = layout.hover_popovers.as_mut() { cx.scene.push_stacking_context(None); // This is safe because we check on layout whether the required row is available let hovered_row_layout = &layout.line_layouts[(position.row() - start_row) as usize]; - let size = hover_popover.size(); + + // Minimum required size: Take the first popover, and add 1.5 times the minimum popover + // height. This is the size we will use to decide whether to render popovers above or below + // the hovered line. + let first_size = hover_popovers[0].size(); + let height_to_reserve = + first_size.y() + 1.5 * MIN_POPOVER_LINE_HEIGHT as f32 * layout.line_height; + + // Compute Hovered Point let x = hovered_row_layout.x_for_index(position.column() as usize) - scroll_left; - let y = position.row() as f32 * layout.line_height - scroll_top - size.y(); - let mut popover_origin = content_origin + vec2f(x, y); + let y = position.row() as f32 * layout.line_height - scroll_top; + let hovered_point = content_origin + vec2f(x, y); - if popover_origin.y() < 0.0 { - popover_origin.set_y(popover_origin.y() + layout.line_height + size.y()); + paint.hover_popover_bounds.clear(); + + if hovered_point.y() - height_to_reserve > 0.0 { + // There is enough space above. Render popovers above the hovered point + let mut current_y = hovered_point.y(); + for hover_popover in hover_popovers { + let size = hover_popover.size(); + let mut popover_origin = vec2f(hovered_point.x(), current_y - size.y()); + + let x_out_of_bounds = bounds.max_x() - (popover_origin.x() + size.x()); + if x_out_of_bounds < 0.0 { + popover_origin.set_x(popover_origin.x() + x_out_of_bounds); + } + + hover_popover.paint( + popover_origin, + RectF::from_points(Vector2F::zero(), vec2f(f32::MAX, f32::MAX)), // Let content bleed outside of editor + cx, + ); + + paint.hover_popover_bounds.push( + RectF::new(popover_origin, hover_popover.size()) + .dilate(Vector2F::new(0., 5.)), + ); + + current_y = popover_origin.y() - HOVER_POPOVER_GAP; + } + } else { + // There is not enough space above. Render popovers below the hovered point + let mut current_y = hovered_point.y() + layout.line_height; + for hover_popover in hover_popovers { + let size = hover_popover.size(); + let mut popover_origin = vec2f(hovered_point.x(), current_y); + + let x_out_of_bounds = bounds.max_x() - (popover_origin.x() + size.x()); + if x_out_of_bounds < 0.0 { + popover_origin.set_x(popover_origin.x() + x_out_of_bounds); + } + + hover_popover.paint( + popover_origin, + RectF::from_points(Vector2F::zero(), vec2f(f32::MAX, f32::MAX)), // Let content bleed outside of editor + cx, + ); + + paint.hover_popover_bounds.push( + RectF::new(popover_origin, hover_popover.size()) + .dilate(Vector2F::new(0., 5.)), + ); + + current_y = popover_origin.y() + size.y() + HOVER_POPOVER_GAP; + } } - let x_out_of_bounds = bounds.max_x() - (popover_origin.x() + size.x()); - if x_out_of_bounds < 0.0 { - popover_origin.set_x(popover_origin.x() + x_out_of_bounds); - } - - hover_popover.paint( - popover_origin, - RectF::from_points(Vector2F::zero(), vec2f(f32::MAX, f32::MAX)), // Let content bleed outside of editor - cx, - ); - - paint.hover_bounds = Some( - RectF::new(popover_origin, hover_popover.size()).dilate(Vector2F::new(0., 5.)), - ); - cx.scene.pop_stacking_context(); } @@ -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)>, context_menu: Option<(DisplayPoint, ElementBox)>, code_actions_indicator: Option<(u32, ElementBox)>, - hover: Option<(DisplayPoint, ElementBox)>, + hover_popovers: Option<(DisplayPoint, Vec)>, } struct BlockLayout { @@ -1635,7 +1681,7 @@ pub struct PaintState { gutter_bounds: RectF, text_bounds: RectF, context_menu_bounds: Option, - hover_bounds: Option, + hover_popover_bounds: Vec, } impl PaintState { diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 7ae9e01b09..99669cb15d 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -3,9 +3,10 @@ use gpui::{ elements::{Flex, MouseEventHandler, Padding, Text}, impl_internal_actions, platform::CursorStyle, - Axis, Element, ElementBox, ModelHandle, MutableAppContext, RenderContext, Task, ViewContext, + Axis, Element, ElementBox, ModelHandle, MouseButton, MutableAppContext, RenderContext, Task, + ViewContext, }; -use language::Bias; +use language::{Bias, DiagnosticEntry, DiagnosticSeverity}; use project::{HoverBlock, Project}; use settings::Settings; use std::{ops::Range, time::Duration}; @@ -13,7 +14,7 @@ use util::TryFutureExt; use crate::{ display_map::ToDisplayPoint, Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSnapshot, - EditorStyle, + EditorStyle, GoToDiagnostic, RangeToAnchorExt, }; pub const HOVER_DELAY_MILLIS: u64 = 350; @@ -54,17 +55,11 @@ pub fn hover_at(editor: &mut Editor, action: &HoverAt, cx: &mut ViewContext) -> bool { - let mut did_hide = false; + let did_hide = editor.hover_state.info_popover.take().is_some() + | editor.hover_state.diagnostic_popover.take().is_some(); - // only notify the context once - if editor.hover_state.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::(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::(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::( - vec![range], - |theme| theme.editor.hover_popover.highlight, - cx, - ); - } - this.hover_state.popover = hover_popover; - cx.notify(); + this.highlight_background::( + vec![hover_popover.symbol_range.clone()], + |theme| theme.editor.hover_popover.highlight, + cx, + ); } else { - if this.hover_state.visible() { - // Popover was visible, but now is hidden. Dismiss it - hide_hover(this, cx); - } else { - // Clear selected symbol range for future requests - this.hover_state.symbol_range = None; - } + this.clear_background_highlights::(cx); } + + this.hover_state.info_popover = hover_popover; + cx.notify(); }); } Ok::<_, anyhow::Error>(()) @@ -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, + pub info_popover: Option, + pub diagnostic_popover: Option, pub triggered_from: Option, - pub symbol_range: Option>, - pub task: Option>>, + pub info_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, + cx: &mut RenderContext, + ) -> Option<(DisplayPoint, Vec)> { + // If there is a diagnostic, position the popovers based on that. + // Otherwise use the start of the hover range + let anchor = self + .diagnostic_popover + .as_ref() + .map(|diagnostic_popover| &diagnostic_popover.local_diagnostic.range.start) + .or_else(|| { + self.info_popover + .as_ref() + .map(|info_popover| &info_popover.symbol_range.start) + })?; + let point = anchor.to_display_point(&snapshot.display_snapshot); + + // Don't render if the relevant point isn't on screen + if !self.visible() || !visible_rows.contains(&point.row()) { + return None; + } + + let mut elements = Vec::new(); + + if let Some(diagnostic_popover) = self.diagnostic_popover.as_ref() { + elements.push(diagnostic_popover.render(style, cx)); + } + if let Some(info_popover) = self.info_popover.as_ref() { + elements.push(info_popover.render(style, cx)); + } + + Some((point, elements)) } } #[derive(Debug, Clone)] -pub struct HoverPopover { +pub struct InfoPopover { pub project: ModelHandle, - pub anchor: Anchor, + pub symbol_range: Range, pub contents: Vec, } -impl HoverPopover { - pub fn render( - &self, - snapshot: &EditorSnapshot, - style: EditorStyle, - cx: &mut RenderContext, - ) -> (DisplayPoint, ElementBox) { - let element = MouseEventHandler::new::(0, cx, |_, cx| { +impl InfoPopover { + pub fn render(&self, style: &EditorStyle, cx: &mut RenderContext) -> ElementBox { + MouseEventHandler::new::(0, cx, |_, cx| { let mut flex = Flex::new(Axis::Vertical).scrollable::(1, None, cx); flex.extend(self.contents.iter().map(|content| { let project = self.project.read(cx); @@ -309,10 +356,61 @@ impl HoverPopover { top: 5., ..Default::default() }) - .boxed(); + .boxed() + } +} - let display_point = self.anchor.to_display_point(&snapshot.display_snapshot); - (display_point, element) +#[derive(Debug, Clone)] +pub struct DiagnosticPopover { + local_diagnostic: DiagnosticEntry, + primary_diagnostic: Option>, +} + +impl DiagnosticPopover { + pub fn render(&self, style: &EditorStyle, cx: &mut RenderContext) -> ElementBox { + enum PrimaryDiagnostic {} + + let mut text_style = style.hover_popover.prose.clone(); + text_style.font_size = style.text.font_size; + + let container_style = match self.local_diagnostic.diagnostic.severity { + DiagnosticSeverity::HINT => style.hover_popover.info_container, + DiagnosticSeverity::INFORMATION => style.hover_popover.info_container, + DiagnosticSeverity::WARNING => style.hover_popover.warning_container, + DiagnosticSeverity::ERROR => style.hover_popover.error_container, + _ => style.hover_popover.container, + }; + + let tooltip_style = cx.global::().theme.tooltip.clone(); + + MouseEventHandler::new::(0, cx, |_, _| { + Text::new(self.local_diagnostic.diagnostic.message.clone(), text_style) + .with_soft_wrap(true) + .contained() + .with_style(container_style) + .boxed() + }) + .on_click(MouseButton::Left, |_, cx| { + cx.dispatch_action(GoToDiagnostic) + }) + .with_cursor_style(CursorStyle::PointingHand) + .with_tooltip::( + 0, + "Go To Diagnostic".to_string(), + Some(Box::new(crate::GoToDiagnostic)), + tooltip_style, + cx, + ) + .boxed() + } + + pub fn activation_info(&self) -> (usize, Anchor) { + let entry = self + .primary_diagnostic + .as_ref() + .unwrap_or(&self.local_diagnostic); + + (entry.diagnostic.group_id, entry.range.start.clone()) } } @@ -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::(move |_, _| async move { - Ok(Some(lsp::Hover { - contents: lsp::HoverContents::Markup(lsp::MarkupContent { - kind: lsp::MarkupKind::Markdown, - value: indoc! {" + cx.handle_request::(move |_, _, _| async move { + Ok(Some(lsp::Hover { + contents: lsp::HoverContents::Markup(lsp::MarkupContent { + kind: lsp::MarkupKind::Markdown, + value: indoc! {" # Some basic docs Some test documentation"} - .to_string(), - }), - range: Some(symbol_range), - })) - }); + .to_string(), + }), + range: Some(symbol_range), + })) + }); cx.foreground() .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); requests.next().await; @@ -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::(|_, _| 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::(|_, _| 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::(move |_, _| async move { - Ok(Some(lsp::Hover { - contents: lsp::HoverContents::Markup(lsp::MarkupContent { - kind: lsp::MarkupKind::Markdown, - value: indoc! {" - # Some other basic docs - Some other test documentation"} - .to_string(), - }), - range: Some(symbol_range), - })) - }) - .next() - .await; - cx.foreground().run_until_parked(); + cx.handle_request::(move |_, _, _| async move { + Ok(Some(lsp::Hover { + contents: lsp::HoverContents::Markup(lsp::MarkupContent { + kind: lsp::MarkupKind::Markdown, + value: indoc! {" + # Some other basic docs + Some other test documentation"} + .to_string(), + }), + range: Some(symbol_range), + })) + }) + .next() + .await; + + cx.condition(|editor, _| editor.hover_state.visible()).await; cx.editor(|editor, _| { - assert!(editor.hover_state.visible()); assert_eq!( - editor.hover_state.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::(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() + }); + } } diff --git a/crates/editor/src/test.rs b/crates/editor/src/test.rs index d5ef041a19..86928e5b8c 100644 --- a/crates/editor/src/test.rs +++ b/crates/editor/src/test.rs @@ -9,9 +9,13 @@ use futures::{Future, StreamExt}; use indoc::indoc; use collections::BTreeMap; -use gpui::{json, keymap::Keystroke, AppContext, ModelHandle, ViewContext, ViewHandle}; -use language::{point_to_lsp, FakeLspAdapter, Language, LanguageConfig, Selection}; -use lsp::request; +use gpui::{ + json, keymap::Keystroke, AppContext, ModelContext, ModelHandle, ViewContext, ViewHandle, +}; +use language::{ + point_to_lsp, Buffer, BufferSnapshot, FakeLspAdapter, Language, LanguageConfig, Selection, +}; +use lsp::{notification, request}; use project::Project; use settings::Settings; use util::{ @@ -119,7 +123,7 @@ impl<'a> EditorTestContext<'a> { self.editor.condition(self.cx, predicate) } - pub fn editor(&mut self, read: F) -> T + pub fn editor(&self, read: F) -> T where F: FnOnce(&Editor, &AppContext) -> T, { @@ -133,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(&self, read: F) -> T + where + F: FnOnce(&MultiBuffer, &AppContext) -> T, + { + self.editor(|editor, cx| read(editor.buffer().read(cx), cx)) + } + + pub fn update_multibuffer(&mut self, update: F) -> T + where + F: FnOnce(&mut MultiBuffer, &mut ModelContext) -> T, + { + self.update_editor(|editor, cx| editor.buffer().update(cx, update)) + } + + pub fn buffer_text(&self) -> String { + self.multibuffer(|buffer, cx| buffer.snapshot(cx).text()) + } + + pub fn buffer(&self, read: F) -> T + where + F: FnOnce(&Buffer, &AppContext) -> T, + { + self.multibuffer(|multibuffer, cx| { + let buffer = multibuffer.as_singleton().unwrap().read(cx); + read(buffer, cx) }) } @@ -145,6 +171,20 @@ impl<'a> EditorTestContext<'a> { }); } + pub fn update_buffer(&mut self, update: F) -> T + where + F: FnOnce(&mut Buffer, &mut ModelContext) -> T, + { + self.update_multibuffer(|multibuffer, cx| { + let buffer = multibuffer.as_singleton().unwrap(); + buffer.update(cx, update) + }) + } + + pub fn buffer_snapshot(&self) -> BufferSnapshot { + self.buffer(|buffer, _| buffer.snapshot()) + } + pub fn simulate_keystroke(&mut self, keystroke_text: &str) { let keystroke = Keystroke::parse(keystroke_text).unwrap(); 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 { + 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, - 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>, { - let url = self.editor_lsp_url.clone(); + let url = self.buffer_lsp_url.clone(); self.lsp.handle_request::(move |params, cx| { let url = url.clone(); handler(url, params, cx) }) } + + pub fn notify(&self, params: T::Params) { + self.lsp.notify::(params); + } } impl<'a> Deref for EditorLspTestContext<'a> { diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 5603fa22ef..d69c95605d 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -317,15 +317,7 @@ mod tests { let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx)); cx.dispatch_action(window_id, Toggle); - let finder = cx.read(|cx| { - workspace - .read(cx) - .modal() - .cloned() - .unwrap() - .downcast::() - .unwrap() - }); + let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); finder .update(cx, |finder, cx| { finder.update_matches("bna".to_string(), cx) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index c413ef2de4..bff9438124 100644 --- a/crates/language/src/buffer.rs +++ b/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, diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 76cc653ff8..c518888456 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -81,7 +81,7 @@ pub struct TerminalSettings { pub working_directory: Option, pub font_size: Option, pub font_family: Option, - pub env: Option>, + pub env: Option>, } #[derive(Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)] diff --git a/crates/terminal/Cargo.toml b/crates/terminal/Cargo.toml index 09a3fb171f..03c6a26b7d 100644 --- a/crates/terminal/Cargo.toml +++ b/crates/terminal/Cargo.toml @@ -8,7 +8,7 @@ path = "src/terminal.rs" doctest = false [dependencies] -alacritty_terminal = "0.16.1" +alacritty_terminal = { git = "https://github.com/zed-industries/alacritty", rev = "e9b864860ec79cc1b70042aafce100cdd6985a0a"} editor = { path = "../editor" } util = { path = "../util" } gpui = { path = "../gpui" } @@ -23,6 +23,10 @@ ordered-float = "2.1.1" itertools = "0.10" dirs = "4.0.0" shellexpand = "2.1.0" +libc = "0.2" +anyhow = "1" +thiserror = "1.0" + [dev-dependencies] gpui = { path = "../gpui", features = ["test-support"] } diff --git a/crates/terminal/src/connected_el.rs b/crates/terminal/src/connected_el.rs new file mode 100644 index 0000000000..865bbc7de7 --- /dev/null +++ b/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, + rects: Vec, + highlights: Vec, + cursor: Option, + background_color: Color, + selection_color: Color, + size: TermDimensions, +} + +///Helper struct for converting data between alacritty's cursor points, and displayed cursor points +struct DisplayCursor { + line: i32, + col: usize, +} + +impl DisplayCursor { + fn from(cursor_point: Point, display_offset: usize) -> Self { + Self { + line: cursor_point.line.0 + display_offset as i32, + col: cursor_point.column.0, + } + } + + pub fn line(&self) -> i32 { + self.line + } + + pub fn col(&self) -> usize { + self.col + } +} + +#[derive(Clone, Copy, Debug)] +pub struct TermDimensions { + cell_width: f32, + line_height: f32, + height: f32, + width: f32, +} + +impl TermDimensions { + pub fn new(line_height: f32, cell_width: f32, size: Vector2F) -> Self { + TermDimensions { + cell_width, + line_height, + width: size.x(), + height: size.y(), + } + } + + pub fn num_lines(&self) -> usize { + (self.height / self.line_height).floor() as usize + } + + pub fn num_columns(&self) -> usize { + (self.width / self.cell_width).floor() as usize + } + + pub fn height(&self) -> f32 { + self.height + } + + pub fn width(&self) -> f32 { + self.width + } + + pub fn cell_width(&self) -> f32 { + self.cell_width + } + + pub fn line_height(&self) -> f32 { + self.line_height + } +} + +impl Into for TermDimensions { + fn into(self) -> WindowSize { + WindowSize { + num_lines: self.num_lines() as u16, + num_cols: self.num_columns() as u16, + cell_width: self.cell_width() as u16, + cell_height: self.line_height() as u16, + } + } +} + +impl Dimensions for TermDimensions { + fn total_lines(&self) -> usize { + self.screen_lines() //TODO: Check that this is fine. This is supposed to be for the back buffer... + } + + fn screen_lines(&self) -> usize { + self.num_lines() + } + + fn columns(&self) -> usize { + self.num_columns() + } +} + +#[derive(Clone, Debug, Default)] +struct LayoutCell { + point: Point, + text: Line, +} + +impl LayoutCell { + fn new(point: Point, text: Line) -> LayoutCell { + LayoutCell { point, text } + } + + fn paint( + &self, + origin: Vector2F, + layout: &LayoutState, + visible_bounds: RectF, + cx: &mut PaintContext, + ) { + let pos = point_to_absolute(origin, self.point, layout); + self.text + .paint(pos, visible_bounds, layout.size.line_height, cx); + } +} + +#[derive(Clone, Debug, Default)] +struct LayoutRect { + point: Point, + num_of_cells: usize, + color: Color, +} + +impl LayoutRect { + fn new(point: Point, num_of_cells: usize, color: Color) -> LayoutRect { + LayoutRect { + point, + num_of_cells, + color, + } + } + + fn extend(&self) -> Self { + LayoutRect { + point: self.point, + num_of_cells: self.num_of_cells + 1, + color: self.color, + } + } + + fn paint(&self, origin: Vector2F, layout: &LayoutState, cx: &mut PaintContext) { + let position = point_to_absolute(origin, self.point, layout); + + let size = vec2f( + (layout.size.cell_width.ceil() * self.num_of_cells as f32).ceil(), + layout.size.line_height, + ); + + cx.scene.push_quad(Quad { + bounds: RectF::new(position, size), + background: Some(self.color), + border: Default::default(), + corner_radius: 0., + }) + } +} + +fn point_to_absolute(origin: Vector2F, point: Point, layout: &LayoutState) -> Vector2F { + vec2f( + (origin.x() + point.column as f32 * layout.size.cell_width).floor(), + origin.y() + point.line as f32 * layout.size.line_height, + ) +} + +#[derive(Clone, Debug, Default)] +struct RelativeHighlightedRange { + line_index: usize, + range: Range, +} + +impl RelativeHighlightedRange { + fn new(line_index: usize, range: Range) -> Self { + RelativeHighlightedRange { line_index, range } + } + + fn to_highlighted_range_line( + &self, + origin: Vector2F, + layout: &LayoutState, + ) -> HighlightedRangeLine { + let start_x = origin.x() + self.range.start as f32 * layout.size.cell_width; + let end_x = + origin.x() + self.range.end as f32 * layout.size.cell_width + layout.size.cell_width; + + return HighlightedRangeLine { start_x, end_x }; + } +} + +///The GPUI element that paints the terminal. +///We need to keep a reference to the view for mouse events, do we need it for any other terminal stuff, or can we move that to connection? +pub struct TerminalEl { + terminal: WeakModelHandle, + view: WeakViewHandle, + modal: bool, +} + +impl TerminalEl { + pub fn new( + view: WeakViewHandle, + terminal: WeakModelHandle, + modal: bool, + ) -> TerminalEl { + TerminalEl { + view, + terminal, + modal, + } + } + + fn layout_grid( + grid: GridIterator, + text_style: &TextStyle, + terminal_theme: &TerminalStyle, + text_layout_cache: &TextLayoutCache, + modal: bool, + selection_range: Option, + ) -> ( + Vec, + Vec, + Vec, + ) { + let mut cells = vec![]; + let mut rects = vec![]; + let mut highlight_ranges = vec![]; + + let mut cur_rect: Option = None; + let mut cur_alac_color = None; + let mut highlighted_range = None; + + let linegroups = grid.group_by(|i| i.point.line); + for (line_index, (_, line)) in linegroups.into_iter().enumerate() { + for (x_index, cell) in line.enumerate() { + //Increase selection range + { + if selection_range + .map(|range| range.contains(cell.point)) + .unwrap_or(false) + { + let mut range = highlighted_range.take().unwrap_or(x_index..x_index); + range.end = range.end.max(x_index); + highlighted_range = Some(range); + } + } + + //Expand background rect range + { + if matches!(cell.bg, Named(NamedColor::Background)) { + //Continue to next cell, resetting variables if nescessary + cur_alac_color = None; + if let Some(rect) = cur_rect { + rects.push(rect); + cur_rect = None + } + } else { + match cur_alac_color { + Some(cur_color) => { + if cell.bg == cur_color { + cur_rect = cur_rect.take().map(|rect| rect.extend()); + } else { + cur_alac_color = Some(cell.bg); + if let Some(_) = cur_rect { + rects.push(cur_rect.take().unwrap()); + } + cur_rect = Some(LayoutRect::new( + Point::new(line_index as i32, cell.point.column.0 as i32), + 1, + convert_color(&cell.bg, &terminal_theme.colors, modal), + )); + } + } + None => { + cur_alac_color = Some(cell.bg); + cur_rect = Some(LayoutRect::new( + Point::new(line_index as i32, cell.point.column.0 as i32), + 1, + convert_color(&cell.bg, &terminal_theme.colors, modal), + )); + } + } + } + } + + //Layout current cell text + { + let cell_text = &cell.c.to_string(); + if cell_text != " " { + let cell_style = + TerminalEl::cell_style(&cell, terminal_theme, text_style, modal); + + let layout_cell = text_layout_cache.layout_str( + cell_text, + text_style.font_size, + &[(cell_text.len(), cell_style)], + ); + + cells.push(LayoutCell::new( + Point::new(line_index as i32, cell.point.column.0 as i32), + layout_cell, + )) + } + }; + } + + if highlighted_range.is_some() { + highlight_ranges.push(RelativeHighlightedRange::new( + line_index, + highlighted_range.take().unwrap(), + )) + } + + if cur_rect.is_some() { + rects.push(cur_rect.take().unwrap()); + } + } + + (cells, rects, highlight_ranges) + } + + // Compute the cursor position and expected block width, may return a zero width if x_for_index returns + // the same position for sequential indexes. Use em_width instead + fn shape_cursor( + cursor_point: DisplayCursor, + size: TermDimensions, + text_fragment: &Line, + ) -> Option<(Vector2F, f32)> { + if cursor_point.line() < size.total_lines() as i32 { + let cursor_width = if text_fragment.width() == 0. { + size.cell_width() + } else { + text_fragment.width() + }; + + Some(( + vec2f( + cursor_point.col() as f32 * size.cell_width(), + cursor_point.line() as f32 * size.line_height(), + ), + cursor_width, + )) + } else { + None + } + } + + ///Convert the Alacritty cell styles to GPUI text styles and background color + fn cell_style( + indexed: &Indexed<&Cell>, + style: &TerminalStyle, + text_style: &TextStyle, + modal: bool, + ) -> RunStyle { + let flags = indexed.cell.flags; + let fg = convert_color(&indexed.cell.fg, &style.colors, modal); + + let underline = flags + .contains(Flags::UNDERLINE) + .then(|| Underline { + color: Some(fg), + squiggly: false, + thickness: OrderedFloat(1.), + }) + .unwrap_or_default(); + + RunStyle { + color: fg, + font_id: text_style.font_id, + underline, + } + } + + fn attach_mouse_handlers( + &self, + origin: Vector2F, + view_id: usize, + visible_bounds: RectF, + cur_size: TermDimensions, + cx: &mut PaintContext, + ) { + let mouse_down_connection = self.terminal.clone(); + let click_connection = self.terminal.clone(); + let drag_connection = self.terminal.clone(); + cx.scene.push_mouse_region( + MouseRegion::new(view_id, None, visible_bounds) + .on_down( + MouseButton::Left, + move |MouseButtonEvent { position, .. }, cx| { + if let Some(conn_handle) = mouse_down_connection.upgrade(cx.app) { + conn_handle.update(cx.app, |terminal, cx| { + let (point, side) = TerminalEl::mouse_to_cell_data( + position, + origin, + cur_size, + terminal.get_display_offset(), + ); + + terminal.mouse_down(point, side); + + cx.notify(); + }) + } + }, + ) + .on_click( + MouseButton::Left, + move |MouseButtonEvent { + position, + click_count, + .. + }, + cx| { + cx.focus_parent_view(); + if let Some(conn_handle) = click_connection.upgrade(cx.app) { + conn_handle.update(cx.app, |terminal, cx| { + let (point, side) = TerminalEl::mouse_to_cell_data( + position, + origin, + cur_size, + terminal.get_display_offset(), + ); + + terminal.click(point, side, click_count); + + cx.notify(); + }); + } + }, + ) + .on_drag( + MouseButton::Left, + move |_, MouseMovedEvent { position, .. }, cx| { + if let Some(conn_handle) = drag_connection.upgrade(cx.app) { + conn_handle.update(cx.app, |terminal, cx| { + let (point, side) = TerminalEl::mouse_to_cell_data( + position, + origin, + cur_size, + terminal.get_display_offset(), + ); + + terminal.drag(point, side); + + cx.notify() + }); + } + }, + ), + ); + } + + ///Configures a text style from the current settings. + pub fn make_text_style(font_cache: &FontCache, settings: &Settings) -> TextStyle { + // Pull the font family from settings properly overriding + let family_id = settings + .terminal_overrides + .font_family + .as_ref() + .or_else(|| settings.terminal_defaults.font_family.as_ref()) + .and_then(|family_name| font_cache.load_family(&[family_name]).log_err()) + .unwrap_or(settings.buffer_font_family); + + let font_size = settings + .terminal_overrides + .font_size + .or(settings.terminal_defaults.font_size) + .unwrap_or(settings.buffer_font_size); + + let font_id = font_cache + .select_font(family_id, &Default::default()) + .unwrap(); + + TextStyle { + color: settings.theme.editor.text_color, + font_family_id: family_id, + font_family_name: font_cache.family_name(family_id).unwrap(), + font_id, + font_size, + font_properties: Default::default(), + underline: Default::default(), + } + } + + pub fn mouse_to_cell_data( + pos: Vector2F, + origin: Vector2F, + cur_size: TermDimensions, + display_offset: usize, + ) -> (Point, alacritty_terminal::index::Direction) { + let pos = pos.sub(origin); + let point = { + let col = pos.x() / cur_size.cell_width; //TODO: underflow... + let col = min(GridCol(col as usize), cur_size.last_column()); + + let line = pos.y() / cur_size.line_height; + let line = min(line as i32, cur_size.bottommost_line().0); + + Point::new(GridLine(line - display_offset as i32), col) + }; + + //Copied (with modifications) from alacritty/src/input.rs > Processor::cell_side() + let side = { + let x = pos.0.x() as usize; + let cell_x = + x.saturating_sub(cur_size.cell_width as usize) % cur_size.cell_width as usize; + let half_cell_width = (cur_size.cell_width / 2.0) as usize; + + let additional_padding = + (cur_size.width() - cur_size.cell_width * 2.) % cur_size.cell_width; + let end_of_grid = cur_size.width() - cur_size.cell_width - additional_padding; + //Width: Pixels or columns? + if cell_x > half_cell_width + // Edge case when mouse leaves the window. + || x as f32 >= end_of_grid + { + Side::Right + } else { + Side::Left + } + }; + + (point, side) + } +} + +impl Element for TerminalEl { + type LayoutState = LayoutState; + type PaintState = (); + + fn layout( + &mut self, + constraint: gpui::SizeConstraint, + cx: &mut gpui::LayoutContext, + ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) { + let settings = cx.global::(); + let font_cache = &cx.font_cache(); + + //Setup layout information + let terminal_theme = &settings.theme.terminal; + let text_style = TerminalEl::make_text_style(font_cache, &settings); + let selection_color = settings.theme.editor.selection.selection; + let dimensions = { + let line_height = font_cache.line_height(text_style.font_size); + let cell_width = font_cache.em_advance(text_style.font_id, text_style.font_size); + TermDimensions::new(line_height, cell_width, constraint.max) + }; + + let terminal = self.terminal.upgrade(cx).unwrap().read(cx); + + let (cursor, cells, rects, highlights) = + terminal.render_lock(Some(dimensions.clone()), |content, cursor_text| { + let (cells, rects, highlights) = TerminalEl::layout_grid( + content.display_iter, + &text_style, + terminal_theme, + cx.text_layout_cache, + self.modal, + content.selection, + ); + + //Layout cursor + let cursor = { + let cursor_point = + DisplayCursor::from(content.cursor.point, content.display_offset); + let cursor_text = { + let str_trxt = cursor_text.to_string(); + cx.text_layout_cache.layout_str( + &str_trxt, + text_style.font_size, + &[( + str_trxt.len(), + RunStyle { + font_id: text_style.font_id, + color: terminal_theme.colors.background, + underline: Default::default(), + }, + )], + ) + }; + + TerminalEl::shape_cursor(cursor_point, dimensions, &cursor_text).map( + move |(cursor_position, block_width)| { + Cursor::new( + cursor_position, + block_width, + dimensions.line_height, + terminal_theme.colors.cursor, + CursorShape::Block, + Some(cursor_text.clone()), + ) + }, + ) + }; + + (cursor, cells, rects, highlights) + }); + + //Select background color + let background_color = if self.modal { + terminal_theme.colors.modal_background + } else { + terminal_theme.colors.background + }; + + //Done! + ( + constraint.max, + LayoutState { + cells, + cursor, + background_color, + selection_color, + size: dimensions, + rects, + highlights, + }, + ) + } + + fn paint( + &mut self, + bounds: gpui::geometry::rect::RectF, + visible_bounds: gpui::geometry::rect::RectF, + layout: &mut Self::LayoutState, + cx: &mut gpui::PaintContext, + ) -> Self::PaintState { + //Setup element stuff + let clip_bounds = Some(visible_bounds); + + cx.paint_layer(clip_bounds, |cx| { + let origin = bounds.origin() + vec2f(layout.size.cell_width, 0.); + + //Elements are ephemeral, only at paint time do we know what could be clicked by a mouse + self.attach_mouse_handlers(origin, self.view.id(), visible_bounds, layout.size, cx); + + cx.paint_layer(clip_bounds, |cx| { + //Start with a background color + cx.scene.push_quad(Quad { + bounds: RectF::new(bounds.origin(), bounds.size()), + background: Some(layout.background_color), + border: Default::default(), + corner_radius: 0., + }); + + for rect in &layout.rects { + rect.paint(origin, &layout, cx) + } + }); + + //Draw Selection + cx.paint_layer(clip_bounds, |cx| { + let start_y = layout.highlights.get(0).map(|highlight| { + origin.y() + highlight.line_index as f32 * layout.size.line_height + }); + + if let Some(y) = start_y { + let range_lines = layout + .highlights + .iter() + .map(|relative_highlight| { + relative_highlight.to_highlighted_range_line(origin, layout) + }) + .collect::>(); + + let hr = HighlightedRange { + start_y: y, //Need to change this + line_height: layout.size.line_height, + lines: range_lines, + color: layout.selection_color, + //Copied from editor. TODO: move to theme or something + corner_radius: 0.15 * layout.size.line_height, + }; + hr.paint(bounds, cx.scene); + } + }); + + //Draw the text cells + cx.paint_layer(clip_bounds, |cx| { + for cell in &layout.cells { + cell.paint(origin, layout, visible_bounds, cx); + } + }); + + //Draw cursor + if let Some(cursor) = &layout.cursor { + cx.paint_layer(clip_bounds, |cx| { + cursor.paint(origin, cx); + }) + } + }); + } + + fn dispatch_event( + &mut self, + event: &gpui::Event, + _bounds: gpui::geometry::rect::RectF, + visible_bounds: gpui::geometry::rect::RectF, + layout: &mut Self::LayoutState, + _paint: &mut Self::PaintState, + cx: &mut gpui::EventContext, + ) -> bool { + match event { + Event::ScrollWheel(ScrollWheelEvent { + delta, position, .. + }) => visible_bounds + .contains_point(*position) + .then(|| { + let vertical_scroll = + (delta.y() / layout.size.line_height) * ALACRITTY_SCROLL_MULTIPLIER; + + self.terminal.upgrade(cx.app).map(|terminal| { + terminal + .read(cx.app) + .scroll(Scroll::Delta(vertical_scroll.round() as i32)); + }); + }) + .is_some(), + Event::KeyDown(KeyDownEvent { keystroke, .. }) => { + if !cx.is_parent_view_focused() { + return false; + } + + //TODO Talk to keith about how to catch events emitted from an element. + if let Some(view) = self.view.upgrade(cx.app) { + view.update(cx.app, |view, cx| view.clear_bel(cx)) + } + + self.terminal + .upgrade(cx.app) + .map(|model_handle| model_handle.read(cx.app)) + .map(|term| term.try_keystroke(keystroke)) + .unwrap_or(false) + } + _ => false, + } + } + + fn metadata(&self) -> Option<&dyn std::any::Any> { + None + } + + fn debug( + &self, + _bounds: gpui::geometry::rect::RectF, + _layout: &Self::LayoutState, + _paint: &Self::PaintState, + _cx: &gpui::DebugContext, + ) -> gpui::serde_json::Value { + json!({ + "type": "TerminalElement", + }) + } + + fn rect_for_text_range( + &self, + _: Range, + bounds: RectF, + _: RectF, + layout: &Self::LayoutState, + _: &Self::PaintState, + _: &gpui::MeasurementContext, + ) -> Option { + // 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), + ) + ); + } +} diff --git a/crates/terminal/src/connected_view.rs b/crates/terminal/src/connected_view.rs new file mode 100644 index 0000000000..e7b7131147 --- /dev/null +++ b/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, + has_new_content: bool, + //Currently using iTerm bell, show bell emoji in tab until input is received + has_bell: bool, + // Only for styling purposes. Doesn't effect behavior + modal: bool, +} + +impl ConnectedView { + pub fn from_terminal( + terminal: ModelHandle, + modal: bool, + cx: &mut ViewContext, + ) -> Self { + cx.observe(&terminal, |_, _, cx| cx.notify()).detach(); + cx.subscribe(&terminal, |this, _, event, cx| match event { + Event::Wakeup => { + if cx.is_self_focused() { + cx.notify() + } else { + this.has_new_content = true; + cx.emit(Event::TitleChanged); + } + } + Event::Bell => { + this.has_bell = true; + cx.emit(Event::TitleChanged); + } + _ => cx.emit(*event), + }) + .detach(); + + Self { + terminal, + has_new_content: true, + has_bell: false, + modal, + } + } + + pub fn handle(&self) -> ModelHandle { + self.terminal.clone() + } + + pub fn has_new_content(&self) -> bool { + self.has_new_content + } + + pub fn has_bell(&self) -> bool { + self.has_bell + } + + pub fn clear_bel(&mut self, cx: &mut ViewContext) { + self.has_bell = false; + cx.emit(Event::TitleChanged); + } + + fn clear(&mut self, _: &Clear, cx: &mut ViewContext) { + self.terminal.read(cx).clear(); + } + + ///Attempt to paste the clipboard into the terminal + fn copy(&mut self, _: &Copy, cx: &mut ViewContext) { + self.terminal + .read(cx) + .copy() + .map(|text| cx.write_to_clipboard(ClipboardItem::new(text))); + } + + ///Attempt to paste the clipboard into the terminal + fn paste(&mut self, _: &Paste, cx: &mut ViewContext) { + cx.read_from_clipboard().map(|item| { + self.terminal.read(cx).paste(item.text()); + }); + } + + ///Synthesize the keyboard event corresponding to 'up' + fn up(&mut self, _: &Up, cx: &mut ViewContext) { + self.terminal + .read(cx) + .try_keystroke(&Keystroke::parse("up").unwrap()); + } + + ///Synthesize the keyboard event corresponding to 'down' + fn down(&mut self, _: &Down, cx: &mut ViewContext) { + self.terminal + .read(cx) + .try_keystroke(&Keystroke::parse("down").unwrap()); + } + + ///Synthesize the keyboard event corresponding to 'ctrl-c' + fn ctrl_c(&mut self, _: &CtrlC, cx: &mut ViewContext) { + self.terminal + .read(cx) + .try_keystroke(&Keystroke::parse("ctrl-c").unwrap()); + } + + ///Synthesize the keyboard event corresponding to 'escape' + fn escape(&mut self, _: &Escape, cx: &mut ViewContext) { + self.terminal + .read(cx) + .try_keystroke(&Keystroke::parse("escape").unwrap()); + } + + ///Synthesize the keyboard event corresponding to 'enter' + fn enter(&mut self, _: &Enter, cx: &mut ViewContext) { + self.terminal + .read(cx) + .try_keystroke(&Keystroke::parse("enter").unwrap()); + } +} + +impl View for ConnectedView { + fn ui_name() -> &'static str { + "Connected Terminal View" + } + + fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox { + let terminal_handle = self.terminal.clone().downgrade(); + TerminalEl::new(cx.handle(), terminal_handle, self.modal) + .contained() + .boxed() + } + + fn on_focus(&mut self, _cx: &mut ViewContext) { + self.has_new_content = false; + } + + fn selected_text_range(&self, _: &AppContext) -> Option> { + Some(0..0) + } + + fn replace_text_in_range( + &mut self, + _: Option>, + text: &str, + cx: &mut ViewContext, + ) { + self.terminal + .update(cx, |terminal, _| terminal.write_to_pty(text.into())); + } +} diff --git a/crates/terminal/src/connection.rs b/crates/terminal/src/connection.rs deleted file mode 100644 index 0e051da17c..0000000000 --- a/crates/terminal/src/connection.rs +++ /dev/null @@ -1,252 +0,0 @@ -mod keymappings; - -use alacritty_terminal::{ - ansi::{ClearMode, Handler}, - config::{Config, Program, PtyConfig}, - event::{Event as AlacTermEvent, Notify}, - event_loop::{EventLoop, Msg, Notifier}, - grid::Scroll, - sync::FairMutex, - term::{SizeInfo, TermMode}, - tty::{self, setup_env}, - Term, -}; -use futures::{channel::mpsc::unbounded, StreamExt}; -use settings::{Settings, Shell}; -use std::{collections::HashMap, path::PathBuf, sync::Arc}; - -use gpui::{keymap::Keystroke, ClipboardItem, CursorStyle, Entity, ModelContext}; - -use crate::{ - color_translation::{get_color_at_index, to_alac_rgb}, - ZedListener, -}; - -use self::keymappings::to_esc_str; - -const DEFAULT_TITLE: &str = "Terminal"; - -///Upward flowing events, for changing the title and such -#[derive(Copy, Clone, Debug)] -pub enum Event { - TitleChanged, - CloseTerminal, - Activate, - Wakeup, - Bell, -} - -pub struct TerminalConnection { - pub pty_tx: Notifier, - pub term: Arc>>, - pub title: String, - pub associated_directory: Option, -} - -impl TerminalConnection { - pub fn new( - working_directory: Option, - shell: Option, - env_vars: Option>, - initial_size: SizeInfo, - cx: &mut ModelContext, - ) -> TerminalConnection { - let pty_config = { - let shell = shell.and_then(|shell| match shell { - Shell::System => None, - Shell::Program(program) => Some(Program::Just(program)), - Shell::WithArguments { program, args } => Some(Program::WithArgs { program, args }), - }); - - PtyConfig { - shell, - working_directory: working_directory.clone(), - hold: false, - } - }; - - let mut env: HashMap = HashMap::new(); - if let Some(envs) = env_vars { - for (var, val) in envs { - env.insert(var, val); - } - } - - //TODO: Properly set the current locale, - env.insert("LC_ALL".to_string(), "en_US.UTF-8".to_string()); - - let config = Config { - pty_config: pty_config.clone(), - env, - ..Default::default() - }; - - setup_env(&config); - - //Spawn a task so the Alacritty EventLoop can communicate with us in a view context - let (events_tx, mut events_rx) = unbounded(); - - //Set up the terminal... - let term = Term::new(&config, initial_size, ZedListener(events_tx.clone())); - let term = Arc::new(FairMutex::new(term)); - - //Setup the pty... - let pty = { - if let Some(pty) = tty::new(&pty_config, &initial_size, None).ok() { - pty - } else { - let pty_config = PtyConfig { - shell: None, - working_directory: working_directory.clone(), - ..Default::default() - }; - - tty::new(&pty_config, &initial_size, None) - .expect("Failed with default shell too :(") - } - }; - - //And connect them together - let event_loop = EventLoop::new( - term.clone(), - ZedListener(events_tx.clone()), - pty, - pty_config.hold, - false, - ); - - //Kick things off - let pty_tx = event_loop.channel(); - let _io_thread = event_loop.spawn(); - - cx.spawn_weak(|this, mut cx| async move { - //Listen for terminal events - while let Some(event) = events_rx.next().await { - match this.upgrade(&cx) { - Some(this) => { - this.update(&mut cx, |this, cx| { - this.process_terminal_event(event, cx); - cx.notify(); - }); - } - None => break, - } - } - }) - .detach(); - - TerminalConnection { - pty_tx: Notifier(pty_tx), - term, - title: DEFAULT_TITLE.to_string(), - associated_directory: working_directory, - } - } - - ///Takes events from Alacritty and translates them to behavior on this view - fn process_terminal_event( - &mut self, - event: alacritty_terminal::event::Event, - cx: &mut ModelContext, - ) { - match event { - // TODO: Handle is_self_focused in subscription on terminal view - AlacTermEvent::Wakeup => { - cx.emit(Event::Wakeup); - } - AlacTermEvent::PtyWrite(out) => self.write_to_pty(out), - AlacTermEvent::MouseCursorDirty => { - //Calculate new cursor style. - //TODO: alacritty/src/input.rs:L922-L939 - //Check on correctly handling mouse events for terminals - cx.platform().set_cursor_style(CursorStyle::Arrow); //??? - } - AlacTermEvent::Title(title) => { - self.title = title; - cx.emit(Event::TitleChanged); - } - AlacTermEvent::ResetTitle => { - self.title = DEFAULT_TITLE.to_string(); - cx.emit(Event::TitleChanged); - } - AlacTermEvent::ClipboardStore(_, data) => { - cx.write_to_clipboard(ClipboardItem::new(data)) - } - AlacTermEvent::ClipboardLoad(_, format) => self.write_to_pty(format( - &cx.read_from_clipboard() - .map(|ci| ci.text().to_string()) - .unwrap_or("".to_string()), - )), - AlacTermEvent::ColorRequest(index, format) => { - let color = self.term.lock().colors()[index].unwrap_or_else(|| { - let term_style = &cx.global::().theme.terminal; - to_alac_rgb(get_color_at_index(&index, &term_style.colors)) - }); - self.write_to_pty(format(color)) - } - AlacTermEvent::CursorBlinkingChange => { - //TODO: Set a timer to blink the cursor on and off - } - AlacTermEvent::Bell => { - cx.emit(Event::Bell); - } - AlacTermEvent::Exit => cx.emit(Event::CloseTerminal), - } - } - - ///Write the Input payload to the tty. This locks the terminal so we can scroll it. - pub fn write_to_pty(&mut self, input: String) { - self.write_bytes_to_pty(input.into_bytes()); - } - - ///Write the Input payload to the tty. This locks the terminal so we can scroll it. - fn write_bytes_to_pty(&mut self, input: Vec) { - self.term.lock().scroll_display(Scroll::Bottom); - self.pty_tx.notify(input); - } - - ///Resize the terminal and the PTY. This locks the terminal. - pub fn set_size(&mut self, new_size: SizeInfo) { - self.pty_tx.0.send(Msg::Resize(new_size)).ok(); - self.term.lock().resize(new_size); - } - - pub fn clear(&mut self) { - self.write_to_pty("\x0c".into()); - self.term.lock().clear_screen(ClearMode::Saved); - } - - pub fn try_keystroke(&mut self, keystroke: &Keystroke) -> bool { - let guard = self.term.lock(); - let mode = guard.mode(); - let esc = to_esc_str(keystroke, mode); - drop(guard); - if esc.is_some() { - self.write_to_pty(esc.unwrap()); - true - } else { - false - } - } - - ///Paste text into the terminal - pub fn paste(&mut self, text: &str) { - if self.term.lock().mode().contains(TermMode::BRACKETED_PASTE) { - self.write_to_pty("\x1b[200~".to_string()); - self.write_to_pty(text.replace('\x1b', "").to_string()); - self.write_to_pty("\x1b[201~".to_string()); - } else { - self.write_to_pty(text.replace("\r\n", "\r").replace('\n', "\r")); - } - } -} - -impl Drop for TerminalConnection { - fn drop(&mut self) { - self.pty_tx.0.send(Msg::Shutdown).ok(); - } -} - -impl Entity for TerminalConnection { - type Event = Event; -} diff --git a/crates/terminal/src/color_translation.rs b/crates/terminal/src/mappings/colors.rs similarity index 98% rename from crates/terminal/src/color_translation.rs rename to crates/terminal/src/mappings/colors.rs index 946a22d304..1a425ebaed 100644 --- a/crates/terminal/src/color_translation.rs +++ b/crates/terminal/src/mappings/colors.rs @@ -133,7 +133,7 @@ mod tests { fn test_rgb_for_index() { //Test every possible value in the color cube for i in 16..=231 { - let (r, g, b) = crate::color_translation::rgb_for_index(&(i as u8)); + let (r, g, b) = crate::mappings::colors::rgb_for_index(&(i as u8)); assert_eq!(i, 16 + 36 * r + 6 * g + b); } } diff --git a/crates/terminal/src/connection/keymappings.rs b/crates/terminal/src/mappings/keys.rs similarity index 98% rename from crates/terminal/src/connection/keymappings.rs rename to crates/terminal/src/mappings/keys.rs index 36921a5b3e..93fd7b3454 100644 --- a/crates/terminal/src/connection/keymappings.rs +++ b/crates/terminal/src/mappings/keys.rs @@ -1,15 +1,6 @@ use alacritty_terminal::term::TermMode; use gpui::keymap::Keystroke; -/* -Connection events still to do: -- Reporting mouse events correctly. -- Reporting scrolls -- Correctly bracketing a paste -- Storing changed colors -- Focus change sequence -*/ - #[derive(Debug)] pub enum Modifiers { None, @@ -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; diff --git a/crates/terminal/src/mappings/mod.rs b/crates/terminal/src/mappings/mod.rs new file mode 100644 index 0000000000..cde6c337ea --- /dev/null +++ b/crates/terminal/src/mappings/mod.rs @@ -0,0 +1,2 @@ +pub mod colors; +pub mod keys; diff --git a/crates/terminal/src/modal.rs b/crates/terminal/src/modal.rs deleted file mode 100644 index 708f96856b..0000000000 --- a/crates/terminal/src/modal.rs +++ /dev/null @@ -1,63 +0,0 @@ -use gpui::{ModelHandle, ViewContext}; -use workspace::Workspace; - -use crate::{get_wd_for_workspace, DeployModal, Event, Terminal, TerminalConnection}; - -#[derive(Debug)] -struct StoredConnection(ModelHandle); - -pub fn deploy_modal(workspace: &mut Workspace, _: &DeployModal, cx: &mut ViewContext) { - // Pull the terminal connection out of the global if it has been stored - let possible_connection = - cx.update_default_global::, _, _>(|possible_connection, _| { - possible_connection.take() - }); - - if let Some(StoredConnection(stored_connection)) = possible_connection { - // Create a view from the stored connection - workspace.toggle_modal(cx, |_, cx| { - cx.add_view(|cx| Terminal::from_connection(stored_connection.clone(), true, cx)) - }); - cx.set_global::>(Some(StoredConnection( - stored_connection.clone(), - ))); - } else { - // No connection was stored, create a new terminal - if let Some(closed_terminal_handle) = workspace.toggle_modal(cx, |workspace, cx| { - let wd = get_wd_for_workspace(workspace, cx); - let this = cx.add_view(|cx| Terminal::new(wd, true, cx)); - let connection_handle = this.read(cx).connection.clone(); - cx.subscribe(&connection_handle, on_event).detach(); - //Set the global immediately, in case the user opens the command palette - cx.set_global::>(Some(StoredConnection( - connection_handle.clone(), - ))); - this - }) { - let connection = closed_terminal_handle.read(cx).connection.clone(); - cx.set_global(Some(StoredConnection(connection))); - } - } - - //The problem is that the terminal modal is never re-stored. -} - -pub fn on_event( - workspace: &mut Workspace, - _: ModelHandle, - event: &Event, - cx: &mut ViewContext, -) { - // Dismiss the modal if the terminal quit - if let Event::CloseTerminal = event { - cx.set_global::>(None); - if workspace - .modal() - .cloned() - .and_then(|modal| modal.downcast::()) - .is_some() - { - workspace.dismiss_modal(cx) - } - } -} diff --git a/crates/terminal/src/modal_view.rs b/crates/terminal/src/modal_view.rs new file mode 100644 index 0000000000..ec5280befc --- /dev/null +++ b/crates/terminal/src/modal_view.rs @@ -0,0 +1,73 @@ +use gpui::{ModelHandle, ViewContext}; +use workspace::Workspace; + +use crate::{ + get_working_directory, model::Terminal, DeployModal, Event, TerminalContent, TerminalView, +}; + +#[derive(Debug)] +struct StoredTerminal(ModelHandle); + +pub fn deploy_modal(workspace: &mut Workspace, _: &DeployModal, cx: &mut ViewContext) { + // Pull the terminal connection out of the global if it has been stored + let possible_terminal = + cx.update_default_global::, _, _>(|possible_connection, _| { + possible_connection.take() + }); + + if let Some(StoredTerminal(stored_terminal)) = possible_terminal { + workspace.toggle_modal(cx, |_, cx| { + // Create a view from the stored connection if the terminal modal is not already shown + cx.add_view(|cx| TerminalView::from_terminal(stored_terminal.clone(), true, cx)) + }); + // Toggle Modal will dismiss the terminal modal if it is currently shown, so we must + // store the terminal back in the global + cx.set_global::>(Some(StoredTerminal(stored_terminal.clone()))); + } else { + // No connection was stored, create a new terminal + if let Some(closed_terminal_handle) = workspace.toggle_modal(cx, |workspace, cx| { + // No terminal modal visible, construct a new one. + let working_directory = get_working_directory(workspace, cx); + + let this = cx.add_view(|cx| TerminalView::new(working_directory, true, cx)); + + if let TerminalContent::Connected(connected) = &this.read(cx).content { + let terminal_handle = connected.read(cx).handle(); + cx.subscribe(&terminal_handle, on_event).detach(); + // Set the global immediately if terminal construction was successful, + // in case the user opens the command palette + cx.set_global::>(Some(StoredTerminal( + terminal_handle.clone(), + ))); + } + + this + }) { + // Terminal modal was dismissed. Store terminal if the terminal view is connected + if let TerminalContent::Connected(connected) = &closed_terminal_handle.read(cx).content + { + let terminal_handle = connected.read(cx).handle(); + // Set the global immediately if terminal construction was successful, + // in case the user opens the command palette + cx.set_global::>(Some(StoredTerminal( + terminal_handle.clone(), + ))); + } + } + } +} + +pub fn on_event( + workspace: &mut Workspace, + _: ModelHandle, + event: &Event, + cx: &mut ViewContext, +) { + // Dismiss the modal if the terminal quit + if let Event::CloseTerminal = event { + cx.set_global::>(None); + if workspace.modal::().is_some() { + workspace.dismiss_modal(cx) + } + } +} diff --git a/crates/terminal/src/model.rs b/crates/terminal/src/model.rs new file mode 100644 index 0000000000..f1b2dd36cf --- /dev/null +++ b/crates/terminal/src/model.rs @@ -0,0 +1,522 @@ +use alacritty_terminal::{ + ansi::{ClearMode, Handler}, + config::{Config, Program, PtyConfig}, + event::{Event as AlacTermEvent, EventListener, Notify, WindowSize}, + event_loop::{EventLoop, Msg, Notifier}, + grid::Scroll, + index::{Direction, Point}, + selection::{Selection, SelectionType}, + sync::FairMutex, + term::{test::TermSize, RenderableContent, TermMode}, + tty::{self, setup_env}, + Term, +}; +use anyhow::{bail, Result}; +use futures::{ + channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender}, + StreamExt, +}; +use settings::{Settings, Shell}; +use std::{collections::HashMap, fmt::Display, path::PathBuf, sync::Arc}; +use thiserror::Error; + +use gpui::{keymap::Keystroke, ClipboardItem, CursorStyle, Entity, ModelContext}; + +use crate::{ + connected_el::TermDimensions, + mappings::{ + colors::{get_color_at_index, to_alac_rgb}, + keys::to_esc_str, + }, +}; + +const DEFAULT_TITLE: &str = "Terminal"; + +///Upward flowing events, for changing the title and such +#[derive(Copy, Clone, Debug)] +pub enum Event { + TitleChanged, + CloseTerminal, + Activate, + Wakeup, + Bell, + KeyInput, +} + +///A translation struct for Alacritty to communicate with us from their event loop +#[derive(Clone)] +pub struct ZedListener(UnboundedSender); + +impl EventListener for ZedListener { + fn send_event(&self, event: AlacTermEvent) { + self.0.unbounded_send(event).ok(); + } +} + +#[derive(Error, Debug)] +pub struct TerminalError { + pub directory: Option, + pub shell: Option, + pub source: std::io::Error, +} + +impl TerminalError { + pub fn fmt_directory(&self) -> String { + self.directory + .clone() + .map(|path| { + match path + .into_os_string() + .into_string() + .map_err(|os_str| format!(" {}", os_str.to_string_lossy())) + { + Ok(s) => s, + Err(s) => s, + } + }) + .unwrap_or_else(|| { + let default_dir = + dirs::home_dir().map(|buf| buf.into_os_string().to_string_lossy().to_string()); + match default_dir { + Some(dir) => format!(" {}", dir), + None => "".to_string(), + } + }) + } + + pub fn shell_to_string(&self) -> Option { + self.shell.as_ref().map(|shell| match shell { + Shell::System => "".to_string(), + Shell::Program(p) => p.to_string(), + Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")), + }) + } + + pub fn fmt_shell(&self) -> String { + self.shell + .clone() + .map(|shell| match shell { + Shell::System => { + let mut buf = [0; 1024]; + let pw = alacritty_unix::get_pw_entry(&mut buf).ok(); + + match pw { + Some(pw) => format!(" {}", pw.shell), + None => "".to_string(), + } + } + Shell::Program(s) => s, + Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")), + }) + .unwrap_or_else(|| { + let mut buf = [0; 1024]; + let pw = alacritty_unix::get_pw_entry(&mut buf).ok(); + match pw { + Some(pw) => { + format!(" {}", pw.shell) + } + None => " {}".to_string(), + } + }) + } +} + +impl Display for TerminalError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let dir_string: String = self.fmt_directory(); + + let shell = self.fmt_shell(); + + write!( + f, + "Working directory: {} Shell command: `{}`, IOError: {}", + dir_string, shell, self.source + ) + } +} + +pub struct TerminalBuilder { + terminal: Terminal, + events_rx: UnboundedReceiver, +} + +impl TerminalBuilder { + pub fn new( + working_directory: Option, + shell: Option, + env: Option>, + initial_size: TermDimensions, + ) -> Result { + let pty_config = { + let alac_shell = shell.clone().and_then(|shell| match shell { + Shell::System => None, + Shell::Program(program) => Some(Program::Just(program)), + Shell::WithArguments { program, args } => Some(Program::WithArgs { program, args }), + }); + + PtyConfig { + shell: alac_shell, + working_directory: working_directory.clone(), + hold: false, + } + }; + + let mut env = env.unwrap_or_else(|| HashMap::new()); + + //TODO: Properly set the current locale, + env.insert("LC_ALL".to_string(), "en_US.UTF-8".to_string()); + + let config = Config { + pty_config: pty_config.clone(), + env, + ..Default::default() + }; + + setup_env(&config); + + //Spawn a task so the Alacritty EventLoop can communicate with us in a view context + let (events_tx, events_rx) = unbounded(); + + //Set up the terminal... + let term = Term::new(&config, &initial_size, ZedListener(events_tx.clone())); + let term = Arc::new(FairMutex::new(term)); + + //Setup the pty... + let pty = match tty::new(&pty_config, initial_size.into(), None) { + Ok(pty) => pty, + Err(error) => { + bail!(TerminalError { + directory: working_directory, + shell, + source: error, + }); + } + }; + + let shell_txt = { + match shell { + Some(Shell::System) | None => { + let mut buf = [0; 1024]; + let pw = alacritty_unix::get_pw_entry(&mut buf).unwrap(); + pw.shell.to_string() + } + Some(Shell::Program(program)) => program, + Some(Shell::WithArguments { program, args }) => { + format!("{} {}", program, args.join(" ")) + } + } + }; + + //And connect them together + let event_loop = EventLoop::new( + term.clone(), + ZedListener(events_tx.clone()), + pty, + pty_config.hold, + false, + ); + + //Kick things off + let pty_tx = event_loop.channel(); + let _io_thread = event_loop.spawn(); + + let terminal = Terminal { + pty_tx: Notifier(pty_tx), + term, + title: shell_txt.to_string(), + }; + + Ok(TerminalBuilder { + terminal, + events_rx, + }) + } + + pub fn subscribe(mut self, cx: &mut ModelContext) -> Terminal { + cx.spawn_weak(|this, mut cx| async move { + //Listen for terminal events + while let Some(event) = self.events_rx.next().await { + match this.upgrade(&cx) { + Some(this) => { + this.update(&mut cx, |this, cx| { + this.process_terminal_event(event, cx); + + cx.notify(); + }); + } + None => break, + } + } + }) + .detach(); + + self.terminal + } +} + +pub struct Terminal { + pty_tx: Notifier, + term: Arc>>, + pub title: String, +} + +impl Terminal { + ///Takes events from Alacritty and translates them to behavior on this view + fn process_terminal_event( + &mut self, + event: alacritty_terminal::event::Event, + cx: &mut ModelContext, + ) { + match event { + // TODO: Handle is_self_focused in subscription on terminal view + AlacTermEvent::Wakeup => { + cx.emit(Event::Wakeup); + } + AlacTermEvent::PtyWrite(out) => self.write_to_pty(out), + AlacTermEvent::MouseCursorDirty => { + //Calculate new cursor style. + //TODO: alacritty/src/input.rs:L922-L939 + //Check on correctly handling mouse events for terminals + cx.platform().set_cursor_style(CursorStyle::Arrow); //??? + } + AlacTermEvent::Title(title) => { + self.title = title; + cx.emit(Event::TitleChanged); + } + AlacTermEvent::ResetTitle => { + self.title = DEFAULT_TITLE.to_string(); + cx.emit(Event::TitleChanged); + } + AlacTermEvent::ClipboardStore(_, data) => { + cx.write_to_clipboard(ClipboardItem::new(data)) + } + AlacTermEvent::ClipboardLoad(_, format) => self.write_to_pty(format( + &cx.read_from_clipboard() + .map(|ci| ci.text().to_string()) + .unwrap_or("".to_string()), + )), + AlacTermEvent::ColorRequest(index, format) => { + let color = self.term.lock().colors()[index].unwrap_or_else(|| { + let term_style = &cx.global::().theme.terminal; + to_alac_rgb(get_color_at_index(&index, &term_style.colors)) + }); + self.write_to_pty(format(color)) + } + AlacTermEvent::CursorBlinkingChange => { + //TODO: Set a timer to blink the cursor on and off + } + AlacTermEvent::Bell => { + cx.emit(Event::Bell); + } + AlacTermEvent::Exit => cx.emit(Event::CloseTerminal), + AlacTermEvent::TextAreaSizeRequest(_) => println!("Received text area resize request"), + } + } + + ///Write the Input payload to the tty. This locks the terminal so we can scroll it. + pub fn write_to_pty(&self, input: String) { + self.write_bytes_to_pty(input.into_bytes()); + } + + ///Write the Input payload to the tty. This locks the terminal so we can scroll it. + fn write_bytes_to_pty(&self, input: Vec) { + self.term.lock().scroll_display(Scroll::Bottom); + self.pty_tx.notify(input); + } + + ///Resize the terminal and the PTY. This locks the terminal. + pub fn set_size(&self, new_size: WindowSize) { + self.pty_tx.0.send(Msg::Resize(new_size)).ok(); + + let term_size = TermSize::new(new_size.num_cols as usize, new_size.num_lines as usize); + self.term.lock().resize(term_size); + } + + pub fn clear(&self) { + self.write_to_pty("\x0c".into()); + self.term.lock().clear_screen(ClearMode::Saved); + } + + pub fn try_keystroke(&self, keystroke: &Keystroke) -> bool { + let guard = self.term.lock(); + let mode = guard.mode(); + let esc = to_esc_str(keystroke, mode); + drop(guard); + if esc.is_some() { + self.write_to_pty(esc.unwrap()); + true + } else { + false + } + } + + ///Paste text into the terminal + pub fn paste(&self, text: &str) { + if self.term.lock().mode().contains(TermMode::BRACKETED_PASTE) { + self.write_to_pty("\x1b[200~".to_string()); + self.write_to_pty(text.replace('\x1b', "").to_string()); + self.write_to_pty("\x1b[201~".to_string()); + } else { + self.write_to_pty(text.replace("\r\n", "\r").replace('\n', "\r")); + } + } + + pub fn copy(&self) -> Option { + let term = self.term.lock(); + term.selection_to_string() + } + + ///Takes the selection out of the terminal + pub fn take_selection(&self) -> Option { + self.term.lock().selection.take() + } + ///Sets the selection object on the terminal + pub fn set_selection(&self, sel: Option) { + self.term.lock().selection = sel; + } + + pub fn render_lock(&self, new_size: Option, f: F) -> T + where + F: FnOnce(RenderableContent, char) -> T, + { + if let Some(new_size) = new_size { + self.pty_tx.0.send(Msg::Resize(new_size.into())).ok(); //Give the PTY a chance to react to the new size + //TODO: Is this bad for performance? + } + + let mut term = self.term.lock(); //Lock + + if let Some(new_size) = new_size { + term.resize(new_size); //Reflow + } + + let content = term.renderable_content(); + let cursor_text = term.grid()[content.cursor.point].c; + + f(content, cursor_text) + } + + pub fn get_display_offset(&self) -> usize { + self.term.lock().renderable_content().display_offset + } + + ///Scroll the terminal + pub fn scroll(&self, scroll: Scroll) { + self.term.lock().scroll_display(scroll) + } + + pub fn click(&self, point: Point, side: Direction, clicks: usize) { + let selection_type = match clicks { + 0 => return, //This is a release + 1 => Some(SelectionType::Simple), + 2 => Some(SelectionType::Semantic), + 3 => Some(SelectionType::Lines), + _ => None, + }; + + let selection = + selection_type.map(|selection_type| Selection::new(selection_type, point, side)); + + self.set_selection(selection); + } + + pub fn drag(&self, point: Point, side: Direction) { + if let Some(mut selection) = self.take_selection() { + selection.update(point, side); + self.set_selection(Some(selection)); + } + } + + pub fn mouse_down(&self, point: Point, side: Direction) { + self.set_selection(Some(Selection::new(SelectionType::Simple, point, side))); + } +} + +impl Drop for Terminal { + fn drop(&mut self) { + self.pty_tx.0.send(Msg::Shutdown).ok(); + } +} + +impl Entity for Terminal { + type Event = Event; +} + +//TODO Move this around +mod alacritty_unix { + use alacritty_terminal::config::Program; + use gpui::anyhow::{bail, Result}; + use libc; + use std::ffi::CStr; + use std::mem::MaybeUninit; + use std::ptr; + + #[derive(Debug)] + pub struct Passwd<'a> { + _name: &'a str, + _dir: &'a str, + pub shell: &'a str, + } + + /// Return a Passwd struct with pointers into the provided buf. + /// + /// # Unsafety + /// + /// If `buf` is changed while `Passwd` is alive, bad thing will almost certainly happen. + pub fn get_pw_entry(buf: &mut [i8; 1024]) -> Result> { + // Create zeroed passwd struct. + let mut entry: MaybeUninit = MaybeUninit::uninit(); + + let mut res: *mut libc::passwd = ptr::null_mut(); + + // Try and read the pw file. + let uid = unsafe { libc::getuid() }; + let status = unsafe { + libc::getpwuid_r( + uid, + entry.as_mut_ptr(), + buf.as_mut_ptr() as *mut _, + buf.len(), + &mut res, + ) + }; + let entry = unsafe { entry.assume_init() }; + + if status < 0 { + bail!("getpwuid_r failed"); + } + + if res.is_null() { + bail!("pw not found"); + } + + // Sanity check. + assert_eq!(entry.pw_uid, uid); + + // Build a borrowed Passwd struct. + Ok(Passwd { + _name: unsafe { CStr::from_ptr(entry.pw_name).to_str().unwrap() }, + _dir: unsafe { CStr::from_ptr(entry.pw_dir).to_str().unwrap() }, + shell: unsafe { CStr::from_ptr(entry.pw_shell).to_str().unwrap() }, + }) + } + + #[cfg(target_os = "macos")] + pub fn _default_shell(pw: &Passwd<'_>) -> Program { + let shell_name = pw.shell.rsplit('/').next().unwrap(); + let argv = vec![ + String::from("-c"), + format!("exec -a -{} {}", shell_name, pw.shell), + ]; + + Program::WithArgs { + program: "/bin/bash".to_owned(), + args: argv, + } + } + + #[cfg(not(target_os = "macos"))] + pub fn default_shell(pw: &Passwd<'_>) -> Program { + Program::Just(env::var("SHELL").unwrap_or_else(|_| pw.shell.to_owned())) + } +} diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 4aa49cccba..2f5ef5ffab 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -1,257 +1,168 @@ -mod color_translation; -pub mod connection; -mod modal; -pub mod terminal_element; +pub mod connected_el; +pub mod connected_view; +pub mod mappings; +pub mod modal_view; +pub mod model; -use alacritty_terminal::{ - event::{Event as AlacTermEvent, EventListener}, - term::SizeInfo, -}; - -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); +//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), + Error(ViewHandle), +} + +impl TerminalContent { + fn handle(&self) -> AnyViewHandle { + match self { + Self::Connected(handle) => handle.into(), + Self::Error(handle) => handle.into(), + } } } -///A terminal view, maintains the PTY's file handles and communicates with the terminal -pub struct Terminal { - connection: ModelHandle, - has_new_content: bool, - //Currently using iTerm bell, show bell emoji in tab until input is received - has_bell: bool, - // Only for styling purposes. Doesn't effect behavior +pub struct TerminalView { modal: bool, + content: TerminalContent, + associated_directory: Option, } -impl Entity for Terminal { +pub struct ErrorView { + error: TerminalError, +} + +impl Entity for TerminalView { type Event = Event; } -impl Terminal { +impl Entity for ConnectedView { + type Event = Event; +} + +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) { + let working_directory = get_working_directory(workspace, cx); + let view = cx.add_view(|cx| TerminalView::new(working_directory, false, cx)); + workspace.add_item(Box::new(view), cx); + } + ///Create a new Terminal view. This spawns a task, a thread, and opens the TTY devices ///To get the right working directory from a workspace, use: `get_wd_for_workspace()` fn new(working_directory: Option, modal: bool, cx: &mut ViewContext) -> Self { //The details here don't matter, the terminal will be resized on the first layout - let size_info = SizeInfo::new( - DEBUG_TERMINAL_WIDTH, - DEBUG_TERMINAL_HEIGHT, - DEBUG_CELL_WIDTH, + let size_info = TermDimensions::new( DEBUG_LINE_HEIGHT, - 0., - 0., - false, + DEBUG_CELL_WIDTH, + vec2f(DEBUG_TERMINAL_WIDTH, DEBUG_TERMINAL_HEIGHT), ); - let (shell, envs) = { - let settings = cx.global::(); - let shell = settings.terminal_overrides.shell.clone(); - let envs = settings.terminal_overrides.env.clone(); //Should be short and cheap. - (shell, envs) + let settings = cx.global::(); + let shell = settings.terminal_overrides.shell.clone(); + let envs = settings.terminal_overrides.env.clone(); //Should be short and cheap. + + let content = match TerminalBuilder::new(working_directory.clone(), shell, envs, size_info) + { + Ok(terminal) => { + let terminal = cx.add_model(|cx| terminal.subscribe(cx)); + let view = cx.add_view(|cx| ConnectedView::from_terminal(terminal, modal, cx)); + cx.subscribe(&view, |_this, _content, event, cx| cx.emit(event.clone())) + .detach(); + TerminalContent::Connected(view) + } + Err(error) => { + let view = cx.add_view(|_| ErrorView { + error: error.downcast::().unwrap(), + }); + TerminalContent::Error(view) + } }; + cx.focus(content.handle()); - let connection = cx - .add_model(|cx| TerminalConnection::new(working_directory, shell, envs, size_info, cx)); - - Terminal::from_connection(connection, modal, cx) + TerminalView { + modal, + content, + associated_directory: working_directory, + } } - fn from_connection( - connection: ModelHandle, + fn from_terminal( + terminal: ModelHandle, modal: bool, cx: &mut ViewContext, - ) -> Terminal { - cx.observe(&connection, |_, _, cx| cx.notify()).detach(); - cx.subscribe(&connection, |this, _, event, cx| match event { - Event::Wakeup => { - if cx.is_self_focused() { - cx.notify() - } else { - this.has_new_content = true; - cx.emit(Event::TitleChanged); - } - } - Event::Bell => { - this.has_bell = true; - cx.emit(Event::TitleChanged); - } - _ => cx.emit(*event), - }) - .detach(); - - Terminal { - connection, - has_new_content: true, - has_bell: false, + ) -> Self { + let connected_view = cx.add_view(|cx| ConnectedView::from_terminal(terminal, modal, cx)); + TerminalView { modal, + content: TerminalContent::Connected(connected_view), + associated_directory: None, } } - - fn input(&mut self, text: &str, cx: &mut ViewContext) { - self.connection.update(cx, |connection, _| { - //TODO: This is probably not encoding UTF8 correctly (see alacritty/src/input.rs:L825-837) - connection.write_to_pty(text.to_string()); - }); - - if self.has_bell { - self.has_bell = false; - cx.emit(Event::TitleChanged); - } - } - - fn clear(&mut self, _: &Clear, cx: &mut ViewContext) { - self.connection - .update(cx, |connection, _| connection.clear()); - } - - ///Create a new Terminal in the current working directory or the user's home directory - fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { - let wd = get_wd_for_workspace(workspace, cx); - workspace.add_item(Box::new(cx.add_view(|cx| Terminal::new(wd, false, cx))), cx); - } - - ///Attempt to paste the clipboard into the terminal - fn copy(&mut self, _: &Copy, cx: &mut ViewContext) { - let term = self.connection.read(cx).term.lock(); - let copy_text = term.selection_to_string(); - match copy_text { - Some(s) => cx.write_to_clipboard(ClipboardItem::new(s)), - None => (), - } - } - - ///Attempt to paste the clipboard into the terminal - fn paste(&mut self, _: &Paste, cx: &mut ViewContext) { - if let Some(item) = cx.read_from_clipboard() { - self.connection.update(cx, |connection, _| { - connection.paste(item.text()); - }) - } - } - - ///Synthesize the keyboard event corresponding to 'up' - fn up(&mut self, _: &Up, cx: &mut ViewContext) { - self.connection.update(cx, |connection, _| { - connection.try_keystroke(&Keystroke::parse("up").unwrap()); - }); - } - - ///Synthesize the keyboard event corresponding to 'down' - fn down(&mut self, _: &Down, cx: &mut ViewContext) { - self.connection.update(cx, |connection, _| { - connection.try_keystroke(&Keystroke::parse("down").unwrap()); - }); - } - - ///Synthesize the keyboard event corresponding to 'ctrl-c' - fn ctrl_c(&mut self, _: &CtrlC, cx: &mut ViewContext) { - self.connection.update(cx, |connection, _| { - connection.try_keystroke(&Keystroke::parse("ctrl-c").unwrap()); - }); - } - - ///Synthesize the keyboard event corresponding to 'escape' - fn escape(&mut self, _: &Escape, cx: &mut ViewContext) { - self.connection.update(cx, |connection, _| { - connection.try_keystroke(&Keystroke::parse("escape").unwrap()); - }); - } - - ///Synthesize the keyboard event corresponding to 'enter' - fn enter(&mut self, _: &Enter, cx: &mut ViewContext) { - self.connection.update(cx, |connection, _| { - connection.try_keystroke(&Keystroke::parse("enter").unwrap()); - }); - } } -impl View for Terminal { +impl View for TerminalView { fn ui_name() -> &'static str { "Terminal" } fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox { - let element = { - let connection_handle = self.connection.clone().downgrade(); - let view_id = cx.view_id(); - TerminalEl::new(view_id, connection_handle, self.modal).contained() + let child_view = match &self.content { + TerminalContent::Connected(connected) => ChildView::new(connected), + TerminalContent::Error(error) => ChildView::new(error), }; if self.modal { let settings = cx.global::(); let container_style = settings.theme.terminal.modal_container; - element.with_style(container_style).boxed() + child_view.contained().with_style(container_style).boxed() } else { - element.boxed() + child_view.boxed() } } fn on_focus(&mut self, cx: &mut ViewContext) { cx.emit(Event::Activate); - self.has_new_content = false; + cx.defer(|view, cx| { + cx.focus(view.content.handle()); + }); } fn keymap_context(&self, _: &gpui::AppContext) -> gpui::keymap::Context { @@ -261,67 +172,83 @@ impl View for Terminal { } context } +} - fn selected_text_range(&self, _: &AppContext) -> Option> { - Some(0..0) +impl View for ErrorView { + fn ui_name() -> &'static str { + "Terminal Error" } - fn replace_text_in_range( - &mut self, - _: Option>, - text: &str, - cx: &mut ViewContext, - ) { - self.input(text, cx); + fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox { + let settings = cx.global::(); + let style = TerminalEl::make_text_style(cx.font_cache(), settings); + + //TODO: + //We want markdown style highlighting so we can format the program and working directory with `` + //We want a max-width of 75% with word-wrap + //We want to be able to select the text + //Want to be able to scroll if the error message is massive somehow (resiliency) + + let program_text = { + match self.error.shell_to_string() { + Some(shell_txt) => format!("Shell Program: `{}`", shell_txt), + None => "No program specified".to_string(), + } + }; + + let directory_text = { + match self.error.directory.as_ref() { + Some(path) => format!("Working directory: `{}`", path.to_string_lossy()), + None => "No working directory specified".to_string(), + } + }; + + let error_text = self.error.source.to_string(); + + Flex::column() + .with_child( + Text::new("Failed to open the terminal.".to_string(), style.clone()) + .contained() + .boxed(), + ) + .with_child(Text::new(program_text, style.clone()).contained().boxed()) + .with_child(Text::new(directory_text, style.clone()).contained().boxed()) + .with_child(Text::new(error_text, style.clone()).contained().boxed()) + .aligned() + .boxed() } } -impl Item for Terminal { +impl Item for TerminalView { fn tab_content( &self, _detail: Option, tab_theme: &theme::Tab, cx: &gpui::AppContext, ) -> ElementBox { - let settings = cx.global::(); - let search_theme = &settings.theme.search; //TODO properly integrate themes - - let mut flex = Flex::row(); - - if self.has_bell { - flex.add_child( - Svg::new("icons/bolt_12.svg") //TODO: Swap out for a better icon, or at least resize this - .with_color(tab_theme.label.text.color) - .constrained() - .with_width(search_theme.tab_icon_width) - .aligned() - .boxed(), - ); + let title = match &self.content { + TerminalContent::Connected(connected) => { + connected.read(cx).handle().read(cx).title.clone() + } + TerminalContent::Error(_) => "Terminal".to_string(), }; - flex.with_child( - Label::new( - self.connection.read(cx).title.clone(), - tab_theme.label.clone(), + Flex::row() + .with_child( + Label::new(title, tab_theme.label.clone()) + .aligned() + .contained() + .boxed(), ) - .aligned() - .contained() - .with_margin_left(if self.has_bell { - search_theme.tab_icon_spacing - } else { - 0. - }) - .boxed(), - ) - .boxed() + .boxed() } fn clone_on_split(&self, cx: &mut ViewContext) -> Option { //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 { +fn get_working_directory(workspace: &Workspace, cx: &AppContext) -> Option { let wd_setting = cx .global::() .terminal_overrides @@ -399,10 +338,12 @@ fn get_wd_for_workspace(workspace: &Workspace, cx: &AppContext) -> Option current_project_directory(workspace, cx), WorkingDirectory::FirstProjectDirectory => first_project_directory(workspace, cx), WorkingDirectory::AlwaysHome => None, - WorkingDirectory::Always { directory } => shellexpand::full(&directory) - .ok() - .map(|dir| Path::new(&dir.to_string()).to_path_buf()) - .filter(|dir| dir.is_dir()), + WorkingDirectory::Always { directory } => { + shellexpand::full(&directory) //TODO handle this better + .ok() + .map(|dir| Path::new(&dir.to_string()).to_path_buf()) + .filter(|dir| dir.is_dir()) + } }; res.or_else(|| home_dir()) } @@ -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(); + let mut cx = TerminalTestContext::new(cx, true); + let (project, workspace) = cx.blank_workspace().await; + cx.create_file_wt(project.clone(), "/root.txt").await; - //Test - cx.read(|cx| { + cx.cx.read(|cx| { let workspace = workspace.read(cx); let active_entry = project.read(cx).active_entry(); @@ -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(); diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs deleted file mode 100644 index d2a85e22d7..0000000000 --- a/crates/terminal/src/terminal_element.rs +++ /dev/null @@ -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, - view_id: usize, - modal: bool, -} - -///New type pattern so I don't mix these two up -struct CellWidth(f32); -struct LineHeight(f32); - -struct LayoutLine { - cells: Vec, - highlighted_range: Option>, -} - -///New type pattern to ensure that we use adjusted mouse positions throughout the code base, rather than -struct PaneRelativePos(Vector2F); - -///Functionally the constructor for the PaneRelativePos type, mutates the mouse_position -fn relative_pos(mouse_position: Vector2F, origin: Vector2F) -> PaneRelativePos { - PaneRelativePos(mouse_position.sub(origin)) //Avoid the extra allocation by mutating -} - -#[derive(Clone, Debug, Default)] -struct LayoutCell { - point: Point, - text: Line, //NOTE TO SELF THIS IS BAD PERFORMANCE RN! - background_color: Color, -} - -impl LayoutCell { - fn new(point: Point, text: Line, background_color: Color) -> LayoutCell { - LayoutCell { - point, - text, - background_color, - } - } -} - -///The information generated during layout that is nescessary for painting -pub struct LayoutState { - layout_lines: Vec, - line_height: LineHeight, - em_width: CellWidth, - cursor: Option, - background_color: Color, - cur_size: SizeInfo, - terminal: Arc>>, - selection_color: Color, -} - -impl TerminalEl { - pub fn new( - view_id: usize, - connection: WeakModelHandle, - modal: bool, - ) -> TerminalEl { - TerminalEl { - view_id, - connection, - modal, - } - } -} - -impl Element for TerminalEl { - type LayoutState = LayoutState; - type PaintState = (); - - fn layout( - &mut self, - constraint: gpui::SizeConstraint, - cx: &mut gpui::LayoutContext, - ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) { - //Settings immutably borrows cx here for the settings and font cache - //and we need to modify the cx to resize the terminal. So instead of - //storing Settings or the font_cache(), we toss them ASAP and then reborrow later - let text_style = make_text_style(cx.font_cache(), cx.global::()); - let line_height = LineHeight(cx.font_cache().line_height(text_style.font_size)); - let cell_width = CellWidth( - cx.font_cache() - .em_advance(text_style.font_id, text_style.font_size), - ); - let connection_handle = self.connection.upgrade(cx).unwrap(); - - //Tell the view our new size. Requires a mutable borrow of cx and the view - let cur_size = make_new_size(constraint, &cell_width, &line_height); - //Note that set_size locks and mutates the terminal. - connection_handle.update(cx.app, |connection, _| connection.set_size(cur_size)); - - let (selection_color, terminal_theme) = { - let theme = &(cx.global::()).theme; - (theme.editor.selection.selection, &theme.terminal) - }; - - let terminal_mutex = connection_handle.read(cx).term.clone(); - let term = terminal_mutex.lock(); - let grid = term.grid(); - let cursor_point = grid.cursor.point; - let cursor_text = grid[cursor_point.line][cursor_point.column].c.to_string(); - - let content = term.renderable_content(); - - let layout_lines = layout_lines( - content.display_iter, - &text_style, - terminal_theme, - cx.text_layout_cache, - self.modal, - content.selection, - ); - - let block_text = cx.text_layout_cache.layout_str( - &cursor_text, - text_style.font_size, - &[( - cursor_text.len(), - RunStyle { - font_id: text_style.font_id, - color: terminal_theme.colors.background, - underline: Default::default(), - }, - )], - ); - - let cursor = get_cursor_shape( - content.cursor.point.line.0 as usize, - content.cursor.point.column.0 as usize, - content.display_offset, - &line_height, - &cell_width, - cur_size.total_lines(), - &block_text, - ) - .map(move |(cursor_position, block_width)| { - let block_width = if block_width != 0.0 { - block_width - } else { - cell_width.0 - }; - - Cursor::new( - cursor_position, - block_width, - line_height.0, - terminal_theme.colors.cursor, - CursorShape::Block, - Some(block_text.clone()), - ) - }); - drop(term); - - let background_color = if self.modal { - terminal_theme.colors.modal_background - } else { - terminal_theme.colors.background - }; - - ( - constraint.max, - LayoutState { - layout_lines, - line_height, - em_width: cell_width, - cursor, - cur_size, - background_color, - terminal: terminal_mutex, - selection_color, - }, - ) - } - - fn paint( - &mut self, - bounds: gpui::geometry::rect::RectF, - visible_bounds: gpui::geometry::rect::RectF, - layout: &mut Self::LayoutState, - cx: &mut gpui::PaintContext, - ) -> Self::PaintState { - //Setup element stuff - let clip_bounds = Some(visible_bounds); - - cx.paint_layer(clip_bounds, |cx| { - let cur_size = layout.cur_size.clone(); - let origin = bounds.origin() + vec2f(layout.em_width.0, 0.); - - //Elements are ephemeral, only at paint time do we know what could be clicked by a mouse - attach_mouse_handlers( - origin, - cur_size, - self.view_id, - &layout.terminal, - visible_bounds, - cx, - ); - - cx.paint_layer(clip_bounds, |cx| { - //Start with a background color - cx.scene.push_quad(Quad { - bounds: RectF::new(bounds.origin(), bounds.size()), - background: Some(layout.background_color), - border: Default::default(), - corner_radius: 0., - }); - - //Draw cell backgrounds - for layout_line in &layout.layout_lines { - for layout_cell in &layout_line.cells { - let position = vec2f( - (origin.x() + layout_cell.point.column as f32 * layout.em_width.0) - .floor(), - origin.y() + layout_cell.point.line as f32 * layout.line_height.0, - ); - let size = vec2f(layout.em_width.0.ceil(), layout.line_height.0); - - cx.scene.push_quad(Quad { - bounds: RectF::new(position, size), - background: Some(layout_cell.background_color), - border: Default::default(), - corner_radius: 0., - }) - } - } - }); - - //Draw Selection - cx.paint_layer(clip_bounds, |cx| { - let mut highlight_y = None; - let highlight_lines = layout - .layout_lines - .iter() - .filter_map(|line| { - if let Some(range) = &line.highlighted_range { - if let None = highlight_y { - highlight_y = Some( - origin.y() - + line.cells[0].point.line as f32 * layout.line_height.0, - ); - } - let start_x = origin.x() - + line.cells[range.start].point.column as f32 * layout.em_width.0; - let end_x = origin.x() - + line.cells[range.end].point.column as f32 * layout.em_width.0 - + layout.em_width.0; - - return Some(HighlightedRangeLine { start_x, end_x }); - } else { - return None; - } - }) - .collect::>(); - - if let Some(y) = highlight_y { - let hr = HighlightedRange { - start_y: y, //Need to change this - line_height: layout.line_height.0, - lines: highlight_lines, - color: layout.selection_color, - //Copied from editor. TODO: move to theme or something - corner_radius: 0.15 * layout.line_height.0, - }; - hr.paint(bounds, cx.scene); - } - }); - - cx.paint_layer(clip_bounds, |cx| { - for layout_line in &layout.layout_lines { - for layout_cell in &layout_line.cells { - let point = layout_cell.point; - - //Don't actually know the start_x for a line, until here: - let cell_origin = vec2f( - (origin.x() + point.column as f32 * layout.em_width.0).floor(), - origin.y() + point.line as f32 * layout.line_height.0, - ); - - layout_cell.text.paint( - cell_origin, - visible_bounds, - layout.line_height.0, - cx, - ); - } - } - }); - - //Draw cursor - if let Some(cursor) = &layout.cursor { - cx.paint_layer(clip_bounds, |cx| { - cursor.paint(origin, cx); - }) - } - - #[cfg(debug_assertions)] - if DEBUG_GRID { - cx.paint_layer(clip_bounds, |cx| { - draw_debug_grid(bounds, layout, cx); - }) - } - }); - } - - fn dispatch_event( - &mut self, - event: &gpui::Event, - _bounds: gpui::geometry::rect::RectF, - visible_bounds: gpui::geometry::rect::RectF, - layout: &mut Self::LayoutState, - _paint: &mut Self::PaintState, - cx: &mut gpui::EventContext, - ) -> bool { - match event { - Event::ScrollWheel(ScrollWheelEvent { - delta, position, .. - }) => visible_bounds - .contains_point(*position) - .then(|| { - let vertical_scroll = - (delta.y() / layout.line_height.0) * ALACRITTY_SCROLL_MULTIPLIER; - - if let Some(connection) = self.connection.upgrade(cx.app) { - connection.update(cx.app, |connection, _| { - connection - .term - .lock() - .scroll_display(Scroll::Delta(vertical_scroll.round() as i32)); - }) - } - }) - .is_some(), - Event::KeyDown(KeyDownEvent { keystroke, .. }) => { - if !cx.is_parent_view_focused() { - return false; - } - - self.connection - .upgrade(cx.app) - .map(|connection| { - connection - .update(cx.app, |connection, _| connection.try_keystroke(keystroke)) - }) - .unwrap_or(false) - } - _ => false, - } - } - - fn rect_for_text_range( - &self, - _: Range, - bounds: RectF, - _: RectF, - layout: &Self::LayoutState, - _: &Self::PaintState, - _: &gpui::MeasurementContext, - ) -> Option { - // 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, - text_style: &TextStyle, - terminal_theme: &TerminalStyle, - text_layout_cache: &TextLayoutCache, - modal: bool, - selection_range: Option, -) -> Vec { - let lines = grid.group_by(|i| i.point.line); - lines - .into_iter() - .enumerate() - .map(|(line_index, (_, line))| { - let mut highlighted_range = None; - let cells = line - .enumerate() - .map(|(x_index, indexed_cell)| { - if selection_range - .map(|range| range.contains(indexed_cell.point)) - .unwrap_or(false) - { - let mut range = highlighted_range.take().unwrap_or(x_index..x_index); - range.end = range.end.max(x_index); - highlighted_range = Some(range); - } - - let cell_text = &indexed_cell.c.to_string(); - - let cell_style = cell_style(&indexed_cell, terminal_theme, text_style, modal); - - //This is where we might be able to get better performance - let layout_cell = text_layout_cache.layout_str( - cell_text, - text_style.font_size, - &[(cell_text.len(), cell_style)], - ); - - LayoutCell::new( - Point::new(line_index as i32, indexed_cell.point.column.0 as i32), - layout_cell, - convert_color(&indexed_cell.bg, &terminal_theme.colors, modal), - ) - }) - .collect::>(); - - LayoutLine { - cells, - highlighted_range, - } - }) - .collect::>() -} - -// Compute the cursor position and expected block width, may return a zero width if x_for_index returns -// the same position for sequential indexes. Use em_width instead -//TODO: This function is messy, too many arguments and too many ifs. Simplify. -fn get_cursor_shape( - line: usize, - line_index: usize, - display_offset: usize, - line_height: &LineHeight, - cell_width: &CellWidth, - total_lines: usize, - text_fragment: &Line, -) -> Option<(Vector2F, f32)> { - let cursor_line = line + display_offset; - if cursor_line <= total_lines { - let cursor_width = if text_fragment.width() == 0. { - cell_width.0 - } else { - text_fragment.width() - }; - - Some(( - vec2f( - line_index as f32 * cell_width.0, - cursor_line as f32 * line_height.0, - ), - cursor_width, - )) - } else { - None - } -} - -///Convert the Alacritty cell styles to GPUI text styles and background color -fn cell_style( - indexed: &Indexed<&Cell>, - style: &TerminalStyle, - text_style: &TextStyle, - modal: bool, -) -> RunStyle { - let flags = indexed.cell.flags; - let fg = convert_color(&indexed.cell.fg, &style.colors, modal); - - let underline = flags - .contains(Flags::UNDERLINE) - .then(|| Underline { - color: Some(fg), - squiggly: false, - thickness: OrderedFloat(1.), - }) - .unwrap_or_default(); - - RunStyle { - color: fg, - font_id: text_style.font_id, - underline, - } -} - -fn attach_mouse_handlers( - origin: Vector2F, - cur_size: SizeInfo, - view_id: usize, - terminal_mutex: &Arc>>, - visible_bounds: RectF, - cx: &mut PaintContext, -) { - let click_mutex = terminal_mutex.clone(); - let drag_mutex = terminal_mutex.clone(); - let mouse_down_mutex = terminal_mutex.clone(); - - cx.scene.push_mouse_region( - MouseRegion::new(view_id, None, visible_bounds) - .on_down( - MouseButton::Left, - move |MouseButtonEvent { position, .. }, _| { - let mut term = mouse_down_mutex.lock(); - - let (point, side) = mouse_to_cell_data( - position, - origin, - cur_size, - term.renderable_content().display_offset, - ); - term.selection = Some(Selection::new(SelectionType::Simple, point, side)) - }, - ) - .on_click( - MouseButton::Left, - move |MouseButtonEvent { - position, - click_count, - .. - }, - cx| { - let mut term = click_mutex.lock(); - - let (point, side) = mouse_to_cell_data( - position, - origin, - cur_size, - term.renderable_content().display_offset, - ); - - let selection_type = match click_count { - 0 => return, //This is a release - 1 => Some(SelectionType::Simple), - 2 => Some(SelectionType::Semantic), - 3 => Some(SelectionType::Lines), - _ => None, - }; - - let selection = selection_type - .map(|selection_type| Selection::new(selection_type, point, side)); - - term.selection = selection; - cx.focus_parent_view(); - cx.notify(); - }, - ) - .on_drag( - MouseButton::Left, - move |_, MouseMovedEvent { position, .. }, cx| { - let mut term = drag_mutex.lock(); - - let (point, side) = mouse_to_cell_data( - position, - origin, - cur_size, - term.renderable_content().display_offset, - ); - - if let Some(mut selection) = term.selection.take() { - selection.update(point, side); - term.selection = Some(selection); - } - - cx.notify(); - }, - ), - ); -} - -///Copied (with modifications) from alacritty/src/input.rs > Processor::cell_side() -fn cell_side(pos: &PaneRelativePos, cur_size: SizeInfo) -> Side { - let x = pos.0.x() as usize; - let cell_x = x.saturating_sub(cur_size.cell_width() as usize) % cur_size.cell_width() as usize; - let half_cell_width = (cur_size.cell_width() / 2.0) as usize; - - let additional_padding = - (cur_size.width() - cur_size.cell_width() * 2.) % cur_size.cell_width(); - let end_of_grid = cur_size.width() - cur_size.cell_width() - additional_padding; - - if cell_x > half_cell_width - // Edge case when mouse leaves the window. - || x as f32 >= end_of_grid - { - Side::Right - } else { - Side::Left - } -} - -///Copied (with modifications) from alacritty/src/event.rs > Mouse::point() -///Position is a pane-relative position. That means the top left corner of the mouse -///Region should be (0,0) -fn grid_cell(pos: &PaneRelativePos, cur_size: SizeInfo, display_offset: usize) -> Point { - let pos = pos.0; - let col = pos.x() / cur_size.cell_width(); //TODO: underflow... - let col = min(GridCol(col as usize), cur_size.last_column()); - - let line = pos.y() / cur_size.cell_height(); - let line = min(line as i32, cur_size.bottommost_line().0); - - //when clicking, need to ADD to get to the top left cell - //e.g. total_lines - viewport_height, THEN subtract display offset - //0 -> total_lines - viewport_height - display_offset + mouse_line - - Point::new(GridLine(line - display_offset as i32), col) -} - -///Draws the grid as Alacritty sees it. Useful for checking if there is an inconsistency between -///Display and conceptual grid. -#[cfg(debug_assertions)] -fn draw_debug_grid(bounds: RectF, layout: &mut LayoutState, cx: &mut PaintContext) { - let width = layout.cur_size.width(); - let height = layout.cur_size.height(); - //Alacritty uses 'as usize', so shall we. - for col in 0..(width / layout.em_width.0).round() as usize { - cx.scene.push_quad(Quad { - bounds: RectF::new( - bounds.origin() + vec2f((col + 1) as f32 * layout.em_width.0, 0.), - vec2f(1., height), - ), - background: Some(Color::green()), - border: Default::default(), - corner_radius: 0., - }); - } - for row in 0..((height / layout.line_height.0) + 1.0).round() as usize { - cx.scene.push_quad(Quad { - bounds: RectF::new( - bounds.origin() + vec2f(layout.em_width.0, row as f32 * layout.line_height.0), - vec2f(width, 1.), - ), - background: Some(Color::green()), - border: Default::default(), - corner_radius: 0., - }); - } -} - -mod test { - - #[test] - fn test_mouse_to_selection() { - let term_width = 100.; - let term_height = 200.; - let cell_width = 10.; - let line_height = 20.; - let mouse_pos_x = 100.; //Window relative - let mouse_pos_y = 100.; //Window relative - let origin_x = 10.; - let origin_y = 20.; - - let cur_size = alacritty_terminal::term::SizeInfo::new( - term_width, - term_height, - cell_width, - line_height, - 0., - 0., - false, - ); - - let mouse_pos = gpui::geometry::vector::vec2f(mouse_pos_x, mouse_pos_y); - let origin = gpui::geometry::vector::vec2f(origin_x, origin_y); //Position of terminal window, 1 'cell' in - let (point, _) = - crate::terminal_element::mouse_to_cell_data(mouse_pos, origin, cur_size, 0); - assert_eq!( - point, - alacritty_terminal::index::Point::new( - alacritty_terminal::index::Line(((mouse_pos_y - origin_y) / line_height) as i32), - alacritty_terminal::index::Column(((mouse_pos_x - origin_x) / cell_width) as usize), - ) - ); - } - - #[test] - fn test_mouse_to_selection_off_edge() { - let term_width = 100.; - let term_height = 200.; - let cell_width = 10.; - let line_height = 20.; - let mouse_pos_x = 100.; //Window relative - let mouse_pos_y = 100.; //Window relative - let origin_x = 10.; - let origin_y = 20.; - - let cur_size = alacritty_terminal::term::SizeInfo::new( - term_width, - term_height, - cell_width, - line_height, - 0., - 0., - false, - ); - - let mouse_pos = gpui::geometry::vector::vec2f(mouse_pos_x, mouse_pos_y); - let origin = gpui::geometry::vector::vec2f(origin_x, origin_y); //Position of terminal window, 1 'cell' in - let (point, _) = - crate::terminal_element::mouse_to_cell_data(mouse_pos, origin, cur_size, 0); - assert_eq!( - point, - alacritty_terminal::index::Point::new( - alacritty_terminal::index::Line(((mouse_pos_y - origin_y) / line_height) as i32), - alacritty_terminal::index::Column(((mouse_pos_x - origin_x) / cell_width) as usize), - ) - ); - } -} diff --git a/crates/terminal/src/tests/terminal_test_context.rs b/crates/terminal/src/tests/terminal_test_context.rs index b5696aff13..e78939224b 100644 --- a/crates/terminal/src/tests/terminal_test_context.rs +++ b/crates/terminal/src/tests/terminal_test_context.rs @@ -1,35 +1,40 @@ -use std::time::Duration; +use std::{path::Path, time::Duration}; -use alacritty_terminal::term::SizeInfo; -use gpui::{AppContext, ModelHandle, ReadModelWith, TestAppContext}; +use gpui::{ + geometry::vector::vec2f, AppContext, ModelHandle, ReadModelWith, TestAppContext, ViewHandle, +}; use itertools::Itertools; +use project::{Entry, Project, ProjectPath, Worktree}; +use workspace::{AppState, Workspace}; use crate::{ - connection::TerminalConnection, DEBUG_CELL_WIDTH, DEBUG_LINE_HEIGHT, DEBUG_TERMINAL_HEIGHT, - DEBUG_TERMINAL_WIDTH, + connected_el::TermDimensions, + model::{Terminal, TerminalBuilder}, + DEBUG_CELL_WIDTH, DEBUG_LINE_HEIGHT, DEBUG_TERMINAL_HEIGHT, DEBUG_TERMINAL_WIDTH, }; pub struct TerminalTestContext<'a> { pub cx: &'a mut TestAppContext, - pub connection: ModelHandle, + pub connection: Option>, } impl<'a> TerminalTestContext<'a> { - pub fn new(cx: &'a mut TestAppContext) -> Self { + pub fn new(cx: &'a mut TestAppContext, term: bool) -> Self { cx.set_condition_duration(Some(Duration::from_secs(5))); - let size_info = SizeInfo::new( - DEBUG_TERMINAL_WIDTH, - DEBUG_TERMINAL_HEIGHT, + let size_info = TermDimensions::new( DEBUG_CELL_WIDTH, DEBUG_LINE_HEIGHT, - 0., - 0., - false, + vec2f(DEBUG_TERMINAL_WIDTH, DEBUG_TERMINAL_HEIGHT), ); - let connection = - cx.add_model(|cx| TerminalConnection::new(None, None, None, size_info, cx)); + let connection = term.then(|| { + cx.add_model(|cx| { + TerminalBuilder::new(None, None, None, size_info) + .unwrap() + .subscribe(cx) + }) + }); TerminalTestContext { cx, connection } } @@ -38,34 +43,112 @@ impl<'a> TerminalTestContext<'a> { where F: Fn(String, &AppContext) -> bool, { + let connection = self.connection.take().unwrap(); + let command = command.to_string(); - self.connection.update(self.cx, |connection, _| { + connection.update(self.cx, |connection, _| { connection.write_to_pty(command); connection.write_to_pty("\r".to_string()); }); - self.connection + connection .condition(self.cx, |conn, cx| { let content = Self::grid_as_str(conn); f(content, cx) }) .await; - self.cx - .read_model_with(&self.connection, &mut |conn, _: &AppContext| { + let res = self + .cx + .read_model_with(&connection, &mut |conn, _: &AppContext| { Self::grid_as_str(conn) - }) + }); + + self.connection = Some(connection); + + res } - fn grid_as_str(connection: &TerminalConnection) -> String { - let term = connection.term.lock(); - let grid_iterator = term.renderable_content().display_iter; - let lines = grid_iterator.group_by(|i| i.point.line.0); - lines - .into_iter() - .map(|(_, line)| line.map(|i| i.c).collect::()) - .collect::>() - .join("\n") + ///Creates a worktree with 1 file: /root.txt + pub async fn blank_workspace(&mut self) -> (ModelHandle, ViewHandle) { + let params = self.cx.update(AppState::test); + + let project = Project::test(params.fs.clone(), [], self.cx).await; + let (_, workspace) = self.cx.add_window(|cx| Workspace::new(project.clone(), cx)); + + (project, workspace) + } + + ///Creates a worktree with 1 folder: /root{suffix}/ + pub async fn create_folder_wt( + &mut self, + project: ModelHandle, + path: impl AsRef, + ) -> (ModelHandle, Entry) { + self.create_wt(project, true, path).await + } + + ///Creates a worktree with 1 file: /root{suffix}.txt + pub async fn create_file_wt( + &mut self, + project: ModelHandle, + path: impl AsRef, + ) -> (ModelHandle, Entry) { + self.create_wt(project, false, path).await + } + + async fn create_wt( + &mut self, + project: ModelHandle, + is_dir: bool, + path: impl AsRef, + ) -> (ModelHandle, Entry) { + let (wt, _) = project + .update(self.cx, |project, cx| { + project.find_or_create_local_worktree(path, true, cx) + }) + .await + .unwrap(); + + let entry = self + .cx + .update(|cx| { + wt.update(cx, |wt, cx| { + wt.as_local() + .unwrap() + .create_entry(Path::new(""), is_dir, cx) + }) + }) + .await + .unwrap(); + + (wt, entry) + } + + pub fn insert_active_entry_for( + &mut self, + wt: ModelHandle, + entry: Entry, + project: ModelHandle, + ) { + self.cx.update(|cx| { + let p = ProjectPath { + worktree_id: wt.read(cx).id(), + path: entry.path, + }; + project.update(cx, |project, cx| project.set_active_path(Some(p), cx)); + }); + } + + fn grid_as_str(connection: &Terminal) -> String { + connection.render_lock(None, |content, _| { + let lines = content.display_iter.group_by(|i| i.point.line.0); + lines + .into_iter() + .map(|(_, line)| line.map(|i| i.c).collect::()) + .collect::>() + .join("\n") + }) } } diff --git a/crates/text/src/selection.rs b/crates/text/src/selection.rs index fd8d57ca9f..e5acbd21bc 100644 --- a/crates/text/src/selection.rs +++ b/crates/text/src/selection.rs @@ -54,6 +54,13 @@ impl Selection { goal: self.goal, } } + + pub fn collapse_to(&mut self, point: T, new_goal: SelectionGoal) { + self.start = point.clone(); + self.end = point; + self.goal = new_goal; + self.reversed = false; + } } impl Selection { @@ -78,13 +85,6 @@ impl Selection { self.goal = new_goal; } - pub fn collapse_to(&mut self, point: T, new_goal: SelectionGoal) { - self.start = point; - self.end = point; - self.goal = new_goal; - self.reversed = false; - } - pub fn range(&self) -> Range { self.start..self.end } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index ca04caf408..f7c470bb96 100644 --- a/crates/theme/src/theme.rs +++ b/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, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 3be07c0f2d..0321e770cb 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1224,8 +1224,10 @@ impl Workspace { } } - pub fn modal(&self) -> Option<&AnyViewHandle> { - self.modal.as_ref() + pub fn modal(&self) -> Option> { + self.modal + .as_ref() + .and_then(|modal| modal.clone().downcast::()) } pub fn dismiss_modal(&mut self, cx: &mut ViewContext) { diff --git a/crates/zed/src/menus.rs b/crates/zed/src/menus.rs index 5aa53d7526..e8297a1727 100644 --- a/crates/zed/src/menus.rs +++ b/crates/zed/src/menus.rs @@ -285,7 +285,7 @@ pub fn menus() -> Vec> { MenuItem::Separator, MenuItem::Action { name: "Next Problem", - action: Box::new(editor::GoToNextDiagnostic), + action: Box::new(editor::GoToDiagnostic), }, MenuItem::Action { name: "Previous Problem", diff --git a/styles/src/styleTree/hoverPopover.ts b/styles/src/styleTree/hoverPopover.ts index d6665cacff..0aac4e66c9 100644 --- a/styles/src/styleTree/hoverPopover.ts +++ b/styles/src/styleTree/hoverPopover.ts @@ -2,22 +2,48 @@ import Theme from "../themes/common/theme"; import { backgroundColor, border, popoverShadow, text } from "./components"; export default function HoverPopover(theme: Theme) { + let baseContainer = { + background: backgroundColor(theme, "on500"), + cornerRadius: 8, + padding: { + left: 8, + right: 8, + top: 4, + bottom: 4 + }, + shadow: popoverShadow(theme), + border: border(theme, "secondary"), + margin: { + left: -8, + }, + }; + return { - container: { - background: backgroundColor(theme, "on500"), - cornerRadius: 8, - padding: { - left: 8, - right: 8, - top: 4, - bottom: 4, + container: baseContainer, + infoContainer: { + ...baseContainer, + background: backgroundColor(theme, "on500Info"), + border: { + color: theme.ramps.blue(0).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 }, }, diff --git a/styles/src/themes/common/base16.ts b/styles/src/themes/common/base16.ts index a73ac7f0cf..321184d40d 100644 --- a/styles/src/themes/common/base16.ts +++ b/styles/src/themes/common/base16.ts @@ -88,16 +88,31 @@ export function createTheme( hovered: withOpacity(sample(ramps.red, 0.5), 0.2), active: withOpacity(sample(ramps.red, 0.5), 0.25), }, + on500Error: { + base: sample(ramps.red, 0.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 = { diff --git a/styles/src/themes/common/theme.ts b/styles/src/themes/common/theme.ts index ac0902d8a2..e01435b846 100644 --- a/styles/src/themes/common/theme.ts +++ b/styles/src/themes/common/theme.ts @@ -79,8 +79,11 @@ export default interface Theme { on500: BackgroundColorSet; ok: BackgroundColorSet; error: BackgroundColorSet; + on500Error: BackgroundColorSet; warning: BackgroundColorSet; + on500Warning: BackgroundColorSet; info: BackgroundColorSet; + on500Info: BackgroundColorSet; }; borderColor: { primary: string;