Merge branch 'main' into ime-support-2
This commit is contained in:
commit
ca3e73106c
32 changed files with 2520 additions and 1790 deletions
13
Cargo.lock
generated
13
Cargo.lock
generated
|
@ -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",
|
||||
]
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(),
|
||||
);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,35 +590,78 @@ impl EditorElement {
|
|||
cx.scene.pop_stacking_context();
|
||||
}
|
||||
|
||||
if let Some((position, hover_popover)) = layout.hover.as_mut() {
|
||||
if let Some((position, hover_popovers)) = layout.hover_popovers.as_mut() {
|
||||
cx.scene.push_stacking_context(None);
|
||||
|
||||
// This is safe because we check on layout whether the required row is available
|
||||
let hovered_row_layout = &layout.line_layouts[(position.row() - start_row) as usize];
|
||||
let size = hover_popover.size();
|
||||
|
||||
// Minimum required size: Take the first popover, and add 1.5 times the minimum popover
|
||||
// height. This is the size we will use to decide whether to render popovers above or below
|
||||
// the hovered line.
|
||||
let first_size = hover_popovers[0].size();
|
||||
let height_to_reserve =
|
||||
first_size.y() + 1.5 * MIN_POPOVER_LINE_HEIGHT as f32 * layout.line_height;
|
||||
|
||||
// Compute Hovered Point
|
||||
let x = hovered_row_layout.x_for_index(position.column() as usize) - scroll_left;
|
||||
let y = position.row() as f32 * layout.line_height - scroll_top - size.y();
|
||||
let mut popover_origin = content_origin + vec2f(x, y);
|
||||
let y = position.row() as f32 * layout.line_height - scroll_top;
|
||||
let hovered_point = content_origin + vec2f(x, y);
|
||||
|
||||
if popover_origin.y() < 0.0 {
|
||||
popover_origin.set_y(popover_origin.y() + layout.line_height + size.y());
|
||||
paint.hover_popover_bounds.clear();
|
||||
|
||||
if hovered_point.y() - height_to_reserve > 0.0 {
|
||||
// There is enough space above. Render popovers above the hovered point
|
||||
let mut current_y = hovered_point.y();
|
||||
for hover_popover in hover_popovers {
|
||||
let size = hover_popover.size();
|
||||
let mut popover_origin = vec2f(hovered_point.x(), current_y - size.y());
|
||||
|
||||
let x_out_of_bounds = bounds.max_x() - (popover_origin.x() + size.x());
|
||||
if x_out_of_bounds < 0.0 {
|
||||
popover_origin.set_x(popover_origin.x() + x_out_of_bounds);
|
||||
}
|
||||
|
||||
hover_popover.paint(
|
||||
popover_origin,
|
||||
RectF::from_points(Vector2F::zero(), vec2f(f32::MAX, f32::MAX)), // Let content bleed outside of editor
|
||||
cx,
|
||||
);
|
||||
|
||||
paint.hover_popover_bounds.push(
|
||||
RectF::new(popover_origin, hover_popover.size())
|
||||
.dilate(Vector2F::new(0., 5.)),
|
||||
);
|
||||
|
||||
current_y = popover_origin.y() - HOVER_POPOVER_GAP;
|
||||
}
|
||||
} else {
|
||||
// There is not enough space above. Render popovers below the hovered point
|
||||
let mut current_y = hovered_point.y() + layout.line_height;
|
||||
for hover_popover in hover_popovers {
|
||||
let size = hover_popover.size();
|
||||
let mut popover_origin = vec2f(hovered_point.x(), current_y);
|
||||
|
||||
let x_out_of_bounds = bounds.max_x() - (popover_origin.x() + size.x());
|
||||
if x_out_of_bounds < 0.0 {
|
||||
popover_origin.set_x(popover_origin.x() + x_out_of_bounds);
|
||||
}
|
||||
|
||||
hover_popover.paint(
|
||||
popover_origin,
|
||||
RectF::from_points(Vector2F::zero(), vec2f(f32::MAX, f32::MAX)), // Let content bleed outside of editor
|
||||
cx,
|
||||
);
|
||||
|
||||
paint.hover_popover_bounds.push(
|
||||
RectF::new(popover_origin, hover_popover.size())
|
||||
.dilate(Vector2F::new(0., 5.)),
|
||||
);
|
||||
|
||||
current_y = popover_origin.y() + size.y() + HOVER_POPOVER_GAP;
|
||||
}
|
||||
}
|
||||
|
||||
let x_out_of_bounds = bounds.max_x() - (popover_origin.x() + size.x());
|
||||
if x_out_of_bounds < 0.0 {
|
||||
popover_origin.set_x(popover_origin.x() + x_out_of_bounds);
|
||||
}
|
||||
|
||||
hover_popover.paint(
|
||||
popover_origin,
|
||||
RectF::from_points(Vector2F::zero(), vec2f(f32::MAX, f32::MAX)), // Let content bleed outside of editor
|
||||
cx,
|
||||
);
|
||||
|
||||
paint.hover_bounds = Some(
|
||||
RectF::new(popover_origin, hover_popover.size()).dilate(Vector2F::new(0., 5.)),
|
||||
);
|
||||
|
||||
cx.scene.pop_stacking_context();
|
||||
}
|
||||
|
||||
|
@ -1147,6 +1195,8 @@ impl Element for EditorElement {
|
|||
});
|
||||
|
||||
let scroll_position = snapshot.scroll_position();
|
||||
// The scroll position is a fractional point, the whole number of which represents
|
||||
// the top of the window in terms of display rows.
|
||||
let start_row = scroll_position.y() as u32;
|
||||
let scroll_top = scroll_position.y() * line_height;
|
||||
|
||||
|
@ -1320,16 +1370,8 @@ impl Element for EditorElement {
|
|||
.map(|indicator| (newest_selection_head.row(), indicator));
|
||||
}
|
||||
|
||||
hover = view.hover_state.popover.clone().and_then(|hover| {
|
||||
let (point, rendered) = hover.render(&snapshot, style.clone(), cx);
|
||||
if point.row() >= snapshot.scroll_position().y() as u32 {
|
||||
if line_layouts.len() > (point.row() - start_row) as usize {
|
||||
return Some((point, rendered));
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
});
|
||||
let visible_rows = start_row..start_row + line_layouts.len() as u32;
|
||||
hover = view.hover_state.render(&snapshot, &style, visible_rows, cx);
|
||||
});
|
||||
|
||||
if let Some((_, context_menu)) = context_menu.as_mut() {
|
||||
|
@ -1352,21 +1394,23 @@ impl Element for EditorElement {
|
|||
);
|
||||
}
|
||||
|
||||
if let Some((_, hover)) = hover.as_mut() {
|
||||
hover.layout(
|
||||
SizeConstraint {
|
||||
min: Vector2F::zero(),
|
||||
max: vec2f(
|
||||
(120. * em_width) // Default size
|
||||
.min(size.x() / 2.) // Shrink to half of the editor width
|
||||
.max(20. * em_width), // Apply minimum width of 20 characters
|
||||
(16. * line_height) // Default size
|
||||
.min(size.y() / 2.) // Shrink to half of the editor height
|
||||
.max(4. * line_height), // Apply minimum height of 4 lines
|
||||
),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
if let Some((_, hover_popovers)) = hover.as_mut() {
|
||||
for hover_popover in hover_popovers.iter_mut() {
|
||||
hover_popover.layout(
|
||||
SizeConstraint {
|
||||
min: Vector2F::zero(),
|
||||
max: vec2f(
|
||||
(120. * em_width) // Default size
|
||||
.min(size.x() / 2.) // Shrink to half of the editor width
|
||||
.max(MIN_POPOVER_CHARACTER_WIDTH * em_width), // Apply minimum width of 20 characters
|
||||
(16. * line_height) // Default size
|
||||
.min(size.y() / 2.) // Shrink to half of the editor height
|
||||
.max(MIN_POPOVER_LINE_HEIGHT * line_height), // Apply minimum height of 4 lines
|
||||
),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
(
|
||||
|
@ -1391,7 +1435,7 @@ impl Element for EditorElement {
|
|||
selections,
|
||||
context_menu,
|
||||
code_actions_indicator,
|
||||
hover,
|
||||
hover_popovers: hover,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
@ -1416,7 +1460,7 @@ impl Element for EditorElement {
|
|||
gutter_bounds,
|
||||
text_bounds,
|
||||
context_menu_bounds: None,
|
||||
hover_bounds: None,
|
||||
hover_popover_bounds: Default::default(),
|
||||
};
|
||||
|
||||
self.paint_background(gutter_bounds, text_bounds, layout, cx);
|
||||
|
@ -1457,9 +1501,11 @@ impl Element for EditorElement {
|
|||
}
|
||||
}
|
||||
|
||||
if let Some((_, hover)) = &mut layout.hover {
|
||||
if hover.dispatch_event(event, cx) {
|
||||
return true;
|
||||
if let Some((_, popover_elements)) = &mut layout.hover_popovers {
|
||||
for popover_element in popover_elements.iter_mut() {
|
||||
if popover_element.dispatch_event(event, cx) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1590,7 +1636,7 @@ pub struct LayoutState {
|
|||
selections: Vec<(ReplicaId, Vec<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 {
|
||||
|
|
|
@ -3,9 +3,10 @@ use gpui::{
|
|||
elements::{Flex, MouseEventHandler, Padding, Text},
|
||||
impl_internal_actions,
|
||||
platform::CursorStyle,
|
||||
Axis, Element, ElementBox, ModelHandle, MutableAppContext, RenderContext, Task, ViewContext,
|
||||
Axis, Element, ElementBox, ModelHandle, MouseButton, MutableAppContext, RenderContext, Task,
|
||||
ViewContext,
|
||||
};
|
||||
use language::Bias;
|
||||
use language::{Bias, DiagnosticEntry, DiagnosticSeverity};
|
||||
use project::{HoverBlock, Project};
|
||||
use settings::Settings;
|
||||
use std::{ops::Range, time::Duration};
|
||||
|
@ -13,7 +14,7 @@ use util::TryFutureExt;
|
|||
|
||||
use crate::{
|
||||
display_map::ToDisplayPoint, Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSnapshot,
|
||||
EditorStyle,
|
||||
EditorStyle, GoToDiagnostic, RangeToAnchorExt,
|
||||
};
|
||||
|
||||
pub const HOVER_DELAY_MILLIS: u64 = 350;
|
||||
|
@ -54,17 +55,11 @@ pub fn hover_at(editor: &mut Editor, action: &HoverAt, cx: &mut ViewContext<Edit
|
|||
/// Triggered by the `Hover` action when the cursor is not over a symbol or when the
|
||||
/// selections changed.
|
||||
pub fn hide_hover(editor: &mut Editor, cx: &mut ViewContext<Editor>) -> bool {
|
||||
let mut did_hide = false;
|
||||
let did_hide = editor.hover_state.info_popover.take().is_some()
|
||||
| editor.hover_state.diagnostic_popover.take().is_some();
|
||||
|
||||
// only notify the context once
|
||||
if editor.hover_state.popover.is_some() {
|
||||
editor.hover_state.popover = None;
|
||||
did_hide = true;
|
||||
cx.notify();
|
||||
}
|
||||
editor.hover_state.task = None;
|
||||
editor.hover_state.info_task = None;
|
||||
editor.hover_state.triggered_from = None;
|
||||
editor.hover_state.symbol_range = None;
|
||||
|
||||
editor.clear_background_highlights::<HoverState>(cx);
|
||||
|
||||
|
@ -114,8 +109,8 @@ fn show_hover(
|
|||
};
|
||||
|
||||
if !ignore_timeout {
|
||||
if let Some(range) = &editor.hover_state.symbol_range {
|
||||
if range
|
||||
if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover {
|
||||
if symbol_range
|
||||
.to_offset(&snapshot.buffer_snapshot)
|
||||
.contains(&multibuffer_offset)
|
||||
{
|
||||
|
@ -167,6 +162,43 @@ fn show_hover(
|
|||
})
|
||||
});
|
||||
|
||||
if let Some(delay) = delay {
|
||||
delay.await;
|
||||
}
|
||||
|
||||
// If there's a diagnostic, assign it on the hover state and notify
|
||||
let local_diagnostic = snapshot
|
||||
.buffer_snapshot
|
||||
.diagnostics_in_range::<_, usize>(multibuffer_offset..multibuffer_offset, false)
|
||||
// Find the entry with the most specific range
|
||||
.min_by_key(|entry| entry.range.end - entry.range.start)
|
||||
.map(|entry| DiagnosticEntry {
|
||||
diagnostic: entry.diagnostic,
|
||||
range: entry.range.to_anchors(&snapshot.buffer_snapshot),
|
||||
});
|
||||
|
||||
// Pull the primary diagnostic out so we can jump to it if the popover is clicked
|
||||
let primary_diagnostic = local_diagnostic.as_ref().and_then(|local_diagnostic| {
|
||||
snapshot
|
||||
.buffer_snapshot
|
||||
.diagnostic_group::<usize>(local_diagnostic.diagnostic.group_id)
|
||||
.find(|diagnostic| diagnostic.diagnostic.is_primary)
|
||||
.map(|entry| DiagnosticEntry {
|
||||
diagnostic: entry.diagnostic,
|
||||
range: entry.range.to_anchors(&snapshot.buffer_snapshot),
|
||||
})
|
||||
});
|
||||
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
this.update(&mut cx, |this, _| {
|
||||
this.hover_state.diagnostic_popover =
|
||||
local_diagnostic.map(|local_diagnostic| DiagnosticPopover {
|
||||
local_diagnostic,
|
||||
primary_diagnostic,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Construct new hover popover from hover request
|
||||
let hover_popover = hover_request.await.ok().flatten().and_then(|hover_result| {
|
||||
if hover_result.contents.is_empty() {
|
||||
|
@ -188,45 +220,28 @@ fn show_hover(
|
|||
anchor.clone()..anchor.clone()
|
||||
};
|
||||
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
this.update(&mut cx, |this, _| {
|
||||
this.hover_state.symbol_range = Some(range.clone());
|
||||
});
|
||||
}
|
||||
|
||||
Some(HoverPopover {
|
||||
Some(InfoPopover {
|
||||
project: project.clone(),
|
||||
anchor: range.start.clone(),
|
||||
symbol_range: range.clone(),
|
||||
contents: hover_result.contents,
|
||||
})
|
||||
});
|
||||
|
||||
if let Some(delay) = delay {
|
||||
delay.await;
|
||||
}
|
||||
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if hover_popover.is_some() {
|
||||
if let Some(hover_popover) = hover_popover.as_ref() {
|
||||
// Highlight the selected symbol using a background highlight
|
||||
if let Some(range) = this.hover_state.symbol_range.clone() {
|
||||
this.highlight_background::<HoverState>(
|
||||
vec![range],
|
||||
|theme| theme.editor.hover_popover.highlight,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
this.hover_state.popover = hover_popover;
|
||||
cx.notify();
|
||||
this.highlight_background::<HoverState>(
|
||||
vec![hover_popover.symbol_range.clone()],
|
||||
|theme| theme.editor.hover_popover.highlight,
|
||||
cx,
|
||||
);
|
||||
} else {
|
||||
if this.hover_state.visible() {
|
||||
// Popover was visible, but now is hidden. Dismiss it
|
||||
hide_hover(this, cx);
|
||||
} else {
|
||||
// Clear selected symbol range for future requests
|
||||
this.hover_state.symbol_range = None;
|
||||
}
|
||||
this.clear_background_highlights::<HoverState>(cx);
|
||||
}
|
||||
|
||||
this.hover_state.info_popover = hover_popover;
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
Ok::<_, anyhow::Error>(())
|
||||
|
@ -234,38 +249,70 @@ fn show_hover(
|
|||
.log_err()
|
||||
});
|
||||
|
||||
editor.hover_state.task = Some(task);
|
||||
editor.hover_state.info_task = Some(task);
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct HoverState {
|
||||
pub popover: Option<HoverPopover>,
|
||||
pub info_popover: Option<InfoPopover>,
|
||||
pub diagnostic_popover: Option<DiagnosticPopover>,
|
||||
pub triggered_from: Option<Anchor>,
|
||||
pub symbol_range: Option<Range<Anchor>>,
|
||||
pub task: Option<Task<Option<()>>>,
|
||||
pub info_task: Option<Task<Option<()>>>,
|
||||
}
|
||||
|
||||
impl HoverState {
|
||||
pub fn visible(&self) -> bool {
|
||||
self.popover.is_some()
|
||||
self.info_popover.is_some() || self.diagnostic_popover.is_some()
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
&self,
|
||||
snapshot: &EditorSnapshot,
|
||||
style: &EditorStyle,
|
||||
visible_rows: Range<u32>,
|
||||
cx: &mut RenderContext<Editor>,
|
||||
) -> Option<(DisplayPoint, Vec<ElementBox>)> {
|
||||
// If there is a diagnostic, position the popovers based on that.
|
||||
// Otherwise use the start of the hover range
|
||||
let anchor = self
|
||||
.diagnostic_popover
|
||||
.as_ref()
|
||||
.map(|diagnostic_popover| &diagnostic_popover.local_diagnostic.range.start)
|
||||
.or_else(|| {
|
||||
self.info_popover
|
||||
.as_ref()
|
||||
.map(|info_popover| &info_popover.symbol_range.start)
|
||||
})?;
|
||||
let point = anchor.to_display_point(&snapshot.display_snapshot);
|
||||
|
||||
// Don't render if the relevant point isn't on screen
|
||||
if !self.visible() || !visible_rows.contains(&point.row()) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut elements = Vec::new();
|
||||
|
||||
if let Some(diagnostic_popover) = self.diagnostic_popover.as_ref() {
|
||||
elements.push(diagnostic_popover.render(style, cx));
|
||||
}
|
||||
if let Some(info_popover) = self.info_popover.as_ref() {
|
||||
elements.push(info_popover.render(style, cx));
|
||||
}
|
||||
|
||||
Some((point, elements))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HoverPopover {
|
||||
pub struct InfoPopover {
|
||||
pub project: ModelHandle<Project>,
|
||||
pub anchor: Anchor,
|
||||
pub symbol_range: Range<Anchor>,
|
||||
pub contents: Vec<HoverBlock>,
|
||||
}
|
||||
|
||||
impl HoverPopover {
|
||||
pub fn render(
|
||||
&self,
|
||||
snapshot: &EditorSnapshot,
|
||||
style: EditorStyle,
|
||||
cx: &mut RenderContext<Editor>,
|
||||
) -> (DisplayPoint, ElementBox) {
|
||||
let element = MouseEventHandler::new::<HoverPopover, _, _>(0, cx, |_, cx| {
|
||||
impl InfoPopover {
|
||||
pub fn render(&self, style: &EditorStyle, cx: &mut RenderContext<Editor>) -> ElementBox {
|
||||
MouseEventHandler::new::<InfoPopover, _, _>(0, cx, |_, cx| {
|
||||
let mut flex = Flex::new(Axis::Vertical).scrollable::<HoverBlock, _>(1, None, cx);
|
||||
flex.extend(self.contents.iter().map(|content| {
|
||||
let project = self.project.read(cx);
|
||||
|
@ -309,10 +356,61 @@ impl HoverPopover {
|
|||
top: 5.,
|
||||
..Default::default()
|
||||
})
|
||||
.boxed();
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
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,19 +461,18 @@ mod tests {
|
|||
fn test()
|
||||
[println!]();"});
|
||||
let mut requests =
|
||||
cx.lsp
|
||||
.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _| async move {
|
||||
Ok(Some(lsp::Hover {
|
||||
contents: lsp::HoverContents::Markup(lsp::MarkupContent {
|
||||
kind: lsp::MarkupKind::Markdown,
|
||||
value: indoc! {"
|
||||
cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
|
||||
Ok(Some(lsp::Hover {
|
||||
contents: lsp::HoverContents::Markup(lsp::MarkupContent {
|
||||
kind: lsp::MarkupKind::Markdown,
|
||||
value: indoc! {"
|
||||
# Some basic docs
|
||||
Some test documentation"}
|
||||
.to_string(),
|
||||
}),
|
||||
range: Some(symbol_range),
|
||||
}))
|
||||
});
|
||||
.to_string(),
|
||||
}),
|
||||
range: Some(symbol_range),
|
||||
}))
|
||||
});
|
||||
cx.foreground()
|
||||
.advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
|
||||
requests.next().await;
|
||||
|
@ -382,7 +480,7 @@ mod tests {
|
|||
cx.editor(|editor, _| {
|
||||
assert!(editor.hover_state.visible());
|
||||
assert_eq!(
|
||||
editor.hover_state.popover.clone().unwrap().contents,
|
||||
editor.hover_state.info_popover.clone().unwrap().contents,
|
||||
vec![
|
||||
HoverBlock {
|
||||
text: "Some basic docs".to_string(),
|
||||
|
@ -400,6 +498,9 @@ mod tests {
|
|||
let hover_point = cx.display_point(indoc! {"
|
||||
fn te|st()
|
||||
println!();"});
|
||||
let mut request = cx
|
||||
.lsp
|
||||
.handle_request::<lsp::request::HoverRequest, _, _>(|_, _| async move { Ok(None) });
|
||||
cx.update_editor(|editor, cx| {
|
||||
hover_at(
|
||||
editor,
|
||||
|
@ -409,15 +510,24 @@ mod tests {
|
|||
cx,
|
||||
)
|
||||
});
|
||||
let mut request = cx
|
||||
.lsp
|
||||
.handle_request::<lsp::request::HoverRequest, _, _>(|_, _| async move { Ok(None) });
|
||||
cx.foreground()
|
||||
.advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
|
||||
request.next().await;
|
||||
cx.editor(|editor, _| {
|
||||
assert!(!editor.hover_state.visible());
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_keyboard_hover_info_popover(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
lsp::ServerCapabilities {
|
||||
hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
// Hover with keyboard has no delay
|
||||
cx.set_state(indoc! {"
|
||||
|
@ -427,26 +537,25 @@ mod tests {
|
|||
let symbol_range = cx.lsp_range(indoc! {"
|
||||
[fn] test()
|
||||
println!();"});
|
||||
cx.lsp
|
||||
.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _| async move {
|
||||
Ok(Some(lsp::Hover {
|
||||
contents: lsp::HoverContents::Markup(lsp::MarkupContent {
|
||||
kind: lsp::MarkupKind::Markdown,
|
||||
value: indoc! {"
|
||||
# Some other basic docs
|
||||
Some other test documentation"}
|
||||
.to_string(),
|
||||
}),
|
||||
range: Some(symbol_range),
|
||||
}))
|
||||
})
|
||||
.next()
|
||||
.await;
|
||||
cx.foreground().run_until_parked();
|
||||
cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
|
||||
Ok(Some(lsp::Hover {
|
||||
contents: lsp::HoverContents::Markup(lsp::MarkupContent {
|
||||
kind: lsp::MarkupKind::Markdown,
|
||||
value: indoc! {"
|
||||
# Some other basic docs
|
||||
Some other test documentation"}
|
||||
.to_string(),
|
||||
}),
|
||||
range: Some(symbol_range),
|
||||
}))
|
||||
})
|
||||
.next()
|
||||
.await;
|
||||
|
||||
cx.condition(|editor, _| editor.hover_state.visible()).await;
|
||||
cx.editor(|editor, _| {
|
||||
assert!(editor.hover_state.visible());
|
||||
assert_eq!(
|
||||
editor.hover_state.popover.clone().unwrap().contents,
|
||||
editor.hover_state.info_popover.clone().unwrap().contents,
|
||||
vec![
|
||||
HoverBlock {
|
||||
text: "Some other basic docs".to_string(),
|
||||
|
@ -460,4 +569,73 @@ mod tests {
|
|||
)
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
lsp::ServerCapabilities {
|
||||
hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
// Hover with just diagnostic, pops DiagnosticPopover immediately and then
|
||||
// info popover once request completes
|
||||
cx.set_state(indoc! {"
|
||||
fn te|st()
|
||||
println!();"});
|
||||
|
||||
// Send diagnostic to client
|
||||
let range = cx.text_anchor_range(indoc! {"
|
||||
fn [test]()
|
||||
println!();"});
|
||||
cx.update_buffer(|buffer, cx| {
|
||||
let snapshot = buffer.text_snapshot();
|
||||
let set = DiagnosticSet::from_sorted_entries(
|
||||
vec![DiagnosticEntry {
|
||||
range,
|
||||
diagnostic: Diagnostic {
|
||||
message: "A test diagnostic message.".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
}],
|
||||
&snapshot,
|
||||
);
|
||||
buffer.update_diagnostics(set, cx);
|
||||
});
|
||||
|
||||
// Hover pops diagnostic immediately
|
||||
cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
|
||||
cx.foreground().run_until_parked();
|
||||
|
||||
cx.editor(|Editor { hover_state, .. }, _| {
|
||||
assert!(hover_state.diagnostic_popover.is_some() && hover_state.info_popover.is_none())
|
||||
});
|
||||
|
||||
// Info Popover shows after request responded to
|
||||
let range = cx.lsp_range(indoc! {"
|
||||
fn [test]()
|
||||
println!();"});
|
||||
cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
|
||||
Ok(Some(lsp::Hover {
|
||||
contents: lsp::HoverContents::Markup(lsp::MarkupContent {
|
||||
kind: lsp::MarkupKind::Markdown,
|
||||
value: indoc! {"
|
||||
# Some other basic docs
|
||||
Some other test documentation"}
|
||||
.to_string(),
|
||||
}),
|
||||
range: Some(range),
|
||||
}))
|
||||
});
|
||||
cx.foreground()
|
||||
.advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
|
||||
|
||||
cx.foreground().run_until_parked();
|
||||
cx.editor(|Editor { hover_state, .. }, _| {
|
||||
hover_state.diagnostic_popover.is_some() && hover_state.info_task.is_some()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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"] }
|
||||
|
|
853
crates/terminal/src/connected_el.rs
Normal file
853
crates/terminal/src/connected_el.rs
Normal 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),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
176
crates/terminal/src/connected_view.rs
Normal file
176
crates/terminal/src/connected_view.rs
Normal 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()));
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
2
crates/terminal/src/mappings/mod.rs
Normal file
2
crates/terminal/src/mappings/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
pub mod colors;
|
||||
pub mod keys;
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
73
crates/terminal/src/modal_view.rs
Normal file
73
crates/terminal/src/modal_view.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
522
crates/terminal/src/model.rs
Normal file
522
crates/terminal/src/model.rs
Normal 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()))
|
||||
}
|
||||
}
|
|
@ -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 settings = cx.global::<Settings>();
|
||||
let shell = settings.terminal_overrides.shell.clone();
|
||||
let envs = settings.terminal_overrides.env.clone(); //Should be short and cheap.
|
||||
|
||||
let content = match TerminalBuilder::new(working_directory.clone(), shell, envs, size_info)
|
||||
{
|
||||
Ok(terminal) => {
|
||||
let terminal = cx.add_model(|cx| terminal.subscribe(cx));
|
||||
let view = cx.add_view(|cx| ConnectedView::from_terminal(terminal, modal, cx));
|
||||
cx.subscribe(&view, |_this, _content, event, cx| cx.emit(event.clone()))
|
||||
.detach();
|
||||
TerminalContent::Connected(view)
|
||||
}
|
||||
Err(error) => {
|
||||
let view = cx.add_view(|_| ErrorView {
|
||||
error: error.downcast::<TerminalError>().unwrap(),
|
||||
});
|
||||
TerminalContent::Error(view)
|
||||
}
|
||||
};
|
||||
cx.focus(content.handle());
|
||||
|
||||
let connection = cx
|
||||
.add_model(|cx| TerminalConnection::new(working_directory, shell, envs, size_info, cx));
|
||||
|
||||
Terminal::from_connection(connection, modal, cx)
|
||||
TerminalView {
|
||||
modal,
|
||||
content,
|
||||
associated_directory: working_directory,
|
||||
}
|
||||
}
|
||||
|
||||
fn from_connection(
|
||||
connection: ModelHandle<TerminalConnection>,
|
||||
fn from_terminal(
|
||||
terminal: ModelHandle<Terminal>,
|
||||
modal: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Terminal {
|
||||
cx.observe(&connection, |_, _, cx| cx.notify()).detach();
|
||||
cx.subscribe(&connection, |this, _, event, cx| match event {
|
||||
Event::Wakeup => {
|
||||
if cx.is_self_focused() {
|
||||
cx.notify()
|
||||
} else {
|
||||
this.has_new_content = true;
|
||||
cx.emit(Event::TitleChanged);
|
||||
}
|
||||
}
|
||||
Event::Bell => {
|
||||
this.has_bell = true;
|
||||
cx.emit(Event::TitleChanged);
|
||||
}
|
||||
_ => cx.emit(*event),
|
||||
})
|
||||
.detach();
|
||||
|
||||
Terminal {
|
||||
connection,
|
||||
has_new_content: true,
|
||||
has_bell: false,
|
||||
) -> Self {
|
||||
let connected_view = cx.add_view(|cx| ConnectedView::from_terminal(terminal, modal, cx));
|
||||
TerminalView {
|
||||
modal,
|
||||
content: TerminalContent::Connected(connected_view),
|
||||
associated_directory: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn input(&mut self, text: &str, cx: &mut ViewContext<Self>) {
|
||||
self.connection.update(cx, |connection, _| {
|
||||
//TODO: This is probably not encoding UTF8 correctly (see alacritty/src/input.rs:L825-837)
|
||||
connection.write_to_pty(text.to_string());
|
||||
});
|
||||
|
||||
if self.has_bell {
|
||||
self.has_bell = false;
|
||||
cx.emit(Event::TitleChanged);
|
||||
}
|
||||
}
|
||||
|
||||
fn clear(&mut self, _: &Clear, cx: &mut ViewContext<Self>) {
|
||||
self.connection
|
||||
.update(cx, |connection, _| connection.clear());
|
||||
}
|
||||
|
||||
///Create a new Terminal in the current working directory or the user's home directory
|
||||
fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
|
||||
let wd = get_wd_for_workspace(workspace, cx);
|
||||
workspace.add_item(Box::new(cx.add_view(|cx| Terminal::new(wd, false, cx))), cx);
|
||||
}
|
||||
|
||||
///Attempt to paste the clipboard into the terminal
|
||||
fn copy(&mut self, _: &Copy, cx: &mut ViewContext<Self>) {
|
||||
let term = self.connection.read(cx).term.lock();
|
||||
let copy_text = term.selection_to_string();
|
||||
match copy_text {
|
||||
Some(s) => cx.write_to_clipboard(ClipboardItem::new(s)),
|
||||
None => (),
|
||||
}
|
||||
}
|
||||
|
||||
///Attempt to paste the clipboard into the terminal
|
||||
fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
|
||||
if let Some(item) = cx.read_from_clipboard() {
|
||||
self.connection.update(cx, |connection, _| {
|
||||
connection.paste(item.text());
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
///Synthesize the keyboard event corresponding to 'up'
|
||||
fn up(&mut self, _: &Up, cx: &mut ViewContext<Self>) {
|
||||
self.connection.update(cx, |connection, _| {
|
||||
connection.try_keystroke(&Keystroke::parse("up").unwrap());
|
||||
});
|
||||
}
|
||||
|
||||
///Synthesize the keyboard event corresponding to 'down'
|
||||
fn down(&mut self, _: &Down, cx: &mut ViewContext<Self>) {
|
||||
self.connection.update(cx, |connection, _| {
|
||||
connection.try_keystroke(&Keystroke::parse("down").unwrap());
|
||||
});
|
||||
}
|
||||
|
||||
///Synthesize the keyboard event corresponding to 'ctrl-c'
|
||||
fn ctrl_c(&mut self, _: &CtrlC, cx: &mut ViewContext<Self>) {
|
||||
self.connection.update(cx, |connection, _| {
|
||||
connection.try_keystroke(&Keystroke::parse("ctrl-c").unwrap());
|
||||
});
|
||||
}
|
||||
|
||||
///Synthesize the keyboard event corresponding to 'escape'
|
||||
fn escape(&mut self, _: &Escape, cx: &mut ViewContext<Self>) {
|
||||
self.connection.update(cx, |connection, _| {
|
||||
connection.try_keystroke(&Keystroke::parse("escape").unwrap());
|
||||
});
|
||||
}
|
||||
|
||||
///Synthesize the keyboard event corresponding to 'enter'
|
||||
fn enter(&mut self, _: &Enter, cx: &mut ViewContext<Self>) {
|
||||
self.connection.update(cx, |connection, _| {
|
||||
connection.try_keystroke(&Keystroke::parse("enter").unwrap());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl View for Terminal {
|
||||
impl View for TerminalView {
|
||||
fn ui_name() -> &'static str {
|
||||
"Terminal"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox {
|
||||
let element = {
|
||||
let connection_handle = self.connection.clone().downgrade();
|
||||
let view_id = cx.view_id();
|
||||
TerminalEl::new(view_id, connection_handle, self.modal).contained()
|
||||
let child_view = match &self.content {
|
||||
TerminalContent::Connected(connected) => ChildView::new(connected),
|
||||
TerminalContent::Error(error) => ChildView::new(error),
|
||||
};
|
||||
|
||||
if self.modal {
|
||||
let settings = cx.global::<Settings>();
|
||||
let container_style = settings.theme.terminal.modal_container;
|
||||
element.with_style(container_style).boxed()
|
||||
child_view.contained().with_style(container_style).boxed()
|
||||
} else {
|
||||
element.boxed()
|
||||
child_view.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(Event::Activate);
|
||||
self.has_new_content = false;
|
||||
cx.defer(|view, cx| {
|
||||
cx.focus(view.content.handle());
|
||||
});
|
||||
}
|
||||
|
||||
fn keymap_context(&self, _: &gpui::AppContext) -> gpui::keymap::Context {
|
||||
|
@ -261,67 +172,83 @@ impl View for Terminal {
|
|||
}
|
||||
context
|
||||
}
|
||||
}
|
||||
|
||||
fn selected_text_range(&self, _: &AppContext) -> Option<std::ops::Range<usize>> {
|
||||
Some(0..0)
|
||||
impl View for ErrorView {
|
||||
fn ui_name() -> &'static str {
|
||||
"Terminal Error"
|
||||
}
|
||||
|
||||
fn replace_text_in_range(
|
||||
&mut self,
|
||||
_: Option<std::ops::Range<usize>>,
|
||||
text: &str,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.input(text, cx);
|
||||
fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox {
|
||||
let settings = cx.global::<Settings>();
|
||||
let style = TerminalEl::make_text_style(cx.font_cache(), settings);
|
||||
|
||||
//TODO:
|
||||
//We want markdown style highlighting so we can format the program and working directory with ``
|
||||
//We want a max-width of 75% with word-wrap
|
||||
//We want to be able to select the text
|
||||
//Want to be able to scroll if the error message is massive somehow (resiliency)
|
||||
|
||||
let program_text = {
|
||||
match self.error.shell_to_string() {
|
||||
Some(shell_txt) => format!("Shell Program: `{}`", shell_txt),
|
||||
None => "No program specified".to_string(),
|
||||
}
|
||||
};
|
||||
|
||||
let directory_text = {
|
||||
match self.error.directory.as_ref() {
|
||||
Some(path) => format!("Working directory: `{}`", path.to_string_lossy()),
|
||||
None => "No working directory specified".to_string(),
|
||||
}
|
||||
};
|
||||
|
||||
let error_text = self.error.source.to_string();
|
||||
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Text::new("Failed to open the terminal.".to_string(), style.clone())
|
||||
.contained()
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(Text::new(program_text, style.clone()).contained().boxed())
|
||||
.with_child(Text::new(directory_text, style.clone()).contained().boxed())
|
||||
.with_child(Text::new(error_text, style.clone()).contained().boxed())
|
||||
.aligned()
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
impl Item for Terminal {
|
||||
impl Item for TerminalView {
|
||||
fn tab_content(
|
||||
&self,
|
||||
_detail: Option<usize>,
|
||||
tab_theme: &theme::Tab,
|
||||
cx: &gpui::AppContext,
|
||||
) -> ElementBox {
|
||||
let settings = cx.global::<Settings>();
|
||||
let search_theme = &settings.theme.search; //TODO properly integrate themes
|
||||
|
||||
let mut flex = Flex::row();
|
||||
|
||||
if self.has_bell {
|
||||
flex.add_child(
|
||||
Svg::new("icons/bolt_12.svg") //TODO: Swap out for a better icon, or at least resize this
|
||||
.with_color(tab_theme.label.text.color)
|
||||
.constrained()
|
||||
.with_width(search_theme.tab_icon_width)
|
||||
.aligned()
|
||||
.boxed(),
|
||||
);
|
||||
let title = match &self.content {
|
||||
TerminalContent::Connected(connected) => {
|
||||
connected.read(cx).handle().read(cx).title.clone()
|
||||
}
|
||||
TerminalContent::Error(_) => "Terminal".to_string(),
|
||||
};
|
||||
|
||||
flex.with_child(
|
||||
Label::new(
|
||||
self.connection.read(cx).title.clone(),
|
||||
tab_theme.label.clone(),
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Label::new(title, tab_theme.label.clone())
|
||||
.aligned()
|
||||
.contained()
|
||||
.boxed(),
|
||||
)
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_margin_left(if self.has_bell {
|
||||
search_theme.tab_icon_spacing
|
||||
} else {
|
||||
0.
|
||||
})
|
||||
.boxed(),
|
||||
)
|
||||
.boxed()
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self> {
|
||||
//From what I can tell, there's no way to tell the current working
|
||||
//Directory of the terminal from outside the terminal. There might be
|
||||
//Directory of the terminal from outside the shell. There might be
|
||||
//solutions to this, but they are non-trivial and require more IPC
|
||||
Some(Terminal::new(
|
||||
self.connection.read(cx).associated_directory.clone(),
|
||||
Some(TerminalView::new(
|
||||
self.associated_directory.clone(),
|
||||
false,
|
||||
cx,
|
||||
))
|
||||
|
@ -370,8 +297,20 @@ impl Item for Terminal {
|
|||
gpui::Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn is_dirty(&self, _: &gpui::AppContext) -> bool {
|
||||
self.has_new_content
|
||||
fn is_dirty(&self, cx: &gpui::AppContext) -> bool {
|
||||
if let TerminalContent::Connected(connected) = &self.content {
|
||||
connected.read(cx).has_new_content()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn has_conflict(&self, cx: &AppContext) -> bool {
|
||||
if let TerminalContent::Connected(connected) = &self.content {
|
||||
connected.read(cx).has_bell()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn should_update_tab_on_event(event: &Self::Event) -> bool {
|
||||
|
@ -388,7 +327,7 @@ impl Item for Terminal {
|
|||
}
|
||||
|
||||
///Get's the working directory for the given workspace, respecting the user's settings.
|
||||
fn get_wd_for_workspace(workspace: &Workspace, cx: &AppContext) -> Option<PathBuf> {
|
||||
fn get_working_directory(workspace: &Workspace, cx: &AppContext) -> Option<PathBuf> {
|
||||
let wd_setting = cx
|
||||
.global::<Settings>()
|
||||
.terminal_overrides
|
||||
|
@ -399,10 +338,12 @@ fn get_wd_for_workspace(workspace: &Workspace, cx: &AppContext) -> Option<PathBu
|
|||
WorkingDirectory::CurrentProjectDirectory => current_project_directory(workspace, cx),
|
||||
WorkingDirectory::FirstProjectDirectory => first_project_directory(workspace, cx),
|
||||
WorkingDirectory::AlwaysHome => None,
|
||||
WorkingDirectory::Always { directory } => shellexpand::full(&directory)
|
||||
.ok()
|
||||
.map(|dir| Path::new(&dir.to_string()).to_path_buf())
|
||||
.filter(|dir| dir.is_dir()),
|
||||
WorkingDirectory::Always { directory } => {
|
||||
shellexpand::full(&directory) //TODO handle this better
|
||||
.ok()
|
||||
.map(|dir| Path::new(&dir.to_string()).to_path_buf())
|
||||
.filter(|dir| dir.is_dir())
|
||||
}
|
||||
};
|
||||
res.or_else(|| home_dir())
|
||||
}
|
||||
|
@ -447,7 +388,6 @@ mod tests {
|
|||
use gpui::TestAppContext;
|
||||
|
||||
use std::path::Path;
|
||||
use workspace::AppState;
|
||||
|
||||
mod terminal_test_context;
|
||||
|
||||
|
@ -455,7 +395,7 @@ mod tests {
|
|||
//and produce noticable output?
|
||||
#[gpui::test(retries = 5)]
|
||||
async fn test_terminal(cx: &mut TestAppContext) {
|
||||
let mut cx = TerminalTestContext::new(cx);
|
||||
let mut cx = TerminalTestContext::new(cx, true);
|
||||
|
||||
cx.execute_and_wait("expr 3 + 4", |content, _cx| content.contains("7"))
|
||||
.await;
|
||||
|
@ -467,12 +407,10 @@ mod tests {
|
|||
#[gpui::test]
|
||||
async fn no_worktree(cx: &mut TestAppContext) {
|
||||
//Setup variables
|
||||
let params = cx.update(AppState::test);
|
||||
let project = Project::test(params.fs.clone(), [], cx).await;
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
|
||||
|
||||
let mut cx = TerminalTestContext::new(cx, true);
|
||||
let (project, workspace) = cx.blank_workspace().await;
|
||||
//Test
|
||||
cx.read(|cx| {
|
||||
cx.cx.read(|cx| {
|
||||
let workspace = workspace.read(cx);
|
||||
let active_entry = project.read(cx).active_entry();
|
||||
|
||||
|
@ -491,28 +429,12 @@ mod tests {
|
|||
#[gpui::test]
|
||||
async fn no_active_entry_worktree_is_file(cx: &mut TestAppContext) {
|
||||
//Setup variables
|
||||
let params = cx.update(AppState::test);
|
||||
let project = Project::test(params.fs.clone(), [], cx).await;
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
|
||||
let (wt, _) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.find_or_create_local_worktree("/root.txt", true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.update(|cx| {
|
||||
wt.update(cx, |wt, cx| {
|
||||
wt.as_local()
|
||||
.unwrap()
|
||||
.create_entry(Path::new(""), false, cx)
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
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();
|
||||
|
||||
|
|
|
@ -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),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
lines
|
||||
.into_iter()
|
||||
.map(|(_, line)| line.map(|i| i.c).collect::<String>())
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n")
|
||||
///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")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>) {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -2,22 +2,48 @@ import Theme from "../themes/common/theme";
|
|||
import { backgroundColor, border, popoverShadow, text } from "./components";
|
||||
|
||||
export default function HoverPopover(theme: Theme) {
|
||||
let baseContainer = {
|
||||
background: backgroundColor(theme, "on500"),
|
||||
cornerRadius: 8,
|
||||
padding: {
|
||||
left: 8,
|
||||
right: 8,
|
||||
top: 4,
|
||||
bottom: 4
|
||||
},
|
||||
shadow: popoverShadow(theme),
|
||||
border: border(theme, "secondary"),
|
||||
margin: {
|
||||
left: -8,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
container: {
|
||||
background: backgroundColor(theme, "on500"),
|
||||
cornerRadius: 8,
|
||||
padding: {
|
||||
left: 8,
|
||||
right: 8,
|
||||
top: 4,
|
||||
bottom: 4,
|
||||
container: baseContainer,
|
||||
infoContainer: {
|
||||
...baseContainer,
|
||||
background: backgroundColor(theme, "on500Info"),
|
||||
border: {
|
||||
color: theme.ramps.blue(0).hex(),
|
||||
width: 1,
|
||||
},
|
||||
shadow: popoverShadow(theme),
|
||||
border: border(theme, "primary"),
|
||||
margin: {
|
||||
left: -8,
|
||||
},
|
||||
warningContainer: {
|
||||
...baseContainer,
|
||||
background: backgroundColor(theme, "on500Warning"),
|
||||
border: {
|
||||
color: theme.ramps.yellow(0).hex(),
|
||||
width: 1,
|
||||
},
|
||||
},
|
||||
errorContainer: {
|
||||
...baseContainer,
|
||||
background: backgroundColor(theme, "on500Error"),
|
||||
border: {
|
||||
color: theme.ramps.red(0).hex(),
|
||||
width: 1,
|
||||
}
|
||||
},
|
||||
block_style: {
|
||||
padding: { top: 4 },
|
||||
},
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue