Merge branch 'main' into ime-support-2

This commit is contained in:
Antonio Scandurra 2022-07-25 10:47:49 +02:00
commit ca3e73106c
32 changed files with 2520 additions and 1790 deletions

13
Cargo.lock generated
View file

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

View file

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

View file

@ -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"

View file

@ -362,12 +362,7 @@ mod tests {
});
let palette = workspace.read_with(cx, |workspace, _| {
workspace
.modal()
.unwrap()
.clone()
.downcast::<CommandPalette>()
.unwrap()
workspace.modal::<CommandPalette>().unwrap()
});
palette
@ -398,12 +393,7 @@ mod tests {
// Assert editor command not present
let palette = workspace.read_with(cx, |workspace, _| {
workspace
.modal()
.unwrap()
.clone()
.downcast::<CommandPalette>()
.unwrap()
workspace.modal::<CommandPalette>().unwrap()
});
palette

View file

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

View file

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

View file

@ -41,6 +41,10 @@ use std::{
ops::Range,
};
const MIN_POPOVER_CHARACTER_WIDTH: f32 = 20.;
const MIN_POPOVER_LINE_HEIGHT: f32 = 4.;
const HOVER_POPOVER_GAP: f32 = 10.;
struct SelectionLayout {
head: DisplayPoint,
range: Range<DisplayPoint>,
@ -268,8 +272,9 @@ impl EditorElement {
}
if paint
.hover_bounds
.map_or(false, |hover_bounds| hover_bounds.contains_point(position))
.hover_popover_bounds
.iter()
.any(|hover_bounds| hover_bounds.contains_point(position))
{
return false;
}
@ -585,19 +590,32 @@ 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();
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);
if popover_origin.y() < 0.0 {
popover_origin.set_y(popover_origin.y() + layout.line_height + size.y());
}
// 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;
let hovered_point = content_origin + vec2f(x, 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 {
@ -610,10 +628,40 @@ impl EditorElement {
cx,
);
paint.hover_bounds = Some(
RectF::new(popover_origin, hover_popover.size()).dilate(Vector2F::new(0., 5.)),
paint.hover_popover_bounds.push(
RectF::new(popover_origin, hover_popover.size())
.dilate(Vector2F::new(0., 5.)),
);
current_y = popover_origin.y() - HOVER_POPOVER_GAP;
}
} else {
// There is not enough space above. Render popovers below the hovered point
let mut current_y = hovered_point.y() + layout.line_height;
for hover_popover in hover_popovers {
let size = hover_popover.size();
let mut popover_origin = vec2f(hovered_point.x(), current_y);
let x_out_of_bounds = bounds.max_x() - (popover_origin.x() + size.x());
if x_out_of_bounds < 0.0 {
popover_origin.set_x(popover_origin.x() + x_out_of_bounds);
}
hover_popover.paint(
popover_origin,
RectF::from_points(Vector2F::zero(), vec2f(f32::MAX, f32::MAX)), // Let content bleed outside of editor
cx,
);
paint.hover_popover_bounds.push(
RectF::new(popover_origin, hover_popover.size())
.dilate(Vector2F::new(0., 5.)),
);
current_y = popover_origin.y() + size.y() + HOVER_POPOVER_GAP;
}
}
cx.scene.pop_stacking_context();
}
@ -1147,6 +1195,8 @@ impl Element for EditorElement {
});
let scroll_position = snapshot.scroll_position();
// The scroll position is a fractional point, the whole number of which represents
// the top of the window in terms of display rows.
let start_row = scroll_position.y() as u32;
let scroll_top = scroll_position.y() * line_height;
@ -1320,16 +1370,8 @@ impl Element for EditorElement {
.map(|indicator| (newest_selection_head.row(), indicator));
}
hover = view.hover_state.popover.clone().and_then(|hover| {
let (point, rendered) = hover.render(&snapshot, style.clone(), cx);
if point.row() >= snapshot.scroll_position().y() as u32 {
if line_layouts.len() > (point.row() - start_row) as usize {
return Some((point, rendered));
}
}
None
});
let visible_rows = start_row..start_row + line_layouts.len() as u32;
hover = view.hover_state.render(&snapshot, &style, visible_rows, cx);
});
if let Some((_, context_menu)) = context_menu.as_mut() {
@ -1352,22 +1394,24 @@ impl Element for EditorElement {
);
}
if let Some((_, hover)) = hover.as_mut() {
hover.layout(
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(20. * em_width), // Apply minimum width of 20 characters
.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(4. * line_height), // Apply minimum height of 4 lines
.max(MIN_POPOVER_LINE_HEIGHT * line_height), // Apply minimum height of 4 lines
),
},
cx,
);
}
}
(
size,
@ -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,11 +1501,13 @@ impl Element for EditorElement {
}
}
if let Some((_, hover)) = &mut layout.hover {
if hover.dispatch_event(event, cx) {
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;
}
}
}
for block in &mut layout.blocks {
if block.element.dispatch_event(event, cx) {
@ -1590,7 +1636,7 @@ pub struct LayoutState {
selections: Vec<(ReplicaId, Vec<SelectionLayout>)>,
context_menu: Option<(DisplayPoint, ElementBox)>,
code_actions_indicator: Option<(u32, ElementBox)>,
hover: Option<(DisplayPoint, ElementBox)>,
hover_popovers: Option<(DisplayPoint, Vec<ElementBox>)>,
}
struct BlockLayout {
@ -1635,7 +1681,7 @@ pub struct PaintState {
gutter_bounds: RectF,
text_bounds: RectF,
context_menu_bounds: Option<RectF>,
hover_bounds: Option<RectF>,
hover_popover_bounds: Vec<RectF>,
}
impl PaintState {

View file

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

View file

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

View file

@ -317,15 +317,7 @@ mod tests {
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
cx.dispatch_action(window_id, Toggle);
let finder = cx.read(|cx| {
workspace
.read(cx)
.modal()
.cloned()
.unwrap()
.downcast::<FileFinder>()
.unwrap()
});
let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
finder
.update(cx, |finder, cx| {
finder.update_matches("bna".to_string(), cx)

View file

@ -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,

View file

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

View file

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

View file

@ -0,0 +1,853 @@
use alacritty_terminal::{
ansi::{Color::Named, NamedColor},
event::WindowSize,
grid::{Dimensions, GridIterator, Indexed, Scroll},
index::{Column as GridCol, Line as GridLine, Point, Side},
selection::SelectionRange,
term::cell::{Cell, Flags},
};
use editor::{Cursor, CursorShape, HighlightedRange, HighlightedRangeLine};
use gpui::{
color::Color,
elements::*,
fonts::{TextStyle, Underline},
geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
},
json::json,
text_layout::{Line, RunStyle},
Event, FontCache, KeyDownEvent, MouseButton, MouseButtonEvent, MouseMovedEvent, MouseRegion,
PaintContext, Quad, ScrollWheelEvent, TextLayoutCache, WeakModelHandle, WeakViewHandle,
};
use itertools::Itertools;
use ordered_float::OrderedFloat;
use settings::Settings;
use theme::TerminalStyle;
use util::ResultExt;
use std::{cmp::min, ops::Range};
use std::{fmt::Debug, ops::Sub};
use crate::{mappings::colors::convert_color, model::Terminal, ConnectedView};
///Scrolling is unbearably sluggish by default. Alacritty supports a configurable
///Scroll multiplier that is set to 3 by default. This will be removed when I
///Implement scroll bars.
const ALACRITTY_SCROLL_MULTIPLIER: f32 = 3.;
///The information generated during layout that is nescessary for painting
pub struct LayoutState {
cells: Vec<LayoutCell>,
rects: Vec<LayoutRect>,
highlights: Vec<RelativeHighlightedRange>,
cursor: Option<Cursor>,
background_color: Color,
selection_color: Color,
size: TermDimensions,
}
///Helper struct for converting data between alacritty's cursor points, and displayed cursor points
struct DisplayCursor {
line: i32,
col: usize,
}
impl DisplayCursor {
fn from(cursor_point: Point, display_offset: usize) -> Self {
Self {
line: cursor_point.line.0 + display_offset as i32,
col: cursor_point.column.0,
}
}
pub fn line(&self) -> i32 {
self.line
}
pub fn col(&self) -> usize {
self.col
}
}
#[derive(Clone, Copy, Debug)]
pub struct TermDimensions {
cell_width: f32,
line_height: f32,
height: f32,
width: f32,
}
impl TermDimensions {
pub fn new(line_height: f32, cell_width: f32, size: Vector2F) -> Self {
TermDimensions {
cell_width,
line_height,
width: size.x(),
height: size.y(),
}
}
pub fn num_lines(&self) -> usize {
(self.height / self.line_height).floor() as usize
}
pub fn num_columns(&self) -> usize {
(self.width / self.cell_width).floor() as usize
}
pub fn height(&self) -> f32 {
self.height
}
pub fn width(&self) -> f32 {
self.width
}
pub fn cell_width(&self) -> f32 {
self.cell_width
}
pub fn line_height(&self) -> f32 {
self.line_height
}
}
impl Into<WindowSize> for TermDimensions {
fn into(self) -> WindowSize {
WindowSize {
num_lines: self.num_lines() as u16,
num_cols: self.num_columns() as u16,
cell_width: self.cell_width() as u16,
cell_height: self.line_height() as u16,
}
}
}
impl Dimensions for TermDimensions {
fn total_lines(&self) -> usize {
self.screen_lines() //TODO: Check that this is fine. This is supposed to be for the back buffer...
}
fn screen_lines(&self) -> usize {
self.num_lines()
}
fn columns(&self) -> usize {
self.num_columns()
}
}
#[derive(Clone, Debug, Default)]
struct LayoutCell {
point: Point<i32, i32>,
text: Line,
}
impl LayoutCell {
fn new(point: Point<i32, i32>, text: Line) -> LayoutCell {
LayoutCell { point, text }
}
fn paint(
&self,
origin: Vector2F,
layout: &LayoutState,
visible_bounds: RectF,
cx: &mut PaintContext,
) {
let pos = point_to_absolute(origin, self.point, layout);
self.text
.paint(pos, visible_bounds, layout.size.line_height, cx);
}
}
#[derive(Clone, Debug, Default)]
struct LayoutRect {
point: Point<i32, i32>,
num_of_cells: usize,
color: Color,
}
impl LayoutRect {
fn new(point: Point<i32, i32>, num_of_cells: usize, color: Color) -> LayoutRect {
LayoutRect {
point,
num_of_cells,
color,
}
}
fn extend(&self) -> Self {
LayoutRect {
point: self.point,
num_of_cells: self.num_of_cells + 1,
color: self.color,
}
}
fn paint(&self, origin: Vector2F, layout: &LayoutState, cx: &mut PaintContext) {
let position = point_to_absolute(origin, self.point, layout);
let size = vec2f(
(layout.size.cell_width.ceil() * self.num_of_cells as f32).ceil(),
layout.size.line_height,
);
cx.scene.push_quad(Quad {
bounds: RectF::new(position, size),
background: Some(self.color),
border: Default::default(),
corner_radius: 0.,
})
}
}
fn point_to_absolute(origin: Vector2F, point: Point<i32, i32>, layout: &LayoutState) -> Vector2F {
vec2f(
(origin.x() + point.column as f32 * layout.size.cell_width).floor(),
origin.y() + point.line as f32 * layout.size.line_height,
)
}
#[derive(Clone, Debug, Default)]
struct RelativeHighlightedRange {
line_index: usize,
range: Range<usize>,
}
impl RelativeHighlightedRange {
fn new(line_index: usize, range: Range<usize>) -> Self {
RelativeHighlightedRange { line_index, range }
}
fn to_highlighted_range_line(
&self,
origin: Vector2F,
layout: &LayoutState,
) -> HighlightedRangeLine {
let start_x = origin.x() + self.range.start as f32 * layout.size.cell_width;
let end_x =
origin.x() + self.range.end as f32 * layout.size.cell_width + layout.size.cell_width;
return HighlightedRangeLine { start_x, end_x };
}
}
///The GPUI element that paints the terminal.
///We need to keep a reference to the view for mouse events, do we need it for any other terminal stuff, or can we move that to connection?
pub struct TerminalEl {
terminal: WeakModelHandle<Terminal>,
view: WeakViewHandle<ConnectedView>,
modal: bool,
}
impl TerminalEl {
pub fn new(
view: WeakViewHandle<ConnectedView>,
terminal: WeakModelHandle<Terminal>,
modal: bool,
) -> TerminalEl {
TerminalEl {
view,
terminal,
modal,
}
}
fn layout_grid(
grid: GridIterator<Cell>,
text_style: &TextStyle,
terminal_theme: &TerminalStyle,
text_layout_cache: &TextLayoutCache,
modal: bool,
selection_range: Option<SelectionRange>,
) -> (
Vec<LayoutCell>,
Vec<LayoutRect>,
Vec<RelativeHighlightedRange>,
) {
let mut cells = vec![];
let mut rects = vec![];
let mut highlight_ranges = vec![];
let mut cur_rect: Option<LayoutRect> = None;
let mut cur_alac_color = None;
let mut highlighted_range = None;
let linegroups = grid.group_by(|i| i.point.line);
for (line_index, (_, line)) in linegroups.into_iter().enumerate() {
for (x_index, cell) in line.enumerate() {
//Increase selection range
{
if selection_range
.map(|range| range.contains(cell.point))
.unwrap_or(false)
{
let mut range = highlighted_range.take().unwrap_or(x_index..x_index);
range.end = range.end.max(x_index);
highlighted_range = Some(range);
}
}
//Expand background rect range
{
if matches!(cell.bg, Named(NamedColor::Background)) {
//Continue to next cell, resetting variables if nescessary
cur_alac_color = None;
if let Some(rect) = cur_rect {
rects.push(rect);
cur_rect = None
}
} else {
match cur_alac_color {
Some(cur_color) => {
if cell.bg == cur_color {
cur_rect = cur_rect.take().map(|rect| rect.extend());
} else {
cur_alac_color = Some(cell.bg);
if let Some(_) = cur_rect {
rects.push(cur_rect.take().unwrap());
}
cur_rect = Some(LayoutRect::new(
Point::new(line_index as i32, cell.point.column.0 as i32),
1,
convert_color(&cell.bg, &terminal_theme.colors, modal),
));
}
}
None => {
cur_alac_color = Some(cell.bg);
cur_rect = Some(LayoutRect::new(
Point::new(line_index as i32, cell.point.column.0 as i32),
1,
convert_color(&cell.bg, &terminal_theme.colors, modal),
));
}
}
}
}
//Layout current cell text
{
let cell_text = &cell.c.to_string();
if cell_text != " " {
let cell_style =
TerminalEl::cell_style(&cell, terminal_theme, text_style, modal);
let layout_cell = text_layout_cache.layout_str(
cell_text,
text_style.font_size,
&[(cell_text.len(), cell_style)],
);
cells.push(LayoutCell::new(
Point::new(line_index as i32, cell.point.column.0 as i32),
layout_cell,
))
}
};
}
if highlighted_range.is_some() {
highlight_ranges.push(RelativeHighlightedRange::new(
line_index,
highlighted_range.take().unwrap(),
))
}
if cur_rect.is_some() {
rects.push(cur_rect.take().unwrap());
}
}
(cells, rects, highlight_ranges)
}
// Compute the cursor position and expected block width, may return a zero width if x_for_index returns
// the same position for sequential indexes. Use em_width instead
fn shape_cursor(
cursor_point: DisplayCursor,
size: TermDimensions,
text_fragment: &Line,
) -> Option<(Vector2F, f32)> {
if cursor_point.line() < size.total_lines() as i32 {
let cursor_width = if text_fragment.width() == 0. {
size.cell_width()
} else {
text_fragment.width()
};
Some((
vec2f(
cursor_point.col() as f32 * size.cell_width(),
cursor_point.line() as f32 * size.line_height(),
),
cursor_width,
))
} else {
None
}
}
///Convert the Alacritty cell styles to GPUI text styles and background color
fn cell_style(
indexed: &Indexed<&Cell>,
style: &TerminalStyle,
text_style: &TextStyle,
modal: bool,
) -> RunStyle {
let flags = indexed.cell.flags;
let fg = convert_color(&indexed.cell.fg, &style.colors, modal);
let underline = flags
.contains(Flags::UNDERLINE)
.then(|| Underline {
color: Some(fg),
squiggly: false,
thickness: OrderedFloat(1.),
})
.unwrap_or_default();
RunStyle {
color: fg,
font_id: text_style.font_id,
underline,
}
}
fn attach_mouse_handlers(
&self,
origin: Vector2F,
view_id: usize,
visible_bounds: RectF,
cur_size: TermDimensions,
cx: &mut PaintContext,
) {
let mouse_down_connection = self.terminal.clone();
let click_connection = self.terminal.clone();
let drag_connection = self.terminal.clone();
cx.scene.push_mouse_region(
MouseRegion::new(view_id, None, visible_bounds)
.on_down(
MouseButton::Left,
move |MouseButtonEvent { position, .. }, cx| {
if let Some(conn_handle) = mouse_down_connection.upgrade(cx.app) {
conn_handle.update(cx.app, |terminal, cx| {
let (point, side) = TerminalEl::mouse_to_cell_data(
position,
origin,
cur_size,
terminal.get_display_offset(),
);
terminal.mouse_down(point, side);
cx.notify();
})
}
},
)
.on_click(
MouseButton::Left,
move |MouseButtonEvent {
position,
click_count,
..
},
cx| {
cx.focus_parent_view();
if let Some(conn_handle) = click_connection.upgrade(cx.app) {
conn_handle.update(cx.app, |terminal, cx| {
let (point, side) = TerminalEl::mouse_to_cell_data(
position,
origin,
cur_size,
terminal.get_display_offset(),
);
terminal.click(point, side, click_count);
cx.notify();
});
}
},
)
.on_drag(
MouseButton::Left,
move |_, MouseMovedEvent { position, .. }, cx| {
if let Some(conn_handle) = drag_connection.upgrade(cx.app) {
conn_handle.update(cx.app, |terminal, cx| {
let (point, side) = TerminalEl::mouse_to_cell_data(
position,
origin,
cur_size,
terminal.get_display_offset(),
);
terminal.drag(point, side);
cx.notify()
});
}
},
),
);
}
///Configures a text style from the current settings.
pub fn make_text_style(font_cache: &FontCache, settings: &Settings) -> TextStyle {
// Pull the font family from settings properly overriding
let family_id = settings
.terminal_overrides
.font_family
.as_ref()
.or_else(|| settings.terminal_defaults.font_family.as_ref())
.and_then(|family_name| font_cache.load_family(&[family_name]).log_err())
.unwrap_or(settings.buffer_font_family);
let font_size = settings
.terminal_overrides
.font_size
.or(settings.terminal_defaults.font_size)
.unwrap_or(settings.buffer_font_size);
let font_id = font_cache
.select_font(family_id, &Default::default())
.unwrap();
TextStyle {
color: settings.theme.editor.text_color,
font_family_id: family_id,
font_family_name: font_cache.family_name(family_id).unwrap(),
font_id,
font_size,
font_properties: Default::default(),
underline: Default::default(),
}
}
pub fn mouse_to_cell_data(
pos: Vector2F,
origin: Vector2F,
cur_size: TermDimensions,
display_offset: usize,
) -> (Point, alacritty_terminal::index::Direction) {
let pos = pos.sub(origin);
let point = {
let col = pos.x() / cur_size.cell_width; //TODO: underflow...
let col = min(GridCol(col as usize), cur_size.last_column());
let line = pos.y() / cur_size.line_height;
let line = min(line as i32, cur_size.bottommost_line().0);
Point::new(GridLine(line - display_offset as i32), col)
};
//Copied (with modifications) from alacritty/src/input.rs > Processor::cell_side()
let side = {
let x = pos.0.x() as usize;
let cell_x =
x.saturating_sub(cur_size.cell_width as usize) % cur_size.cell_width as usize;
let half_cell_width = (cur_size.cell_width / 2.0) as usize;
let additional_padding =
(cur_size.width() - cur_size.cell_width * 2.) % cur_size.cell_width;
let end_of_grid = cur_size.width() - cur_size.cell_width - additional_padding;
//Width: Pixels or columns?
if cell_x > half_cell_width
// Edge case when mouse leaves the window.
|| x as f32 >= end_of_grid
{
Side::Right
} else {
Side::Left
}
};
(point, side)
}
}
impl Element for TerminalEl {
type LayoutState = LayoutState;
type PaintState = ();
fn layout(
&mut self,
constraint: gpui::SizeConstraint,
cx: &mut gpui::LayoutContext,
) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
let settings = cx.global::<Settings>();
let font_cache = &cx.font_cache();
//Setup layout information
let terminal_theme = &settings.theme.terminal;
let text_style = TerminalEl::make_text_style(font_cache, &settings);
let selection_color = settings.theme.editor.selection.selection;
let dimensions = {
let line_height = font_cache.line_height(text_style.font_size);
let cell_width = font_cache.em_advance(text_style.font_id, text_style.font_size);
TermDimensions::new(line_height, cell_width, constraint.max)
};
let terminal = self.terminal.upgrade(cx).unwrap().read(cx);
let (cursor, cells, rects, highlights) =
terminal.render_lock(Some(dimensions.clone()), |content, cursor_text| {
let (cells, rects, highlights) = TerminalEl::layout_grid(
content.display_iter,
&text_style,
terminal_theme,
cx.text_layout_cache,
self.modal,
content.selection,
);
//Layout cursor
let cursor = {
let cursor_point =
DisplayCursor::from(content.cursor.point, content.display_offset);
let cursor_text = {
let str_trxt = cursor_text.to_string();
cx.text_layout_cache.layout_str(
&str_trxt,
text_style.font_size,
&[(
str_trxt.len(),
RunStyle {
font_id: text_style.font_id,
color: terminal_theme.colors.background,
underline: Default::default(),
},
)],
)
};
TerminalEl::shape_cursor(cursor_point, dimensions, &cursor_text).map(
move |(cursor_position, block_width)| {
Cursor::new(
cursor_position,
block_width,
dimensions.line_height,
terminal_theme.colors.cursor,
CursorShape::Block,
Some(cursor_text.clone()),
)
},
)
};
(cursor, cells, rects, highlights)
});
//Select background color
let background_color = if self.modal {
terminal_theme.colors.modal_background
} else {
terminal_theme.colors.background
};
//Done!
(
constraint.max,
LayoutState {
cells,
cursor,
background_color,
selection_color,
size: dimensions,
rects,
highlights,
},
)
}
fn paint(
&mut self,
bounds: gpui::geometry::rect::RectF,
visible_bounds: gpui::geometry::rect::RectF,
layout: &mut Self::LayoutState,
cx: &mut gpui::PaintContext,
) -> Self::PaintState {
//Setup element stuff
let clip_bounds = Some(visible_bounds);
cx.paint_layer(clip_bounds, |cx| {
let origin = bounds.origin() + vec2f(layout.size.cell_width, 0.);
//Elements are ephemeral, only at paint time do we know what could be clicked by a mouse
self.attach_mouse_handlers(origin, self.view.id(), visible_bounds, layout.size, cx);
cx.paint_layer(clip_bounds, |cx| {
//Start with a background color
cx.scene.push_quad(Quad {
bounds: RectF::new(bounds.origin(), bounds.size()),
background: Some(layout.background_color),
border: Default::default(),
corner_radius: 0.,
});
for rect in &layout.rects {
rect.paint(origin, &layout, cx)
}
});
//Draw Selection
cx.paint_layer(clip_bounds, |cx| {
let start_y = layout.highlights.get(0).map(|highlight| {
origin.y() + highlight.line_index as f32 * layout.size.line_height
});
if let Some(y) = start_y {
let range_lines = layout
.highlights
.iter()
.map(|relative_highlight| {
relative_highlight.to_highlighted_range_line(origin, layout)
})
.collect::<Vec<HighlightedRangeLine>>();
let hr = HighlightedRange {
start_y: y, //Need to change this
line_height: layout.size.line_height,
lines: range_lines,
color: layout.selection_color,
//Copied from editor. TODO: move to theme or something
corner_radius: 0.15 * layout.size.line_height,
};
hr.paint(bounds, cx.scene);
}
});
//Draw the text cells
cx.paint_layer(clip_bounds, |cx| {
for cell in &layout.cells {
cell.paint(origin, layout, visible_bounds, cx);
}
});
//Draw cursor
if let Some(cursor) = &layout.cursor {
cx.paint_layer(clip_bounds, |cx| {
cursor.paint(origin, cx);
})
}
});
}
fn dispatch_event(
&mut self,
event: &gpui::Event,
_bounds: gpui::geometry::rect::RectF,
visible_bounds: gpui::geometry::rect::RectF,
layout: &mut Self::LayoutState,
_paint: &mut Self::PaintState,
cx: &mut gpui::EventContext,
) -> bool {
match event {
Event::ScrollWheel(ScrollWheelEvent {
delta, position, ..
}) => visible_bounds
.contains_point(*position)
.then(|| {
let vertical_scroll =
(delta.y() / layout.size.line_height) * ALACRITTY_SCROLL_MULTIPLIER;
self.terminal.upgrade(cx.app).map(|terminal| {
terminal
.read(cx.app)
.scroll(Scroll::Delta(vertical_scroll.round() as i32));
});
})
.is_some(),
Event::KeyDown(KeyDownEvent { keystroke, .. }) => {
if !cx.is_parent_view_focused() {
return false;
}
//TODO Talk to keith about how to catch events emitted from an element.
if let Some(view) = self.view.upgrade(cx.app) {
view.update(cx.app, |view, cx| view.clear_bel(cx))
}
self.terminal
.upgrade(cx.app)
.map(|model_handle| model_handle.read(cx.app))
.map(|term| term.try_keystroke(keystroke))
.unwrap_or(false)
}
_ => false,
}
}
fn metadata(&self) -> Option<&dyn std::any::Any> {
None
}
fn debug(
&self,
_bounds: gpui::geometry::rect::RectF,
_layout: &Self::LayoutState,
_paint: &Self::PaintState,
_cx: &gpui::DebugContext,
) -> gpui::serde_json::Value {
json!({
"type": "TerminalElement",
})
}
fn rect_for_text_range(
&self,
_: Range<usize>,
bounds: RectF,
_: RectF,
layout: &Self::LayoutState,
_: &Self::PaintState,
_: &gpui::MeasurementContext,
) -> Option<RectF> {
// Use the same origin that's passed to `Cursor::paint` in the paint
// method bove.
let mut origin = bounds.origin() + vec2f(layout.size.cell_width, 0.);
// TODO - Why is it necessary to move downward one line to get correct
// positioning? I would think that we'd want the same rect that is
// painted for the cursor.
origin += vec2f(0., layout.size.line_height);
Some(layout.cursor.as_ref()?.bounding_rect(origin))
}
}
mod test {
#[test]
fn test_mouse_to_selection() {
let term_width = 100.;
let term_height = 200.;
let cell_width = 10.;
let line_height = 20.;
let mouse_pos_x = 100.; //Window relative
let mouse_pos_y = 100.; //Window relative
let origin_x = 10.;
let origin_y = 20.;
let cur_size = crate::connected_el::TermDimensions::new(
line_height,
cell_width,
gpui::geometry::vector::vec2f(term_width, term_height),
);
let mouse_pos = gpui::geometry::vector::vec2f(mouse_pos_x, mouse_pos_y);
let origin = gpui::geometry::vector::vec2f(origin_x, origin_y); //Position of terminal window, 1 'cell' in
let (point, _) =
crate::connected_el::TerminalEl::mouse_to_cell_data(mouse_pos, origin, cur_size, 0);
assert_eq!(
point,
alacritty_terminal::index::Point::new(
alacritty_terminal::index::Line(((mouse_pos_y - origin_y) / line_height) as i32),
alacritty_terminal::index::Column(((mouse_pos_x - origin_x) / cell_width) as usize),
)
);
}
}

View file

@ -0,0 +1,176 @@
use gpui::{
actions, keymap::Keystroke, AppContext, ClipboardItem, Element, ElementBox, ModelHandle,
MutableAppContext, View, ViewContext,
};
use crate::{
connected_el::TerminalEl,
model::{Event, Terminal},
};
///Event to transmit the scroll from the element to the view
#[derive(Clone, Debug, PartialEq)]
pub struct ScrollTerminal(pub i32);
actions!(
terminal,
[Up, Down, CtrlC, Escape, Enter, Clear, Copy, Paste,]
);
pub fn init(cx: &mut MutableAppContext) {
//Global binding overrrides
cx.add_action(ConnectedView::ctrl_c);
cx.add_action(ConnectedView::up);
cx.add_action(ConnectedView::down);
cx.add_action(ConnectedView::escape);
cx.add_action(ConnectedView::enter);
//Useful terminal views
cx.add_action(ConnectedView::copy);
cx.add_action(ConnectedView::paste);
cx.add_action(ConnectedView::clear);
}
///A terminal view, maintains the PTY's file handles and communicates with the terminal
pub struct ConnectedView {
terminal: ModelHandle<Terminal>,
has_new_content: bool,
//Currently using iTerm bell, show bell emoji in tab until input is received
has_bell: bool,
// Only for styling purposes. Doesn't effect behavior
modal: bool,
}
impl ConnectedView {
pub fn from_terminal(
terminal: ModelHandle<Terminal>,
modal: bool,
cx: &mut ViewContext<Self>,
) -> Self {
cx.observe(&terminal, |_, _, cx| cx.notify()).detach();
cx.subscribe(&terminal, |this, _, event, cx| match event {
Event::Wakeup => {
if cx.is_self_focused() {
cx.notify()
} else {
this.has_new_content = true;
cx.emit(Event::TitleChanged);
}
}
Event::Bell => {
this.has_bell = true;
cx.emit(Event::TitleChanged);
}
_ => cx.emit(*event),
})
.detach();
Self {
terminal,
has_new_content: true,
has_bell: false,
modal,
}
}
pub fn handle(&self) -> ModelHandle<Terminal> {
self.terminal.clone()
}
pub fn has_new_content(&self) -> bool {
self.has_new_content
}
pub fn has_bell(&self) -> bool {
self.has_bell
}
pub fn clear_bel(&mut self, cx: &mut ViewContext<ConnectedView>) {
self.has_bell = false;
cx.emit(Event::TitleChanged);
}
fn clear(&mut self, _: &Clear, cx: &mut ViewContext<Self>) {
self.terminal.read(cx).clear();
}
///Attempt to paste the clipboard into the terminal
fn copy(&mut self, _: &Copy, cx: &mut ViewContext<Self>) {
self.terminal
.read(cx)
.copy()
.map(|text| cx.write_to_clipboard(ClipboardItem::new(text)));
}
///Attempt to paste the clipboard into the terminal
fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
cx.read_from_clipboard().map(|item| {
self.terminal.read(cx).paste(item.text());
});
}
///Synthesize the keyboard event corresponding to 'up'
fn up(&mut self, _: &Up, cx: &mut ViewContext<Self>) {
self.terminal
.read(cx)
.try_keystroke(&Keystroke::parse("up").unwrap());
}
///Synthesize the keyboard event corresponding to 'down'
fn down(&mut self, _: &Down, cx: &mut ViewContext<Self>) {
self.terminal
.read(cx)
.try_keystroke(&Keystroke::parse("down").unwrap());
}
///Synthesize the keyboard event corresponding to 'ctrl-c'
fn ctrl_c(&mut self, _: &CtrlC, cx: &mut ViewContext<Self>) {
self.terminal
.read(cx)
.try_keystroke(&Keystroke::parse("ctrl-c").unwrap());
}
///Synthesize the keyboard event corresponding to 'escape'
fn escape(&mut self, _: &Escape, cx: &mut ViewContext<Self>) {
self.terminal
.read(cx)
.try_keystroke(&Keystroke::parse("escape").unwrap());
}
///Synthesize the keyboard event corresponding to 'enter'
fn enter(&mut self, _: &Enter, cx: &mut ViewContext<Self>) {
self.terminal
.read(cx)
.try_keystroke(&Keystroke::parse("enter").unwrap());
}
}
impl View for ConnectedView {
fn ui_name() -> &'static str {
"Connected Terminal View"
}
fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox {
let terminal_handle = self.terminal.clone().downgrade();
TerminalEl::new(cx.handle(), terminal_handle, self.modal)
.contained()
.boxed()
}
fn on_focus(&mut self, _cx: &mut ViewContext<Self>) {
self.has_new_content = false;
}
fn selected_text_range(&self, _: &AppContext) -> Option<std::ops::Range<usize>> {
Some(0..0)
}
fn replace_text_in_range(
&mut self,
_: Option<std::ops::Range<usize>>,
text: &str,
cx: &mut ViewContext<Self>,
) {
self.terminal
.update(cx, |terminal, _| terminal.write_to_pty(text.into()));
}
}

View file

@ -1,252 +0,0 @@
mod keymappings;
use alacritty_terminal::{
ansi::{ClearMode, Handler},
config::{Config, Program, PtyConfig},
event::{Event as AlacTermEvent, Notify},
event_loop::{EventLoop, Msg, Notifier},
grid::Scroll,
sync::FairMutex,
term::{SizeInfo, TermMode},
tty::{self, setup_env},
Term,
};
use futures::{channel::mpsc::unbounded, StreamExt};
use settings::{Settings, Shell};
use std::{collections::HashMap, path::PathBuf, sync::Arc};
use gpui::{keymap::Keystroke, ClipboardItem, CursorStyle, Entity, ModelContext};
use crate::{
color_translation::{get_color_at_index, to_alac_rgb},
ZedListener,
};
use self::keymappings::to_esc_str;
const DEFAULT_TITLE: &str = "Terminal";
///Upward flowing events, for changing the title and such
#[derive(Copy, Clone, Debug)]
pub enum Event {
TitleChanged,
CloseTerminal,
Activate,
Wakeup,
Bell,
}
pub struct TerminalConnection {
pub pty_tx: Notifier,
pub term: Arc<FairMutex<Term<ZedListener>>>,
pub title: String,
pub associated_directory: Option<PathBuf>,
}
impl TerminalConnection {
pub fn new(
working_directory: Option<PathBuf>,
shell: Option<Shell>,
env_vars: Option<Vec<(String, String)>>,
initial_size: SizeInfo,
cx: &mut ModelContext<Self>,
) -> TerminalConnection {
let pty_config = {
let shell = shell.and_then(|shell| match shell {
Shell::System => None,
Shell::Program(program) => Some(Program::Just(program)),
Shell::WithArguments { program, args } => Some(Program::WithArgs { program, args }),
});
PtyConfig {
shell,
working_directory: working_directory.clone(),
hold: false,
}
};
let mut env: HashMap<String, String> = HashMap::new();
if let Some(envs) = env_vars {
for (var, val) in envs {
env.insert(var, val);
}
}
//TODO: Properly set the current locale,
env.insert("LC_ALL".to_string(), "en_US.UTF-8".to_string());
let config = Config {
pty_config: pty_config.clone(),
env,
..Default::default()
};
setup_env(&config);
//Spawn a task so the Alacritty EventLoop can communicate with us in a view context
let (events_tx, mut events_rx) = unbounded();
//Set up the terminal...
let term = Term::new(&config, initial_size, ZedListener(events_tx.clone()));
let term = Arc::new(FairMutex::new(term));
//Setup the pty...
let pty = {
if let Some(pty) = tty::new(&pty_config, &initial_size, None).ok() {
pty
} else {
let pty_config = PtyConfig {
shell: None,
working_directory: working_directory.clone(),
..Default::default()
};
tty::new(&pty_config, &initial_size, None)
.expect("Failed with default shell too :(")
}
};
//And connect them together
let event_loop = EventLoop::new(
term.clone(),
ZedListener(events_tx.clone()),
pty,
pty_config.hold,
false,
);
//Kick things off
let pty_tx = event_loop.channel();
let _io_thread = event_loop.spawn();
cx.spawn_weak(|this, mut cx| async move {
//Listen for terminal events
while let Some(event) = events_rx.next().await {
match this.upgrade(&cx) {
Some(this) => {
this.update(&mut cx, |this, cx| {
this.process_terminal_event(event, cx);
cx.notify();
});
}
None => break,
}
}
})
.detach();
TerminalConnection {
pty_tx: Notifier(pty_tx),
term,
title: DEFAULT_TITLE.to_string(),
associated_directory: working_directory,
}
}
///Takes events from Alacritty and translates them to behavior on this view
fn process_terminal_event(
&mut self,
event: alacritty_terminal::event::Event,
cx: &mut ModelContext<Self>,
) {
match event {
// TODO: Handle is_self_focused in subscription on terminal view
AlacTermEvent::Wakeup => {
cx.emit(Event::Wakeup);
}
AlacTermEvent::PtyWrite(out) => self.write_to_pty(out),
AlacTermEvent::MouseCursorDirty => {
//Calculate new cursor style.
//TODO: alacritty/src/input.rs:L922-L939
//Check on correctly handling mouse events for terminals
cx.platform().set_cursor_style(CursorStyle::Arrow); //???
}
AlacTermEvent::Title(title) => {
self.title = title;
cx.emit(Event::TitleChanged);
}
AlacTermEvent::ResetTitle => {
self.title = DEFAULT_TITLE.to_string();
cx.emit(Event::TitleChanged);
}
AlacTermEvent::ClipboardStore(_, data) => {
cx.write_to_clipboard(ClipboardItem::new(data))
}
AlacTermEvent::ClipboardLoad(_, format) => self.write_to_pty(format(
&cx.read_from_clipboard()
.map(|ci| ci.text().to_string())
.unwrap_or("".to_string()),
)),
AlacTermEvent::ColorRequest(index, format) => {
let color = self.term.lock().colors()[index].unwrap_or_else(|| {
let term_style = &cx.global::<Settings>().theme.terminal;
to_alac_rgb(get_color_at_index(&index, &term_style.colors))
});
self.write_to_pty(format(color))
}
AlacTermEvent::CursorBlinkingChange => {
//TODO: Set a timer to blink the cursor on and off
}
AlacTermEvent::Bell => {
cx.emit(Event::Bell);
}
AlacTermEvent::Exit => cx.emit(Event::CloseTerminal),
}
}
///Write the Input payload to the tty. This locks the terminal so we can scroll it.
pub fn write_to_pty(&mut self, input: String) {
self.write_bytes_to_pty(input.into_bytes());
}
///Write the Input payload to the tty. This locks the terminal so we can scroll it.
fn write_bytes_to_pty(&mut self, input: Vec<u8>) {
self.term.lock().scroll_display(Scroll::Bottom);
self.pty_tx.notify(input);
}
///Resize the terminal and the PTY. This locks the terminal.
pub fn set_size(&mut self, new_size: SizeInfo) {
self.pty_tx.0.send(Msg::Resize(new_size)).ok();
self.term.lock().resize(new_size);
}
pub fn clear(&mut self) {
self.write_to_pty("\x0c".into());
self.term.lock().clear_screen(ClearMode::Saved);
}
pub fn try_keystroke(&mut self, keystroke: &Keystroke) -> bool {
let guard = self.term.lock();
let mode = guard.mode();
let esc = to_esc_str(keystroke, mode);
drop(guard);
if esc.is_some() {
self.write_to_pty(esc.unwrap());
true
} else {
false
}
}
///Paste text into the terminal
pub fn paste(&mut self, text: &str) {
if self.term.lock().mode().contains(TermMode::BRACKETED_PASTE) {
self.write_to_pty("\x1b[200~".to_string());
self.write_to_pty(text.replace('\x1b', "").to_string());
self.write_to_pty("\x1b[201~".to_string());
} else {
self.write_to_pty(text.replace("\r\n", "\r").replace('\n', "\r"));
}
}
}
impl Drop for TerminalConnection {
fn drop(&mut self) {
self.pty_tx.0.send(Msg::Shutdown).ok();
}
}
impl Entity for TerminalConnection {
type Event = Event;
}

View file

@ -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);
}
}

View file

@ -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;

View file

@ -0,0 +1,2 @@
pub mod colors;
pub mod keys;

View file

@ -1,63 +0,0 @@
use gpui::{ModelHandle, ViewContext};
use workspace::Workspace;
use crate::{get_wd_for_workspace, DeployModal, Event, Terminal, TerminalConnection};
#[derive(Debug)]
struct StoredConnection(ModelHandle<TerminalConnection>);
pub fn deploy_modal(workspace: &mut Workspace, _: &DeployModal, cx: &mut ViewContext<Workspace>) {
// Pull the terminal connection out of the global if it has been stored
let possible_connection =
cx.update_default_global::<Option<StoredConnection>, _, _>(|possible_connection, _| {
possible_connection.take()
});
if let Some(StoredConnection(stored_connection)) = possible_connection {
// Create a view from the stored connection
workspace.toggle_modal(cx, |_, cx| {
cx.add_view(|cx| Terminal::from_connection(stored_connection.clone(), true, cx))
});
cx.set_global::<Option<StoredConnection>>(Some(StoredConnection(
stored_connection.clone(),
)));
} else {
// No connection was stored, create a new terminal
if let Some(closed_terminal_handle) = workspace.toggle_modal(cx, |workspace, cx| {
let wd = get_wd_for_workspace(workspace, cx);
let this = cx.add_view(|cx| Terminal::new(wd, true, cx));
let connection_handle = this.read(cx).connection.clone();
cx.subscribe(&connection_handle, on_event).detach();
//Set the global immediately, in case the user opens the command palette
cx.set_global::<Option<StoredConnection>>(Some(StoredConnection(
connection_handle.clone(),
)));
this
}) {
let connection = closed_terminal_handle.read(cx).connection.clone();
cx.set_global(Some(StoredConnection(connection)));
}
}
//The problem is that the terminal modal is never re-stored.
}
pub fn on_event(
workspace: &mut Workspace,
_: ModelHandle<TerminalConnection>,
event: &Event,
cx: &mut ViewContext<Workspace>,
) {
// Dismiss the modal if the terminal quit
if let Event::CloseTerminal = event {
cx.set_global::<Option<StoredConnection>>(None);
if workspace
.modal()
.cloned()
.and_then(|modal| modal.downcast::<Terminal>())
.is_some()
{
workspace.dismiss_modal(cx)
}
}
}

View file

@ -0,0 +1,73 @@
use gpui::{ModelHandle, ViewContext};
use workspace::Workspace;
use crate::{
get_working_directory, model::Terminal, DeployModal, Event, TerminalContent, TerminalView,
};
#[derive(Debug)]
struct StoredTerminal(ModelHandle<Terminal>);
pub fn deploy_modal(workspace: &mut Workspace, _: &DeployModal, cx: &mut ViewContext<Workspace>) {
// Pull the terminal connection out of the global if it has been stored
let possible_terminal =
cx.update_default_global::<Option<StoredTerminal>, _, _>(|possible_connection, _| {
possible_connection.take()
});
if let Some(StoredTerminal(stored_terminal)) = possible_terminal {
workspace.toggle_modal(cx, |_, cx| {
// Create a view from the stored connection if the terminal modal is not already shown
cx.add_view(|cx| TerminalView::from_terminal(stored_terminal.clone(), true, cx))
});
// Toggle Modal will dismiss the terminal modal if it is currently shown, so we must
// store the terminal back in the global
cx.set_global::<Option<StoredTerminal>>(Some(StoredTerminal(stored_terminal.clone())));
} else {
// No connection was stored, create a new terminal
if let Some(closed_terminal_handle) = workspace.toggle_modal(cx, |workspace, cx| {
// No terminal modal visible, construct a new one.
let working_directory = get_working_directory(workspace, cx);
let this = cx.add_view(|cx| TerminalView::new(working_directory, true, cx));
if let TerminalContent::Connected(connected) = &this.read(cx).content {
let terminal_handle = connected.read(cx).handle();
cx.subscribe(&terminal_handle, on_event).detach();
// Set the global immediately if terminal construction was successful,
// in case the user opens the command palette
cx.set_global::<Option<StoredTerminal>>(Some(StoredTerminal(
terminal_handle.clone(),
)));
}
this
}) {
// Terminal modal was dismissed. Store terminal if the terminal view is connected
if let TerminalContent::Connected(connected) = &closed_terminal_handle.read(cx).content
{
let terminal_handle = connected.read(cx).handle();
// Set the global immediately if terminal construction was successful,
// in case the user opens the command palette
cx.set_global::<Option<StoredTerminal>>(Some(StoredTerminal(
terminal_handle.clone(),
)));
}
}
}
}
pub fn on_event(
workspace: &mut Workspace,
_: ModelHandle<Terminal>,
event: &Event,
cx: &mut ViewContext<Workspace>,
) {
// Dismiss the modal if the terminal quit
if let Event::CloseTerminal = event {
cx.set_global::<Option<StoredTerminal>>(None);
if workspace.modal::<TerminalView>().is_some() {
workspace.dismiss_modal(cx)
}
}
}

View file

@ -0,0 +1,522 @@
use alacritty_terminal::{
ansi::{ClearMode, Handler},
config::{Config, Program, PtyConfig},
event::{Event as AlacTermEvent, EventListener, Notify, WindowSize},
event_loop::{EventLoop, Msg, Notifier},
grid::Scroll,
index::{Direction, Point},
selection::{Selection, SelectionType},
sync::FairMutex,
term::{test::TermSize, RenderableContent, TermMode},
tty::{self, setup_env},
Term,
};
use anyhow::{bail, Result};
use futures::{
channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender},
StreamExt,
};
use settings::{Settings, Shell};
use std::{collections::HashMap, fmt::Display, path::PathBuf, sync::Arc};
use thiserror::Error;
use gpui::{keymap::Keystroke, ClipboardItem, CursorStyle, Entity, ModelContext};
use crate::{
connected_el::TermDimensions,
mappings::{
colors::{get_color_at_index, to_alac_rgb},
keys::to_esc_str,
},
};
const DEFAULT_TITLE: &str = "Terminal";
///Upward flowing events, for changing the title and such
#[derive(Copy, Clone, Debug)]
pub enum Event {
TitleChanged,
CloseTerminal,
Activate,
Wakeup,
Bell,
KeyInput,
}
///A translation struct for Alacritty to communicate with us from their event loop
#[derive(Clone)]
pub struct ZedListener(UnboundedSender<AlacTermEvent>);
impl EventListener for ZedListener {
fn send_event(&self, event: AlacTermEvent) {
self.0.unbounded_send(event).ok();
}
}
#[derive(Error, Debug)]
pub struct TerminalError {
pub directory: Option<PathBuf>,
pub shell: Option<Shell>,
pub source: std::io::Error,
}
impl TerminalError {
pub fn fmt_directory(&self) -> String {
self.directory
.clone()
.map(|path| {
match path
.into_os_string()
.into_string()
.map_err(|os_str| format!("<non-utf8 path> {}", os_str.to_string_lossy()))
{
Ok(s) => s,
Err(s) => s,
}
})
.unwrap_or_else(|| {
let default_dir =
dirs::home_dir().map(|buf| buf.into_os_string().to_string_lossy().to_string());
match default_dir {
Some(dir) => format!("<none specified, using home directory> {}", dir),
None => "<none specified, could not find home directory>".to_string(),
}
})
}
pub fn shell_to_string(&self) -> Option<String> {
self.shell.as_ref().map(|shell| match shell {
Shell::System => "<system shell>".to_string(),
Shell::Program(p) => p.to_string(),
Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")),
})
}
pub fn fmt_shell(&self) -> String {
self.shell
.clone()
.map(|shell| match shell {
Shell::System => {
let mut buf = [0; 1024];
let pw = alacritty_unix::get_pw_entry(&mut buf).ok();
match pw {
Some(pw) => format!("<system defined shell> {}", pw.shell),
None => "<could not access the password file>".to_string(),
}
}
Shell::Program(s) => s,
Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")),
})
.unwrap_or_else(|| {
let mut buf = [0; 1024];
let pw = alacritty_unix::get_pw_entry(&mut buf).ok();
match pw {
Some(pw) => {
format!("<none specified, using system defined shell> {}", pw.shell)
}
None => "<none specified, could not access the password file> {}".to_string(),
}
})
}
}
impl Display for TerminalError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let dir_string: String = self.fmt_directory();
let shell = self.fmt_shell();
write!(
f,
"Working directory: {} Shell command: `{}`, IOError: {}",
dir_string, shell, self.source
)
}
}
pub struct TerminalBuilder {
terminal: Terminal,
events_rx: UnboundedReceiver<AlacTermEvent>,
}
impl TerminalBuilder {
pub fn new(
working_directory: Option<PathBuf>,
shell: Option<Shell>,
env: Option<HashMap<String, String>>,
initial_size: TermDimensions,
) -> Result<TerminalBuilder> {
let pty_config = {
let alac_shell = shell.clone().and_then(|shell| match shell {
Shell::System => None,
Shell::Program(program) => Some(Program::Just(program)),
Shell::WithArguments { program, args } => Some(Program::WithArgs { program, args }),
});
PtyConfig {
shell: alac_shell,
working_directory: working_directory.clone(),
hold: false,
}
};
let mut env = env.unwrap_or_else(|| HashMap::new());
//TODO: Properly set the current locale,
env.insert("LC_ALL".to_string(), "en_US.UTF-8".to_string());
let config = Config {
pty_config: pty_config.clone(),
env,
..Default::default()
};
setup_env(&config);
//Spawn a task so the Alacritty EventLoop can communicate with us in a view context
let (events_tx, events_rx) = unbounded();
//Set up the terminal...
let term = Term::new(&config, &initial_size, ZedListener(events_tx.clone()));
let term = Arc::new(FairMutex::new(term));
//Setup the pty...
let pty = match tty::new(&pty_config, initial_size.into(), None) {
Ok(pty) => pty,
Err(error) => {
bail!(TerminalError {
directory: working_directory,
shell,
source: error,
});
}
};
let shell_txt = {
match shell {
Some(Shell::System) | None => {
let mut buf = [0; 1024];
let pw = alacritty_unix::get_pw_entry(&mut buf).unwrap();
pw.shell.to_string()
}
Some(Shell::Program(program)) => program,
Some(Shell::WithArguments { program, args }) => {
format!("{} {}", program, args.join(" "))
}
}
};
//And connect them together
let event_loop = EventLoop::new(
term.clone(),
ZedListener(events_tx.clone()),
pty,
pty_config.hold,
false,
);
//Kick things off
let pty_tx = event_loop.channel();
let _io_thread = event_loop.spawn();
let terminal = Terminal {
pty_tx: Notifier(pty_tx),
term,
title: shell_txt.to_string(),
};
Ok(TerminalBuilder {
terminal,
events_rx,
})
}
pub fn subscribe(mut self, cx: &mut ModelContext<Terminal>) -> Terminal {
cx.spawn_weak(|this, mut cx| async move {
//Listen for terminal events
while let Some(event) = self.events_rx.next().await {
match this.upgrade(&cx) {
Some(this) => {
this.update(&mut cx, |this, cx| {
this.process_terminal_event(event, cx);
cx.notify();
});
}
None => break,
}
}
})
.detach();
self.terminal
}
}
pub struct Terminal {
pty_tx: Notifier,
term: Arc<FairMutex<Term<ZedListener>>>,
pub title: String,
}
impl Terminal {
///Takes events from Alacritty and translates them to behavior on this view
fn process_terminal_event(
&mut self,
event: alacritty_terminal::event::Event,
cx: &mut ModelContext<Terminal>,
) {
match event {
// TODO: Handle is_self_focused in subscription on terminal view
AlacTermEvent::Wakeup => {
cx.emit(Event::Wakeup);
}
AlacTermEvent::PtyWrite(out) => self.write_to_pty(out),
AlacTermEvent::MouseCursorDirty => {
//Calculate new cursor style.
//TODO: alacritty/src/input.rs:L922-L939
//Check on correctly handling mouse events for terminals
cx.platform().set_cursor_style(CursorStyle::Arrow); //???
}
AlacTermEvent::Title(title) => {
self.title = title;
cx.emit(Event::TitleChanged);
}
AlacTermEvent::ResetTitle => {
self.title = DEFAULT_TITLE.to_string();
cx.emit(Event::TitleChanged);
}
AlacTermEvent::ClipboardStore(_, data) => {
cx.write_to_clipboard(ClipboardItem::new(data))
}
AlacTermEvent::ClipboardLoad(_, format) => self.write_to_pty(format(
&cx.read_from_clipboard()
.map(|ci| ci.text().to_string())
.unwrap_or("".to_string()),
)),
AlacTermEvent::ColorRequest(index, format) => {
let color = self.term.lock().colors()[index].unwrap_or_else(|| {
let term_style = &cx.global::<Settings>().theme.terminal;
to_alac_rgb(get_color_at_index(&index, &term_style.colors))
});
self.write_to_pty(format(color))
}
AlacTermEvent::CursorBlinkingChange => {
//TODO: Set a timer to blink the cursor on and off
}
AlacTermEvent::Bell => {
cx.emit(Event::Bell);
}
AlacTermEvent::Exit => cx.emit(Event::CloseTerminal),
AlacTermEvent::TextAreaSizeRequest(_) => println!("Received text area resize request"),
}
}
///Write the Input payload to the tty. This locks the terminal so we can scroll it.
pub fn write_to_pty(&self, input: String) {
self.write_bytes_to_pty(input.into_bytes());
}
///Write the Input payload to the tty. This locks the terminal so we can scroll it.
fn write_bytes_to_pty(&self, input: Vec<u8>) {
self.term.lock().scroll_display(Scroll::Bottom);
self.pty_tx.notify(input);
}
///Resize the terminal and the PTY. This locks the terminal.
pub fn set_size(&self, new_size: WindowSize) {
self.pty_tx.0.send(Msg::Resize(new_size)).ok();
let term_size = TermSize::new(new_size.num_cols as usize, new_size.num_lines as usize);
self.term.lock().resize(term_size);
}
pub fn clear(&self) {
self.write_to_pty("\x0c".into());
self.term.lock().clear_screen(ClearMode::Saved);
}
pub fn try_keystroke(&self, keystroke: &Keystroke) -> bool {
let guard = self.term.lock();
let mode = guard.mode();
let esc = to_esc_str(keystroke, mode);
drop(guard);
if esc.is_some() {
self.write_to_pty(esc.unwrap());
true
} else {
false
}
}
///Paste text into the terminal
pub fn paste(&self, text: &str) {
if self.term.lock().mode().contains(TermMode::BRACKETED_PASTE) {
self.write_to_pty("\x1b[200~".to_string());
self.write_to_pty(text.replace('\x1b', "").to_string());
self.write_to_pty("\x1b[201~".to_string());
} else {
self.write_to_pty(text.replace("\r\n", "\r").replace('\n', "\r"));
}
}
pub fn copy(&self) -> Option<String> {
let term = self.term.lock();
term.selection_to_string()
}
///Takes the selection out of the terminal
pub fn take_selection(&self) -> Option<Selection> {
self.term.lock().selection.take()
}
///Sets the selection object on the terminal
pub fn set_selection(&self, sel: Option<Selection>) {
self.term.lock().selection = sel;
}
pub fn render_lock<F, T>(&self, new_size: Option<TermDimensions>, f: F) -> T
where
F: FnOnce(RenderableContent, char) -> T,
{
if let Some(new_size) = new_size {
self.pty_tx.0.send(Msg::Resize(new_size.into())).ok(); //Give the PTY a chance to react to the new size
//TODO: Is this bad for performance?
}
let mut term = self.term.lock(); //Lock
if let Some(new_size) = new_size {
term.resize(new_size); //Reflow
}
let content = term.renderable_content();
let cursor_text = term.grid()[content.cursor.point].c;
f(content, cursor_text)
}
pub fn get_display_offset(&self) -> usize {
self.term.lock().renderable_content().display_offset
}
///Scroll the terminal
pub fn scroll(&self, scroll: Scroll) {
self.term.lock().scroll_display(scroll)
}
pub fn click(&self, point: Point, side: Direction, clicks: usize) {
let selection_type = match clicks {
0 => return, //This is a release
1 => Some(SelectionType::Simple),
2 => Some(SelectionType::Semantic),
3 => Some(SelectionType::Lines),
_ => None,
};
let selection =
selection_type.map(|selection_type| Selection::new(selection_type, point, side));
self.set_selection(selection);
}
pub fn drag(&self, point: Point, side: Direction) {
if let Some(mut selection) = self.take_selection() {
selection.update(point, side);
self.set_selection(Some(selection));
}
}
pub fn mouse_down(&self, point: Point, side: Direction) {
self.set_selection(Some(Selection::new(SelectionType::Simple, point, side)));
}
}
impl Drop for Terminal {
fn drop(&mut self) {
self.pty_tx.0.send(Msg::Shutdown).ok();
}
}
impl Entity for Terminal {
type Event = Event;
}
//TODO Move this around
mod alacritty_unix {
use alacritty_terminal::config::Program;
use gpui::anyhow::{bail, Result};
use libc;
use std::ffi::CStr;
use std::mem::MaybeUninit;
use std::ptr;
#[derive(Debug)]
pub struct Passwd<'a> {
_name: &'a str,
_dir: &'a str,
pub shell: &'a str,
}
/// Return a Passwd struct with pointers into the provided buf.
///
/// # Unsafety
///
/// If `buf` is changed while `Passwd` is alive, bad thing will almost certainly happen.
pub fn get_pw_entry(buf: &mut [i8; 1024]) -> Result<Passwd<'_>> {
// Create zeroed passwd struct.
let mut entry: MaybeUninit<libc::passwd> = MaybeUninit::uninit();
let mut res: *mut libc::passwd = ptr::null_mut();
// Try and read the pw file.
let uid = unsafe { libc::getuid() };
let status = unsafe {
libc::getpwuid_r(
uid,
entry.as_mut_ptr(),
buf.as_mut_ptr() as *mut _,
buf.len(),
&mut res,
)
};
let entry = unsafe { entry.assume_init() };
if status < 0 {
bail!("getpwuid_r failed");
}
if res.is_null() {
bail!("pw not found");
}
// Sanity check.
assert_eq!(entry.pw_uid, uid);
// Build a borrowed Passwd struct.
Ok(Passwd {
_name: unsafe { CStr::from_ptr(entry.pw_name).to_str().unwrap() },
_dir: unsafe { CStr::from_ptr(entry.pw_dir).to_str().unwrap() },
shell: unsafe { CStr::from_ptr(entry.pw_shell).to_str().unwrap() },
})
}
#[cfg(target_os = "macos")]
pub fn _default_shell(pw: &Passwd<'_>) -> Program {
let shell_name = pw.shell.rsplit('/').next().unwrap();
let argv = vec![
String::from("-c"),
format!("exec -a -{} {}", shell_name, pw.shell),
];
Program::WithArgs {
program: "/bin/bash".to_owned(),
args: argv,
}
}
#[cfg(not(target_os = "macos"))]
pub fn default_shell(pw: &Passwd<'_>) -> Program {
Program::Just(env::var("SHELL").unwrap_or_else(|_| pw.shell.to_owned()))
}
}

View file

@ -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<AlacTermEvent>);
//Make terminal view an enum, that can give you views for the error and non-error states
//Take away all the result unwrapping in the current TerminalView by making it 'infallible'
//Bubble up to deploy(_modal)() calls
impl EventListener for ZedListener {
fn send_event(&self, event: AlacTermEvent) {
self.0.unbounded_send(event).ok();
enum TerminalContent {
Connected(ViewHandle<ConnectedView>),
Error(ViewHandle<ErrorView>),
}
impl TerminalContent {
fn handle(&self) -> AnyViewHandle {
match self {
Self::Connected(handle) => handle.into(),
Self::Error(handle) => handle.into(),
}
}
}
///A terminal view, maintains the PTY's file handles and communicates with the terminal
pub struct Terminal {
connection: ModelHandle<TerminalConnection>,
has_new_content: bool,
//Currently using iTerm bell, show bell emoji in tab until input is received
has_bell: bool,
// Only for styling purposes. Doesn't effect behavior
pub struct TerminalView {
modal: bool,
content: TerminalContent,
associated_directory: Option<PathBuf>,
}
impl Entity for Terminal {
pub struct ErrorView {
error: TerminalError,
}
impl Entity for TerminalView {
type Event = Event;
}
impl 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<Workspace>) {
let working_directory = get_working_directory(workspace, cx);
let view = cx.add_view(|cx| TerminalView::new(working_directory, false, cx));
workspace.add_item(Box::new(view), cx);
}
///Create a new Terminal view. This spawns a task, a thread, and opens the TTY devices
///To get the right working directory from a workspace, use: `get_wd_for_workspace()`
fn new(working_directory: Option<PathBuf>, modal: bool, cx: &mut ViewContext<Self>) -> Self {
//The details here don't matter, the terminal will be resized on the first layout
let size_info = SizeInfo::new(
DEBUG_TERMINAL_WIDTH,
DEBUG_TERMINAL_HEIGHT,
DEBUG_CELL_WIDTH,
let size_info = TermDimensions::new(
DEBUG_LINE_HEIGHT,
0.,
0.,
false,
DEBUG_CELL_WIDTH,
vec2f(DEBUG_TERMINAL_WIDTH, DEBUG_TERMINAL_HEIGHT),
);
let (shell, envs) = {
let settings = cx.global::<Settings>();
let shell = settings.terminal_overrides.shell.clone();
let envs = settings.terminal_overrides.env.clone(); //Should be short and cheap.
(shell, envs)
let content = match TerminalBuilder::new(working_directory.clone(), shell, envs, size_info)
{
Ok(terminal) => {
let terminal = cx.add_model(|cx| terminal.subscribe(cx));
let view = cx.add_view(|cx| ConnectedView::from_terminal(terminal, modal, cx));
cx.subscribe(&view, |_this, _content, event, cx| cx.emit(event.clone()))
.detach();
TerminalContent::Connected(view)
}
Err(error) => {
let view = cx.add_view(|_| ErrorView {
error: error.downcast::<TerminalError>().unwrap(),
});
TerminalContent::Error(view)
}
};
cx.focus(content.handle());
let connection = cx
.add_model(|cx| TerminalConnection::new(working_directory, shell, envs, size_info, cx));
Terminal::from_connection(connection, modal, cx)
TerminalView {
modal,
content,
associated_directory: working_directory,
}
}
fn from_connection(
connection: ModelHandle<TerminalConnection>,
fn from_terminal(
terminal: ModelHandle<Terminal>,
modal: bool,
cx: &mut ViewContext<Self>,
) -> Terminal {
cx.observe(&connection, |_, _, cx| cx.notify()).detach();
cx.subscribe(&connection, |this, _, event, cx| match event {
Event::Wakeup => {
if cx.is_self_focused() {
cx.notify()
} else {
this.has_new_content = true;
cx.emit(Event::TitleChanged);
}
}
Event::Bell => {
this.has_bell = true;
cx.emit(Event::TitleChanged);
}
_ => cx.emit(*event),
})
.detach();
Terminal {
connection,
has_new_content: true,
has_bell: false,
) -> Self {
let connected_view = cx.add_view(|cx| ConnectedView::from_terminal(terminal, modal, cx));
TerminalView {
modal,
content: TerminalContent::Connected(connected_view),
associated_directory: None,
}
}
}
fn input(&mut self, text: &str, cx: &mut ViewContext<Self>) {
self.connection.update(cx, |connection, _| {
//TODO: This is probably not encoding UTF8 correctly (see alacritty/src/input.rs:L825-837)
connection.write_to_pty(text.to_string());
});
if self.has_bell {
self.has_bell = false;
cx.emit(Event::TitleChanged);
}
}
fn clear(&mut self, _: &Clear, cx: &mut ViewContext<Self>) {
self.connection
.update(cx, |connection, _| connection.clear());
}
///Create a new Terminal in the current working directory or the user's home directory
fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
let wd = get_wd_for_workspace(workspace, cx);
workspace.add_item(Box::new(cx.add_view(|cx| Terminal::new(wd, false, cx))), cx);
}
///Attempt to paste the clipboard into the terminal
fn copy(&mut self, _: &Copy, cx: &mut ViewContext<Self>) {
let term = self.connection.read(cx).term.lock();
let copy_text = term.selection_to_string();
match copy_text {
Some(s) => cx.write_to_clipboard(ClipboardItem::new(s)),
None => (),
}
}
///Attempt to paste the clipboard into the terminal
fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
if let Some(item) = cx.read_from_clipboard() {
self.connection.update(cx, |connection, _| {
connection.paste(item.text());
})
}
}
///Synthesize the keyboard event corresponding to 'up'
fn up(&mut self, _: &Up, cx: &mut ViewContext<Self>) {
self.connection.update(cx, |connection, _| {
connection.try_keystroke(&Keystroke::parse("up").unwrap());
});
}
///Synthesize the keyboard event corresponding to 'down'
fn down(&mut self, _: &Down, cx: &mut ViewContext<Self>) {
self.connection.update(cx, |connection, _| {
connection.try_keystroke(&Keystroke::parse("down").unwrap());
});
}
///Synthesize the keyboard event corresponding to 'ctrl-c'
fn ctrl_c(&mut self, _: &CtrlC, cx: &mut ViewContext<Self>) {
self.connection.update(cx, |connection, _| {
connection.try_keystroke(&Keystroke::parse("ctrl-c").unwrap());
});
}
///Synthesize the keyboard event corresponding to 'escape'
fn escape(&mut self, _: &Escape, cx: &mut ViewContext<Self>) {
self.connection.update(cx, |connection, _| {
connection.try_keystroke(&Keystroke::parse("escape").unwrap());
});
}
///Synthesize the keyboard event corresponding to 'enter'
fn enter(&mut self, _: &Enter, cx: &mut ViewContext<Self>) {
self.connection.update(cx, |connection, _| {
connection.try_keystroke(&Keystroke::parse("enter").unwrap());
});
}
}
impl View for Terminal {
impl View for TerminalView {
fn ui_name() -> &'static str {
"Terminal"
}
fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox {
let element = {
let connection_handle = self.connection.clone().downgrade();
let view_id = cx.view_id();
TerminalEl::new(view_id, connection_handle, self.modal).contained()
let child_view = match &self.content {
TerminalContent::Connected(connected) => ChildView::new(connected),
TerminalContent::Error(error) => ChildView::new(error),
};
if self.modal {
let settings = cx.global::<Settings>();
let container_style = settings.theme.terminal.modal_container;
element.with_style(container_style).boxed()
child_view.contained().with_style(container_style).boxed()
} else {
element.boxed()
child_view.boxed()
}
}
fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
cx.emit(Event::Activate);
self.has_new_content = false;
cx.defer(|view, cx| {
cx.focus(view.content.handle());
});
}
fn keymap_context(&self, _: &gpui::AppContext) -> gpui::keymap::Context {
@ -261,56 +172,72 @@ impl View for Terminal {
}
context
}
fn selected_text_range(&self, _: &AppContext) -> Option<std::ops::Range<usize>> {
Some(0..0)
}
fn replace_text_in_range(
&mut self,
_: Option<std::ops::Range<usize>>,
text: &str,
cx: &mut ViewContext<Self>,
) {
self.input(text, cx);
impl View for ErrorView {
fn ui_name() -> &'static str {
"Terminal Error"
}
fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox {
let settings = cx.global::<Settings>();
let style = TerminalEl::make_text_style(cx.font_cache(), settings);
//TODO:
//We want markdown style highlighting so we can format the program and working directory with ``
//We want a max-width of 75% with word-wrap
//We want to be able to select the text
//Want to be able to scroll if the error message is massive somehow (resiliency)
let program_text = {
match self.error.shell_to_string() {
Some(shell_txt) => format!("Shell Program: `{}`", shell_txt),
None => "No program specified".to_string(),
}
};
let directory_text = {
match self.error.directory.as_ref() {
Some(path) => format!("Working directory: `{}`", path.to_string_lossy()),
None => "No working directory specified".to_string(),
}
};
let error_text = self.error.source.to_string();
Flex::column()
.with_child(
Text::new("Failed to open the terminal.".to_string(), style.clone())
.contained()
.boxed(),
)
.with_child(Text::new(program_text, style.clone()).contained().boxed())
.with_child(Text::new(directory_text, style.clone()).contained().boxed())
.with_child(Text::new(error_text, style.clone()).contained().boxed())
.aligned()
.boxed()
}
}
impl Item for Terminal {
impl Item for TerminalView {
fn tab_content(
&self,
_detail: Option<usize>,
tab_theme: &theme::Tab,
cx: &gpui::AppContext,
) -> ElementBox {
let settings = cx.global::<Settings>();
let search_theme = &settings.theme.search; //TODO properly integrate themes
let mut flex = Flex::row();
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()
.with_margin_left(if self.has_bell {
search_theme.tab_icon_spacing
} else {
0.
})
.boxed(),
)
.boxed()
@ -318,10 +245,10 @@ impl Item for Terminal {
fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self> {
//From what I can tell, there's no way to tell the current working
//Directory of the terminal from outside the terminal. There might be
//Directory of the terminal from outside the shell. There might be
//solutions to this, but they are non-trivial and require more IPC
Some(Terminal::new(
self.connection.read(cx).associated_directory.clone(),
Some(TerminalView::new(
self.associated_directory.clone(),
false,
cx,
))
@ -370,8 +297,20 @@ impl Item for Terminal {
gpui::Task::ready(Ok(()))
}
fn is_dirty(&self, _: &gpui::AppContext) -> bool {
self.has_new_content
fn is_dirty(&self, cx: &gpui::AppContext) -> bool {
if let TerminalContent::Connected(connected) = &self.content {
connected.read(cx).has_new_content()
} else {
false
}
}
fn has_conflict(&self, cx: &AppContext) -> bool {
if let TerminalContent::Connected(connected) = &self.content {
connected.read(cx).has_bell()
} else {
false
}
}
fn should_update_tab_on_event(event: &Self::Event) -> bool {
@ -388,7 +327,7 @@ impl Item for Terminal {
}
///Get's the working directory for the given workspace, respecting the user's settings.
fn get_wd_for_workspace(workspace: &Workspace, cx: &AppContext) -> Option<PathBuf> {
fn get_working_directory(workspace: &Workspace, cx: &AppContext) -> Option<PathBuf> {
let wd_setting = cx
.global::<Settings>()
.terminal_overrides
@ -399,10 +338,12 @@ fn get_wd_for_workspace(workspace: &Workspace, cx: &AppContext) -> Option<PathBu
WorkingDirectory::CurrentProjectDirectory => current_project_directory(workspace, cx),
WorkingDirectory::FirstProjectDirectory => first_project_directory(workspace, cx),
WorkingDirectory::AlwaysHome => None,
WorkingDirectory::Always { directory } => shellexpand::full(&directory)
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()),
.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();

View file

@ -1,828 +0,0 @@
use alacritty_terminal::{
grid::{Dimensions, GridIterator, Indexed, Scroll},
index::{Column as GridCol, Line as GridLine, Point, Side},
selection::{Selection, SelectionRange, SelectionType},
sync::FairMutex,
term::{
cell::{Cell, Flags},
SizeInfo,
},
Term,
};
use editor::{Cursor, CursorShape, HighlightedRange, HighlightedRangeLine};
use gpui::{
color::Color,
elements::*,
fonts::{TextStyle, Underline},
geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
},
json::json,
text_layout::{Line, RunStyle},
Event, FontCache, KeyDownEvent, MouseButton, MouseButtonEvent, MouseMovedEvent, MouseRegion,
PaintContext, Quad, ScrollWheelEvent, SizeConstraint, TextLayoutCache, WeakModelHandle,
};
use itertools::Itertools;
use ordered_float::OrderedFloat;
use settings::Settings;
use theme::TerminalStyle;
use util::ResultExt;
use std::{cmp::min, ops::Range, sync::Arc};
use std::{fmt::Debug, ops::Sub};
use crate::{color_translation::convert_color, connection::TerminalConnection, ZedListener};
///Scrolling is unbearably sluggish by default. Alacritty supports a configurable
///Scroll multiplier that is set to 3 by default. This will be removed when I
///Implement scroll bars.
const ALACRITTY_SCROLL_MULTIPLIER: f32 = 3.;
///Used to display the grid as passed to Alacritty and the TTY.
///Useful for debugging inconsistencies between behavior and display
#[cfg(debug_assertions)]
const DEBUG_GRID: bool = false;
///The GPUI element that paints the terminal.
///We need to keep a reference to the view for mouse events, do we need it for any other terminal stuff, or can we move that to connection?
pub struct TerminalEl {
connection: WeakModelHandle<TerminalConnection>,
view_id: usize,
modal: bool,
}
///New type pattern so I don't mix these two up
struct CellWidth(f32);
struct LineHeight(f32);
struct LayoutLine {
cells: Vec<LayoutCell>,
highlighted_range: Option<Range<usize>>,
}
///New type pattern to ensure that we use adjusted mouse positions throughout the code base, rather than
struct PaneRelativePos(Vector2F);
///Functionally the constructor for the PaneRelativePos type, mutates the mouse_position
fn relative_pos(mouse_position: Vector2F, origin: Vector2F) -> PaneRelativePos {
PaneRelativePos(mouse_position.sub(origin)) //Avoid the extra allocation by mutating
}
#[derive(Clone, Debug, Default)]
struct LayoutCell {
point: Point<i32, i32>,
text: Line, //NOTE TO SELF THIS IS BAD PERFORMANCE RN!
background_color: Color,
}
impl LayoutCell {
fn new(point: Point<i32, i32>, text: Line, background_color: Color) -> LayoutCell {
LayoutCell {
point,
text,
background_color,
}
}
}
///The information generated during layout that is nescessary for painting
pub struct LayoutState {
layout_lines: Vec<LayoutLine>,
line_height: LineHeight,
em_width: CellWidth,
cursor: Option<Cursor>,
background_color: Color,
cur_size: SizeInfo,
terminal: Arc<FairMutex<Term<ZedListener>>>,
selection_color: Color,
}
impl TerminalEl {
pub fn new(
view_id: usize,
connection: WeakModelHandle<TerminalConnection>,
modal: bool,
) -> TerminalEl {
TerminalEl {
view_id,
connection,
modal,
}
}
}
impl Element for TerminalEl {
type LayoutState = LayoutState;
type PaintState = ();
fn layout(
&mut self,
constraint: gpui::SizeConstraint,
cx: &mut gpui::LayoutContext,
) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
//Settings immutably borrows cx here for the settings and font cache
//and we need to modify the cx to resize the terminal. So instead of
//storing Settings or the font_cache(), we toss them ASAP and then reborrow later
let text_style = make_text_style(cx.font_cache(), cx.global::<Settings>());
let line_height = LineHeight(cx.font_cache().line_height(text_style.font_size));
let cell_width = CellWidth(
cx.font_cache()
.em_advance(text_style.font_id, text_style.font_size),
);
let connection_handle = self.connection.upgrade(cx).unwrap();
//Tell the view our new size. Requires a mutable borrow of cx and the view
let cur_size = make_new_size(constraint, &cell_width, &line_height);
//Note that set_size locks and mutates the terminal.
connection_handle.update(cx.app, |connection, _| connection.set_size(cur_size));
let (selection_color, terminal_theme) = {
let theme = &(cx.global::<Settings>()).theme;
(theme.editor.selection.selection, &theme.terminal)
};
let terminal_mutex = connection_handle.read(cx).term.clone();
let term = terminal_mutex.lock();
let grid = term.grid();
let cursor_point = grid.cursor.point;
let cursor_text = grid[cursor_point.line][cursor_point.column].c.to_string();
let content = term.renderable_content();
let layout_lines = layout_lines(
content.display_iter,
&text_style,
terminal_theme,
cx.text_layout_cache,
self.modal,
content.selection,
);
let block_text = cx.text_layout_cache.layout_str(
&cursor_text,
text_style.font_size,
&[(
cursor_text.len(),
RunStyle {
font_id: text_style.font_id,
color: terminal_theme.colors.background,
underline: Default::default(),
},
)],
);
let cursor = get_cursor_shape(
content.cursor.point.line.0 as usize,
content.cursor.point.column.0 as usize,
content.display_offset,
&line_height,
&cell_width,
cur_size.total_lines(),
&block_text,
)
.map(move |(cursor_position, block_width)| {
let block_width = if block_width != 0.0 {
block_width
} else {
cell_width.0
};
Cursor::new(
cursor_position,
block_width,
line_height.0,
terminal_theme.colors.cursor,
CursorShape::Block,
Some(block_text.clone()),
)
});
drop(term);
let background_color = if self.modal {
terminal_theme.colors.modal_background
} else {
terminal_theme.colors.background
};
(
constraint.max,
LayoutState {
layout_lines,
line_height,
em_width: cell_width,
cursor,
cur_size,
background_color,
terminal: terminal_mutex,
selection_color,
},
)
}
fn paint(
&mut self,
bounds: gpui::geometry::rect::RectF,
visible_bounds: gpui::geometry::rect::RectF,
layout: &mut Self::LayoutState,
cx: &mut gpui::PaintContext,
) -> Self::PaintState {
//Setup element stuff
let clip_bounds = Some(visible_bounds);
cx.paint_layer(clip_bounds, |cx| {
let cur_size = layout.cur_size.clone();
let origin = bounds.origin() + vec2f(layout.em_width.0, 0.);
//Elements are ephemeral, only at paint time do we know what could be clicked by a mouse
attach_mouse_handlers(
origin,
cur_size,
self.view_id,
&layout.terminal,
visible_bounds,
cx,
);
cx.paint_layer(clip_bounds, |cx| {
//Start with a background color
cx.scene.push_quad(Quad {
bounds: RectF::new(bounds.origin(), bounds.size()),
background: Some(layout.background_color),
border: Default::default(),
corner_radius: 0.,
});
//Draw cell backgrounds
for layout_line in &layout.layout_lines {
for layout_cell in &layout_line.cells {
let position = vec2f(
(origin.x() + layout_cell.point.column as f32 * layout.em_width.0)
.floor(),
origin.y() + layout_cell.point.line as f32 * layout.line_height.0,
);
let size = vec2f(layout.em_width.0.ceil(), layout.line_height.0);
cx.scene.push_quad(Quad {
bounds: RectF::new(position, size),
background: Some(layout_cell.background_color),
border: Default::default(),
corner_radius: 0.,
})
}
}
});
//Draw Selection
cx.paint_layer(clip_bounds, |cx| {
let mut highlight_y = None;
let highlight_lines = layout
.layout_lines
.iter()
.filter_map(|line| {
if let Some(range) = &line.highlighted_range {
if let None = highlight_y {
highlight_y = Some(
origin.y()
+ line.cells[0].point.line as f32 * layout.line_height.0,
);
}
let start_x = origin.x()
+ line.cells[range.start].point.column as f32 * layout.em_width.0;
let end_x = origin.x()
+ line.cells[range.end].point.column as f32 * layout.em_width.0
+ layout.em_width.0;
return Some(HighlightedRangeLine { start_x, end_x });
} else {
return None;
}
})
.collect::<Vec<HighlightedRangeLine>>();
if let Some(y) = highlight_y {
let hr = HighlightedRange {
start_y: y, //Need to change this
line_height: layout.line_height.0,
lines: highlight_lines,
color: layout.selection_color,
//Copied from editor. TODO: move to theme or something
corner_radius: 0.15 * layout.line_height.0,
};
hr.paint(bounds, cx.scene);
}
});
cx.paint_layer(clip_bounds, |cx| {
for layout_line in &layout.layout_lines {
for layout_cell in &layout_line.cells {
let point = layout_cell.point;
//Don't actually know the start_x for a line, until here:
let cell_origin = vec2f(
(origin.x() + point.column as f32 * layout.em_width.0).floor(),
origin.y() + point.line as f32 * layout.line_height.0,
);
layout_cell.text.paint(
cell_origin,
visible_bounds,
layout.line_height.0,
cx,
);
}
}
});
//Draw cursor
if let Some(cursor) = &layout.cursor {
cx.paint_layer(clip_bounds, |cx| {
cursor.paint(origin, cx);
})
}
#[cfg(debug_assertions)]
if DEBUG_GRID {
cx.paint_layer(clip_bounds, |cx| {
draw_debug_grid(bounds, layout, cx);
})
}
});
}
fn dispatch_event(
&mut self,
event: &gpui::Event,
_bounds: gpui::geometry::rect::RectF,
visible_bounds: gpui::geometry::rect::RectF,
layout: &mut Self::LayoutState,
_paint: &mut Self::PaintState,
cx: &mut gpui::EventContext,
) -> bool {
match event {
Event::ScrollWheel(ScrollWheelEvent {
delta, position, ..
}) => visible_bounds
.contains_point(*position)
.then(|| {
let vertical_scroll =
(delta.y() / layout.line_height.0) * ALACRITTY_SCROLL_MULTIPLIER;
if let Some(connection) = self.connection.upgrade(cx.app) {
connection.update(cx.app, |connection, _| {
connection
.term
.lock()
.scroll_display(Scroll::Delta(vertical_scroll.round() as i32));
})
}
})
.is_some(),
Event::KeyDown(KeyDownEvent { keystroke, .. }) => {
if !cx.is_parent_view_focused() {
return false;
}
self.connection
.upgrade(cx.app)
.map(|connection| {
connection
.update(cx.app, |connection, _| connection.try_keystroke(keystroke))
})
.unwrap_or(false)
}
_ => false,
}
}
fn rect_for_text_range(
&self,
_: Range<usize>,
bounds: RectF,
_: RectF,
layout: &Self::LayoutState,
_: &Self::PaintState,
_: &gpui::MeasurementContext,
) -> Option<RectF> {
// Use the same origin that's passed to `Cursor::paint` in the paint
// method bove.
let mut origin = bounds.origin() + vec2f(layout.em_width.0, 0.);
// TODO - Why is it necessary to move downward one line to get correct
// positioning? I would think that we'd want the same rect that is
// painted for the cursor.
origin += vec2f(0., layout.line_height.0);
Some(layout.cursor.as_ref()?.bounding_rect(origin))
}
fn debug(
&self,
_bounds: gpui::geometry::rect::RectF,
_layout: &Self::LayoutState,
_paint: &Self::PaintState,
_cx: &gpui::DebugContext,
) -> gpui::serde_json::Value {
json!({
"type": "TerminalElement",
})
}
}
pub fn mouse_to_cell_data(
pos: Vector2F,
origin: Vector2F,
cur_size: SizeInfo,
display_offset: usize,
) -> (Point, alacritty_terminal::index::Direction) {
let relative_pos = relative_pos(pos, origin);
let point = grid_cell(&relative_pos, cur_size, display_offset);
let side = cell_side(&relative_pos, cur_size);
(point, side)
}
///Configures a text style from the current settings.
fn make_text_style(font_cache: &FontCache, settings: &Settings) -> TextStyle {
// Pull the font family from settings properly overriding
let family_id = settings
.terminal_overrides
.font_family
.as_ref()
.and_then(|family_name| font_cache.load_family(&[family_name]).log_err())
.or_else(|| {
settings
.terminal_defaults
.font_family
.as_ref()
.and_then(|family_name| font_cache.load_family(&[family_name]).log_err())
})
.unwrap_or(settings.buffer_font_family);
TextStyle {
color: settings.theme.editor.text_color,
font_family_id: family_id,
font_family_name: font_cache.family_name(family_id).unwrap(),
font_id: font_cache
.select_font(family_id, &Default::default())
.unwrap(),
font_size: settings
.terminal_overrides
.font_size
.or(settings.terminal_defaults.font_size)
.unwrap_or(settings.buffer_font_size),
font_properties: Default::default(),
underline: Default::default(),
}
}
///Configures a size info object from the given information.
fn make_new_size(
constraint: SizeConstraint,
cell_width: &CellWidth,
line_height: &LineHeight,
) -> SizeInfo {
SizeInfo::new(
constraint.max.x() - cell_width.0,
constraint.max.y(),
cell_width.0,
line_height.0,
0.,
0.,
false,
)
}
fn layout_lines(
grid: GridIterator<Cell>,
text_style: &TextStyle,
terminal_theme: &TerminalStyle,
text_layout_cache: &TextLayoutCache,
modal: bool,
selection_range: Option<SelectionRange>,
) -> Vec<LayoutLine> {
let lines = grid.group_by(|i| i.point.line);
lines
.into_iter()
.enumerate()
.map(|(line_index, (_, line))| {
let mut highlighted_range = None;
let cells = line
.enumerate()
.map(|(x_index, indexed_cell)| {
if selection_range
.map(|range| range.contains(indexed_cell.point))
.unwrap_or(false)
{
let mut range = highlighted_range.take().unwrap_or(x_index..x_index);
range.end = range.end.max(x_index);
highlighted_range = Some(range);
}
let cell_text = &indexed_cell.c.to_string();
let cell_style = cell_style(&indexed_cell, terminal_theme, text_style, modal);
//This is where we might be able to get better performance
let layout_cell = text_layout_cache.layout_str(
cell_text,
text_style.font_size,
&[(cell_text.len(), cell_style)],
);
LayoutCell::new(
Point::new(line_index as i32, indexed_cell.point.column.0 as i32),
layout_cell,
convert_color(&indexed_cell.bg, &terminal_theme.colors, modal),
)
})
.collect::<Vec<LayoutCell>>();
LayoutLine {
cells,
highlighted_range,
}
})
.collect::<Vec<LayoutLine>>()
}
// Compute the cursor position and expected block width, may return a zero width if x_for_index returns
// the same position for sequential indexes. Use em_width instead
//TODO: This function is messy, too many arguments and too many ifs. Simplify.
fn get_cursor_shape(
line: usize,
line_index: usize,
display_offset: usize,
line_height: &LineHeight,
cell_width: &CellWidth,
total_lines: usize,
text_fragment: &Line,
) -> Option<(Vector2F, f32)> {
let cursor_line = line + display_offset;
if cursor_line <= total_lines {
let cursor_width = if text_fragment.width() == 0. {
cell_width.0
} else {
text_fragment.width()
};
Some((
vec2f(
line_index as f32 * cell_width.0,
cursor_line as f32 * line_height.0,
),
cursor_width,
))
} else {
None
}
}
///Convert the Alacritty cell styles to GPUI text styles and background color
fn cell_style(
indexed: &Indexed<&Cell>,
style: &TerminalStyle,
text_style: &TextStyle,
modal: bool,
) -> RunStyle {
let flags = indexed.cell.flags;
let fg = convert_color(&indexed.cell.fg, &style.colors, modal);
let underline = flags
.contains(Flags::UNDERLINE)
.then(|| Underline {
color: Some(fg),
squiggly: false,
thickness: OrderedFloat(1.),
})
.unwrap_or_default();
RunStyle {
color: fg,
font_id: text_style.font_id,
underline,
}
}
fn attach_mouse_handlers(
origin: Vector2F,
cur_size: SizeInfo,
view_id: usize,
terminal_mutex: &Arc<FairMutex<Term<ZedListener>>>,
visible_bounds: RectF,
cx: &mut PaintContext,
) {
let click_mutex = terminal_mutex.clone();
let drag_mutex = terminal_mutex.clone();
let mouse_down_mutex = terminal_mutex.clone();
cx.scene.push_mouse_region(
MouseRegion::new(view_id, None, visible_bounds)
.on_down(
MouseButton::Left,
move |MouseButtonEvent { position, .. }, _| {
let mut term = mouse_down_mutex.lock();
let (point, side) = mouse_to_cell_data(
position,
origin,
cur_size,
term.renderable_content().display_offset,
);
term.selection = Some(Selection::new(SelectionType::Simple, point, side))
},
)
.on_click(
MouseButton::Left,
move |MouseButtonEvent {
position,
click_count,
..
},
cx| {
let mut term = click_mutex.lock();
let (point, side) = mouse_to_cell_data(
position,
origin,
cur_size,
term.renderable_content().display_offset,
);
let selection_type = match click_count {
0 => return, //This is a release
1 => Some(SelectionType::Simple),
2 => Some(SelectionType::Semantic),
3 => Some(SelectionType::Lines),
_ => None,
};
let selection = selection_type
.map(|selection_type| Selection::new(selection_type, point, side));
term.selection = selection;
cx.focus_parent_view();
cx.notify();
},
)
.on_drag(
MouseButton::Left,
move |_, MouseMovedEvent { position, .. }, cx| {
let mut term = drag_mutex.lock();
let (point, side) = mouse_to_cell_data(
position,
origin,
cur_size,
term.renderable_content().display_offset,
);
if let Some(mut selection) = term.selection.take() {
selection.update(point, side);
term.selection = Some(selection);
}
cx.notify();
},
),
);
}
///Copied (with modifications) from alacritty/src/input.rs > Processor::cell_side()
fn cell_side(pos: &PaneRelativePos, cur_size: SizeInfo) -> Side {
let x = pos.0.x() as usize;
let cell_x = x.saturating_sub(cur_size.cell_width() as usize) % cur_size.cell_width() as usize;
let half_cell_width = (cur_size.cell_width() / 2.0) as usize;
let additional_padding =
(cur_size.width() - cur_size.cell_width() * 2.) % cur_size.cell_width();
let end_of_grid = cur_size.width() - cur_size.cell_width() - additional_padding;
if cell_x > half_cell_width
// Edge case when mouse leaves the window.
|| x as f32 >= end_of_grid
{
Side::Right
} else {
Side::Left
}
}
///Copied (with modifications) from alacritty/src/event.rs > Mouse::point()
///Position is a pane-relative position. That means the top left corner of the mouse
///Region should be (0,0)
fn grid_cell(pos: &PaneRelativePos, cur_size: SizeInfo, display_offset: usize) -> Point {
let pos = pos.0;
let col = pos.x() / cur_size.cell_width(); //TODO: underflow...
let col = min(GridCol(col as usize), cur_size.last_column());
let line = pos.y() / cur_size.cell_height();
let line = min(line as i32, cur_size.bottommost_line().0);
//when clicking, need to ADD to get to the top left cell
//e.g. total_lines - viewport_height, THEN subtract display offset
//0 -> total_lines - viewport_height - display_offset + mouse_line
Point::new(GridLine(line - display_offset as i32), col)
}
///Draws the grid as Alacritty sees it. Useful for checking if there is an inconsistency between
///Display and conceptual grid.
#[cfg(debug_assertions)]
fn draw_debug_grid(bounds: RectF, layout: &mut LayoutState, cx: &mut PaintContext) {
let width = layout.cur_size.width();
let height = layout.cur_size.height();
//Alacritty uses 'as usize', so shall we.
for col in 0..(width / layout.em_width.0).round() as usize {
cx.scene.push_quad(Quad {
bounds: RectF::new(
bounds.origin() + vec2f((col + 1) as f32 * layout.em_width.0, 0.),
vec2f(1., height),
),
background: Some(Color::green()),
border: Default::default(),
corner_radius: 0.,
});
}
for row in 0..((height / layout.line_height.0) + 1.0).round() as usize {
cx.scene.push_quad(Quad {
bounds: RectF::new(
bounds.origin() + vec2f(layout.em_width.0, row as f32 * layout.line_height.0),
vec2f(width, 1.),
),
background: Some(Color::green()),
border: Default::default(),
corner_radius: 0.,
});
}
}
mod test {
#[test]
fn test_mouse_to_selection() {
let term_width = 100.;
let term_height = 200.;
let cell_width = 10.;
let line_height = 20.;
let mouse_pos_x = 100.; //Window relative
let mouse_pos_y = 100.; //Window relative
let origin_x = 10.;
let origin_y = 20.;
let cur_size = alacritty_terminal::term::SizeInfo::new(
term_width,
term_height,
cell_width,
line_height,
0.,
0.,
false,
);
let mouse_pos = gpui::geometry::vector::vec2f(mouse_pos_x, mouse_pos_y);
let origin = gpui::geometry::vector::vec2f(origin_x, origin_y); //Position of terminal window, 1 'cell' in
let (point, _) =
crate::terminal_element::mouse_to_cell_data(mouse_pos, origin, cur_size, 0);
assert_eq!(
point,
alacritty_terminal::index::Point::new(
alacritty_terminal::index::Line(((mouse_pos_y - origin_y) / line_height) as i32),
alacritty_terminal::index::Column(((mouse_pos_x - origin_x) / cell_width) as usize),
)
);
}
#[test]
fn test_mouse_to_selection_off_edge() {
let term_width = 100.;
let term_height = 200.;
let cell_width = 10.;
let line_height = 20.;
let mouse_pos_x = 100.; //Window relative
let mouse_pos_y = 100.; //Window relative
let origin_x = 10.;
let origin_y = 20.;
let cur_size = alacritty_terminal::term::SizeInfo::new(
term_width,
term_height,
cell_width,
line_height,
0.,
0.,
false,
);
let mouse_pos = gpui::geometry::vector::vec2f(mouse_pos_x, mouse_pos_y);
let origin = gpui::geometry::vector::vec2f(origin_x, origin_y); //Position of terminal window, 1 'cell' in
let (point, _) =
crate::terminal_element::mouse_to_cell_data(mouse_pos, origin, cur_size, 0);
assert_eq!(
point,
alacritty_terminal::index::Point::new(
alacritty_terminal::index::Line(((mouse_pos_y - origin_y) / line_height) as i32),
alacritty_terminal::index::Column(((mouse_pos_x - origin_x) / cell_width) as usize),
)
);
}
}

View file

@ -1,35 +1,40 @@
use std::time::Duration;
use std::{path::Path, time::Duration};
use alacritty_terminal::term::SizeInfo;
use gpui::{AppContext, ModelHandle, ReadModelWith, TestAppContext};
use gpui::{
geometry::vector::vec2f, AppContext, ModelHandle, ReadModelWith, TestAppContext, ViewHandle,
};
use itertools::Itertools;
use project::{Entry, Project, ProjectPath, Worktree};
use workspace::{AppState, Workspace};
use crate::{
connection::TerminalConnection, DEBUG_CELL_WIDTH, DEBUG_LINE_HEIGHT, DEBUG_TERMINAL_HEIGHT,
DEBUG_TERMINAL_WIDTH,
connected_el::TermDimensions,
model::{Terminal, TerminalBuilder},
DEBUG_CELL_WIDTH, DEBUG_LINE_HEIGHT, DEBUG_TERMINAL_HEIGHT, DEBUG_TERMINAL_WIDTH,
};
pub struct TerminalTestContext<'a> {
pub cx: &'a mut TestAppContext,
pub connection: ModelHandle<TerminalConnection>,
pub connection: Option<ModelHandle<Terminal>>,
}
impl<'a> TerminalTestContext<'a> {
pub fn new(cx: &'a mut TestAppContext) -> Self {
pub fn new(cx: &'a mut TestAppContext, term: bool) -> Self {
cx.set_condition_duration(Some(Duration::from_secs(5)));
let size_info = SizeInfo::new(
DEBUG_TERMINAL_WIDTH,
DEBUG_TERMINAL_HEIGHT,
let size_info = TermDimensions::new(
DEBUG_CELL_WIDTH,
DEBUG_LINE_HEIGHT,
0.,
0.,
false,
vec2f(DEBUG_TERMINAL_WIDTH, DEBUG_TERMINAL_HEIGHT),
);
let connection =
cx.add_model(|cx| TerminalConnection::new(None, None, None, size_info, cx));
let connection = term.then(|| {
cx.add_model(|cx| {
TerminalBuilder::new(None, None, None, size_info)
.unwrap()
.subscribe(cx)
})
});
TerminalTestContext { cx, connection }
}
@ -38,34 +43,112 @@ impl<'a> TerminalTestContext<'a> {
where
F: Fn(String, &AppContext) -> bool,
{
let connection = self.connection.take().unwrap();
let command = command.to_string();
self.connection.update(self.cx, |connection, _| {
connection.update(self.cx, |connection, _| {
connection.write_to_pty(command);
connection.write_to_pty("\r".to_string());
});
self.connection
connection
.condition(self.cx, |conn, cx| {
let content = Self::grid_as_str(conn);
f(content, cx)
})
.await;
self.cx
.read_model_with(&self.connection, &mut |conn, _: &AppContext| {
let res = self
.cx
.read_model_with(&connection, &mut |conn, _: &AppContext| {
Self::grid_as_str(conn)
})
});
self.connection = Some(connection);
res
}
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);
///Creates a worktree with 1 file: /root.txt
pub async fn blank_workspace(&mut self) -> (ModelHandle<Project>, ViewHandle<Workspace>) {
let params = self.cx.update(AppState::test);
let project = Project::test(params.fs.clone(), [], self.cx).await;
let (_, workspace) = self.cx.add_window(|cx| Workspace::new(project.clone(), cx));
(project, workspace)
}
///Creates a worktree with 1 folder: /root{suffix}/
pub async fn create_folder_wt(
&mut self,
project: ModelHandle<Project>,
path: impl AsRef<Path>,
) -> (ModelHandle<Worktree>, Entry) {
self.create_wt(project, true, path).await
}
///Creates a worktree with 1 file: /root{suffix}.txt
pub async fn create_file_wt(
&mut self,
project: ModelHandle<Project>,
path: impl AsRef<Path>,
) -> (ModelHandle<Worktree>, Entry) {
self.create_wt(project, false, path).await
}
async fn create_wt(
&mut self,
project: ModelHandle<Project>,
is_dir: bool,
path: impl AsRef<Path>,
) -> (ModelHandle<Worktree>, Entry) {
let (wt, _) = project
.update(self.cx, |project, cx| {
project.find_or_create_local_worktree(path, true, cx)
})
.await
.unwrap();
let entry = self
.cx
.update(|cx| {
wt.update(cx, |wt, cx| {
wt.as_local()
.unwrap()
.create_entry(Path::new(""), is_dir, cx)
})
})
.await
.unwrap();
(wt, entry)
}
pub fn insert_active_entry_for(
&mut self,
wt: ModelHandle<Worktree>,
entry: Entry,
project: ModelHandle<Project>,
) {
self.cx.update(|cx| {
let p = ProjectPath {
worktree_id: wt.read(cx).id(),
path: entry.path,
};
project.update(cx, |project, cx| project.set_active_path(Some(p), cx));
});
}
fn grid_as_str(connection: &Terminal) -> String {
connection.render_lock(None, |content, _| {
let lines = content.display_iter.group_by(|i| i.point.line.0);
lines
.into_iter()
.map(|(_, line)| line.map(|i| i.c).collect::<String>())
.collect::<Vec<String>>()
.join("\n")
})
}
}

View file

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

View file

@ -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,

View file

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

View file

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

View file

@ -2,21 +2,47 @@ import Theme from "../themes/common/theme";
import { backgroundColor, border, popoverShadow, text } from "./components";
export default function HoverPopover(theme: Theme) {
return {
container: {
let baseContainer = {
background: backgroundColor(theme, "on500"),
cornerRadius: 8,
padding: {
left: 8,
right: 8,
top: 4,
bottom: 4,
bottom: 4
},
shadow: popoverShadow(theme),
border: border(theme, "primary"),
border: border(theme, "secondary"),
margin: {
left: -8,
},
};
return {
container: baseContainer,
infoContainer: {
...baseContainer,
background: backgroundColor(theme, "on500Info"),
border: {
color: theme.ramps.blue(0).hex(),
width: 1,
},
},
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 },

View file

@ -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 = {

View file

@ -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;