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]]
|
[[package]]
|
||||||
name = "alacritty_config_derive"
|
name = "alacritty_config_derive"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "git+https://github.com/zed-industries/alacritty?rev=e9b864860ec79cc1b70042aafce100cdd6985a0a#e9b864860ec79cc1b70042aafce100cdd6985a0a"
|
||||||
checksum = "77044c45bdb871e501b5789ad16293ecb619e5733b60f4bb01d1cb31c463c336"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -72,14 +71,13 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "alacritty_terminal"
|
name = "alacritty_terminal"
|
||||||
version = "0.16.1"
|
version = "0.17.0-dev"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "git+https://github.com/zed-industries/alacritty?rev=e9b864860ec79cc1b70042aafce100cdd6985a0a#e9b864860ec79cc1b70042aafce100cdd6985a0a"
|
||||||
checksum = "02fb5d4af84e39f9754d039ff6de2233c8996dbae0af74910156e559e5766e2f"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"alacritty_config_derive",
|
"alacritty_config_derive",
|
||||||
"base64 0.13.0",
|
"base64 0.13.0",
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"dirs 3.0.2",
|
"dirs 4.0.0",
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"mio 0.6.23",
|
"mio 0.6.23",
|
||||||
|
@ -5355,12 +5353,14 @@ name = "terminal"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"alacritty_terminal",
|
"alacritty_terminal",
|
||||||
|
"anyhow",
|
||||||
"client",
|
"client",
|
||||||
"dirs 4.0.0",
|
"dirs 4.0.0",
|
||||||
"editor",
|
"editor",
|
||||||
"futures",
|
"futures",
|
||||||
"gpui",
|
"gpui",
|
||||||
"itertools",
|
"itertools",
|
||||||
|
"libc",
|
||||||
"mio-extras",
|
"mio-extras",
|
||||||
"ordered-float",
|
"ordered-float",
|
||||||
"project",
|
"project",
|
||||||
|
@ -5368,6 +5368,7 @@ dependencies = [
|
||||||
"shellexpand",
|
"shellexpand",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"theme",
|
"theme",
|
||||||
|
"thiserror",
|
||||||
"util",
|
"util",
|
||||||
"workspace",
|
"workspace",
|
||||||
]
|
]
|
||||||
|
|
|
@ -188,7 +188,7 @@
|
||||||
"alt-down": "editor::SelectSmallerSyntaxNode",
|
"alt-down": "editor::SelectSmallerSyntaxNode",
|
||||||
"cmd-u": "editor::UndoSelection",
|
"cmd-u": "editor::UndoSelection",
|
||||||
"cmd-shift-u": "editor::RedoSelection",
|
"cmd-shift-u": "editor::RedoSelection",
|
||||||
"f8": "editor::GoToNextDiagnostic",
|
"f8": "editor::GoToDiagnostic",
|
||||||
"shift-f8": "editor::GoToPrevDiagnostic",
|
"shift-f8": "editor::GoToPrevDiagnostic",
|
||||||
"f2": "editor::Rename",
|
"f2": "editor::Rename",
|
||||||
"f12": "editor::GoToDefinition",
|
"f12": "editor::GoToDefinition",
|
||||||
|
|
|
@ -102,10 +102,10 @@
|
||||||
//
|
//
|
||||||
"working_directory": "current_project_directory",
|
"working_directory": "current_project_directory",
|
||||||
//Any key-value pairs added to this list will be added to the terminal's
|
//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
|
//enviroment. Use `:` to seperate multiple values.
|
||||||
"env": [
|
"env": {
|
||||||
//["KEY", "value1:value2"]
|
//"KEY": "value1:value2"
|
||||||
]
|
}
|
||||||
//Set the terminal's font size. If this option is not included,
|
//Set the terminal's font size. If this option is not included,
|
||||||
//the terminal will default to matching the buffer's font size.
|
//the terminal will default to matching the buffer's font size.
|
||||||
//"font_size": "15"
|
//"font_size": "15"
|
||||||
|
|
|
@ -362,12 +362,7 @@ mod tests {
|
||||||
});
|
});
|
||||||
|
|
||||||
let palette = workspace.read_with(cx, |workspace, _| {
|
let palette = workspace.read_with(cx, |workspace, _| {
|
||||||
workspace
|
workspace.modal::<CommandPalette>().unwrap()
|
||||||
.modal()
|
|
||||||
.unwrap()
|
|
||||||
.clone()
|
|
||||||
.downcast::<CommandPalette>()
|
|
||||||
.unwrap()
|
|
||||||
});
|
});
|
||||||
|
|
||||||
palette
|
palette
|
||||||
|
@ -398,12 +393,7 @@ mod tests {
|
||||||
|
|
||||||
// Assert editor command not present
|
// Assert editor command not present
|
||||||
let palette = workspace.read_with(cx, |workspace, _| {
|
let palette = workspace.read_with(cx, |workspace, _| {
|
||||||
workspace
|
workspace.modal::<CommandPalette>().unwrap()
|
||||||
.modal()
|
|
||||||
.unwrap()
|
|
||||||
.clone()
|
|
||||||
.downcast::<CommandPalette>()
|
|
||||||
.unwrap()
|
|
||||||
});
|
});
|
||||||
|
|
||||||
palette
|
palette
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use collections::HashSet;
|
use collections::HashSet;
|
||||||
use editor::{Editor, GoToNextDiagnostic};
|
use editor::{Editor, GoToDiagnostic};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
elements::*, platform::CursorStyle, serde_json, Entity, ModelHandle, MouseButton,
|
elements::*, platform::CursorStyle, serde_json, Entity, ModelHandle, MouseButton,
|
||||||
MutableAppContext, RenderContext, Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
|
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)) {
|
if let Some(editor) = self.active_editor.as_ref().and_then(|e| e.upgrade(cx)) {
|
||||||
editor.update(cx, |editor, 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)
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
.on_click(MouseButton::Left, |_, cx| {
|
.on_click(MouseButton::Left, |_, cx| {
|
||||||
cx.dispatch_action(GoToNextDiagnostic)
|
cx.dispatch_action(GoToDiagnostic)
|
||||||
})
|
})
|
||||||
.boxed(),
|
.boxed(),
|
||||||
);
|
);
|
||||||
|
|
|
@ -82,9 +82,6 @@ pub struct SelectNext {
|
||||||
pub replace_newest: bool,
|
pub replace_newest: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq)]
|
|
||||||
pub struct GoToDiagnostic(pub Direction);
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
pub struct Scroll(pub Vector2F);
|
pub struct Scroll(pub Vector2F);
|
||||||
|
|
||||||
|
@ -135,7 +132,7 @@ actions!(
|
||||||
Backspace,
|
Backspace,
|
||||||
Delete,
|
Delete,
|
||||||
Newline,
|
Newline,
|
||||||
GoToNextDiagnostic,
|
GoToDiagnostic,
|
||||||
GoToPrevDiagnostic,
|
GoToPrevDiagnostic,
|
||||||
Indent,
|
Indent,
|
||||||
Outdent,
|
Outdent,
|
||||||
|
@ -297,7 +294,7 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||||
cx.add_action(Editor::move_to_enclosing_bracket);
|
cx.add_action(Editor::move_to_enclosing_bracket);
|
||||||
cx.add_action(Editor::undo_selection);
|
cx.add_action(Editor::undo_selection);
|
||||||
cx.add_action(Editor::redo_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_prev_diagnostic);
|
||||||
cx.add_action(Editor::go_to_definition);
|
cx.add_action(Editor::go_to_definition);
|
||||||
cx.add_action(Editor::page_up);
|
cx.add_action(Editor::page_up);
|
||||||
|
@ -4567,17 +4564,32 @@ impl Editor {
|
||||||
self.selection_history.mode = SelectionHistoryMode::Normal;
|
self.selection_history.mode = SelectionHistoryMode::Normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn go_to_next_diagnostic(&mut self, _: &GoToNextDiagnostic, cx: &mut ViewContext<Self>) {
|
fn go_to_diagnostic(&mut self, _: &GoToDiagnostic, cx: &mut ViewContext<Self>) {
|
||||||
self.go_to_diagnostic(Direction::Next, cx)
|
self.go_to_diagnostic_impl(Direction::Next, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn go_to_prev_diagnostic(&mut self, _: &GoToPrevDiagnostic, cx: &mut ViewContext<Self>) {
|
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 buffer = self.buffer.read(cx).snapshot(cx);
|
||||||
let selection = self.selections.newest::<usize>(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| {
|
let mut active_primary_range = self.active_diagnostics.as_ref().map(|active_diagnostics| {
|
||||||
active_diagnostics
|
active_diagnostics
|
||||||
.primary_range
|
.primary_range
|
||||||
|
|
|
@ -41,6 +41,10 @@ use std::{
|
||||||
ops::Range,
|
ops::Range,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MIN_POPOVER_CHARACTER_WIDTH: f32 = 20.;
|
||||||
|
const MIN_POPOVER_LINE_HEIGHT: f32 = 4.;
|
||||||
|
const HOVER_POPOVER_GAP: f32 = 10.;
|
||||||
|
|
||||||
struct SelectionLayout {
|
struct SelectionLayout {
|
||||||
head: DisplayPoint,
|
head: DisplayPoint,
|
||||||
range: Range<DisplayPoint>,
|
range: Range<DisplayPoint>,
|
||||||
|
@ -268,8 +272,9 @@ impl EditorElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
if paint
|
if paint
|
||||||
.hover_bounds
|
.hover_popover_bounds
|
||||||
.map_or(false, |hover_bounds| hover_bounds.contains_point(position))
|
.iter()
|
||||||
|
.any(|hover_bounds| hover_bounds.contains_point(position))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -585,35 +590,78 @@ impl EditorElement {
|
||||||
cx.scene.pop_stacking_context();
|
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);
|
cx.scene.push_stacking_context(None);
|
||||||
|
|
||||||
// This is safe because we check on layout whether the required row is available
|
// 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 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 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 y = position.row() as f32 * layout.line_height - scroll_top;
|
||||||
let mut popover_origin = content_origin + vec2f(x, y);
|
let hovered_point = content_origin + vec2f(x, y);
|
||||||
|
|
||||||
if popover_origin.y() < 0.0 {
|
paint.hover_popover_bounds.clear();
|
||||||
popover_origin.set_y(popover_origin.y() + layout.line_height + size.y());
|
|
||||||
|
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();
|
cx.scene.pop_stacking_context();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1147,6 +1195,8 @@ impl Element for EditorElement {
|
||||||
});
|
});
|
||||||
|
|
||||||
let scroll_position = snapshot.scroll_position();
|
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 start_row = scroll_position.y() as u32;
|
||||||
let scroll_top = scroll_position.y() * line_height;
|
let scroll_top = scroll_position.y() * line_height;
|
||||||
|
|
||||||
|
@ -1320,16 +1370,8 @@ impl Element for EditorElement {
|
||||||
.map(|indicator| (newest_selection_head.row(), indicator));
|
.map(|indicator| (newest_selection_head.row(), indicator));
|
||||||
}
|
}
|
||||||
|
|
||||||
hover = view.hover_state.popover.clone().and_then(|hover| {
|
let visible_rows = start_row..start_row + line_layouts.len() as u32;
|
||||||
let (point, rendered) = hover.render(&snapshot, style.clone(), cx);
|
hover = view.hover_state.render(&snapshot, &style, visible_rows, 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
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some((_, context_menu)) = context_menu.as_mut() {
|
if let Some((_, context_menu)) = context_menu.as_mut() {
|
||||||
|
@ -1352,21 +1394,23 @@ impl Element for EditorElement {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some((_, hover)) = hover.as_mut() {
|
if let Some((_, hover_popovers)) = hover.as_mut() {
|
||||||
hover.layout(
|
for hover_popover in hover_popovers.iter_mut() {
|
||||||
SizeConstraint {
|
hover_popover.layout(
|
||||||
min: Vector2F::zero(),
|
SizeConstraint {
|
||||||
max: vec2f(
|
min: Vector2F::zero(),
|
||||||
(120. * em_width) // Default size
|
max: vec2f(
|
||||||
.min(size.x() / 2.) // Shrink to half of the editor width
|
(120. * em_width) // Default size
|
||||||
.max(20. * em_width), // Apply minimum width of 20 characters
|
.min(size.x() / 2.) // Shrink to half of the editor width
|
||||||
(16. * line_height) // Default size
|
.max(MIN_POPOVER_CHARACTER_WIDTH * em_width), // Apply minimum width of 20 characters
|
||||||
.min(size.y() / 2.) // Shrink to half of the editor height
|
(16. * line_height) // Default size
|
||||||
.max(4. * line_height), // Apply minimum height of 4 lines
|
.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,
|
},
|
||||||
);
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
(
|
(
|
||||||
|
@ -1391,7 +1435,7 @@ impl Element for EditorElement {
|
||||||
selections,
|
selections,
|
||||||
context_menu,
|
context_menu,
|
||||||
code_actions_indicator,
|
code_actions_indicator,
|
||||||
hover,
|
hover_popovers: hover,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1416,7 +1460,7 @@ impl Element for EditorElement {
|
||||||
gutter_bounds,
|
gutter_bounds,
|
||||||
text_bounds,
|
text_bounds,
|
||||||
context_menu_bounds: None,
|
context_menu_bounds: None,
|
||||||
hover_bounds: None,
|
hover_popover_bounds: Default::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.paint_background(gutter_bounds, text_bounds, layout, cx);
|
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 let Some((_, popover_elements)) = &mut layout.hover_popovers {
|
||||||
if hover.dispatch_event(event, cx) {
|
for popover_element in popover_elements.iter_mut() {
|
||||||
return true;
|
if popover_element.dispatch_event(event, cx) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1590,7 +1636,7 @@ pub struct LayoutState {
|
||||||
selections: Vec<(ReplicaId, Vec<SelectionLayout>)>,
|
selections: Vec<(ReplicaId, Vec<SelectionLayout>)>,
|
||||||
context_menu: Option<(DisplayPoint, ElementBox)>,
|
context_menu: Option<(DisplayPoint, ElementBox)>,
|
||||||
code_actions_indicator: Option<(u32, ElementBox)>,
|
code_actions_indicator: Option<(u32, ElementBox)>,
|
||||||
hover: Option<(DisplayPoint, ElementBox)>,
|
hover_popovers: Option<(DisplayPoint, Vec<ElementBox>)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct BlockLayout {
|
struct BlockLayout {
|
||||||
|
@ -1635,7 +1681,7 @@ pub struct PaintState {
|
||||||
gutter_bounds: RectF,
|
gutter_bounds: RectF,
|
||||||
text_bounds: RectF,
|
text_bounds: RectF,
|
||||||
context_menu_bounds: Option<RectF>,
|
context_menu_bounds: Option<RectF>,
|
||||||
hover_bounds: Option<RectF>,
|
hover_popover_bounds: Vec<RectF>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PaintState {
|
impl PaintState {
|
||||||
|
|
|
@ -3,9 +3,10 @@ use gpui::{
|
||||||
elements::{Flex, MouseEventHandler, Padding, Text},
|
elements::{Flex, MouseEventHandler, Padding, Text},
|
||||||
impl_internal_actions,
|
impl_internal_actions,
|
||||||
platform::CursorStyle,
|
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 project::{HoverBlock, Project};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use std::{ops::Range, time::Duration};
|
use std::{ops::Range, time::Duration};
|
||||||
|
@ -13,7 +14,7 @@ use util::TryFutureExt;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
display_map::ToDisplayPoint, Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSnapshot,
|
display_map::ToDisplayPoint, Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSnapshot,
|
||||||
EditorStyle,
|
EditorStyle, GoToDiagnostic, RangeToAnchorExt,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const HOVER_DELAY_MILLIS: u64 = 350;
|
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
|
/// Triggered by the `Hover` action when the cursor is not over a symbol or when the
|
||||||
/// selections changed.
|
/// selections changed.
|
||||||
pub fn hide_hover(editor: &mut Editor, cx: &mut ViewContext<Editor>) -> bool {
|
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
|
editor.hover_state.info_task = None;
|
||||||
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.triggered_from = None;
|
editor.hover_state.triggered_from = None;
|
||||||
editor.hover_state.symbol_range = None;
|
|
||||||
|
|
||||||
editor.clear_background_highlights::<HoverState>(cx);
|
editor.clear_background_highlights::<HoverState>(cx);
|
||||||
|
|
||||||
|
@ -114,8 +109,8 @@ fn show_hover(
|
||||||
};
|
};
|
||||||
|
|
||||||
if !ignore_timeout {
|
if !ignore_timeout {
|
||||||
if let Some(range) = &editor.hover_state.symbol_range {
|
if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover {
|
||||||
if range
|
if symbol_range
|
||||||
.to_offset(&snapshot.buffer_snapshot)
|
.to_offset(&snapshot.buffer_snapshot)
|
||||||
.contains(&multibuffer_offset)
|
.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
|
// Construct new hover popover from hover request
|
||||||
let hover_popover = hover_request.await.ok().flatten().and_then(|hover_result| {
|
let hover_popover = hover_request.await.ok().flatten().and_then(|hover_result| {
|
||||||
if hover_result.contents.is_empty() {
|
if hover_result.contents.is_empty() {
|
||||||
|
@ -188,45 +220,28 @@ fn show_hover(
|
||||||
anchor.clone()..anchor.clone()
|
anchor.clone()..anchor.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(this) = this.upgrade(&cx) {
|
Some(InfoPopover {
|
||||||
this.update(&mut cx, |this, _| {
|
|
||||||
this.hover_state.symbol_range = Some(range.clone());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(HoverPopover {
|
|
||||||
project: project.clone(),
|
project: project.clone(),
|
||||||
anchor: range.start.clone(),
|
symbol_range: range.clone(),
|
||||||
contents: hover_result.contents,
|
contents: hover_result.contents,
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(delay) = delay {
|
|
||||||
delay.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(this) = this.upgrade(&cx) {
|
if let Some(this) = this.upgrade(&cx) {
|
||||||
this.update(&mut cx, |this, 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
|
// Highlight the selected symbol using a background highlight
|
||||||
if let Some(range) = this.hover_state.symbol_range.clone() {
|
this.highlight_background::<HoverState>(
|
||||||
this.highlight_background::<HoverState>(
|
vec![hover_popover.symbol_range.clone()],
|
||||||
vec![range],
|
|theme| theme.editor.hover_popover.highlight,
|
||||||
|theme| theme.editor.hover_popover.highlight,
|
cx,
|
||||||
cx,
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
this.hover_state.popover = hover_popover;
|
|
||||||
cx.notify();
|
|
||||||
} else {
|
} else {
|
||||||
if this.hover_state.visible() {
|
this.clear_background_highlights::<HoverState>(cx);
|
||||||
// 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.hover_state.info_popover = hover_popover;
|
||||||
|
cx.notify();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Ok::<_, anyhow::Error>(())
|
Ok::<_, anyhow::Error>(())
|
||||||
|
@ -234,38 +249,70 @@ fn show_hover(
|
||||||
.log_err()
|
.log_err()
|
||||||
});
|
});
|
||||||
|
|
||||||
editor.hover_state.task = Some(task);
|
editor.hover_state.info_task = Some(task);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct HoverState {
|
pub struct HoverState {
|
||||||
pub popover: Option<HoverPopover>,
|
pub info_popover: Option<InfoPopover>,
|
||||||
|
pub diagnostic_popover: Option<DiagnosticPopover>,
|
||||||
pub triggered_from: Option<Anchor>,
|
pub triggered_from: Option<Anchor>,
|
||||||
pub symbol_range: Option<Range<Anchor>>,
|
pub info_task: Option<Task<Option<()>>>,
|
||||||
pub task: Option<Task<Option<()>>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HoverState {
|
impl HoverState {
|
||||||
pub fn visible(&self) -> bool {
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct HoverPopover {
|
pub struct InfoPopover {
|
||||||
pub project: ModelHandle<Project>,
|
pub project: ModelHandle<Project>,
|
||||||
pub anchor: Anchor,
|
pub symbol_range: Range<Anchor>,
|
||||||
pub contents: Vec<HoverBlock>,
|
pub contents: Vec<HoverBlock>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HoverPopover {
|
impl InfoPopover {
|
||||||
pub fn render(
|
pub fn render(&self, style: &EditorStyle, cx: &mut RenderContext<Editor>) -> ElementBox {
|
||||||
&self,
|
MouseEventHandler::new::<InfoPopover, _, _>(0, cx, |_, cx| {
|
||||||
snapshot: &EditorSnapshot,
|
|
||||||
style: EditorStyle,
|
|
||||||
cx: &mut RenderContext<Editor>,
|
|
||||||
) -> (DisplayPoint, ElementBox) {
|
|
||||||
let element = MouseEventHandler::new::<HoverPopover, _, _>(0, cx, |_, cx| {
|
|
||||||
let mut flex = Flex::new(Axis::Vertical).scrollable::<HoverBlock, _>(1, None, cx);
|
let mut flex = Flex::new(Axis::Vertical).scrollable::<HoverBlock, _>(1, None, cx);
|
||||||
flex.extend(self.contents.iter().map(|content| {
|
flex.extend(self.contents.iter().map(|content| {
|
||||||
let project = self.project.read(cx);
|
let project = self.project.read(cx);
|
||||||
|
@ -309,10 +356,61 @@ impl HoverPopover {
|
||||||
top: 5.,
|
top: 5.,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
.boxed();
|
.boxed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let display_point = self.anchor.to_display_point(&snapshot.display_snapshot);
|
#[derive(Debug, Clone)]
|
||||||
(display_point, element)
|
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 futures::StreamExt;
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
|
|
||||||
|
use language::{Diagnostic, DiagnosticSet};
|
||||||
use project::HoverBlock;
|
use project::HoverBlock;
|
||||||
|
|
||||||
use crate::test::EditorLspTestContext;
|
use crate::test::EditorLspTestContext;
|
||||||
|
@ -328,7 +427,7 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[gpui::test]
|
#[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(
|
let mut cx = EditorLspTestContext::new_rust(
|
||||||
lsp::ServerCapabilities {
|
lsp::ServerCapabilities {
|
||||||
hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
|
hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
|
||||||
|
@ -362,19 +461,18 @@ mod tests {
|
||||||
fn test()
|
fn test()
|
||||||
[println!]();"});
|
[println!]();"});
|
||||||
let mut requests =
|
let mut requests =
|
||||||
cx.lsp
|
cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
|
||||||
.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _| async move {
|
Ok(Some(lsp::Hover {
|
||||||
Ok(Some(lsp::Hover {
|
contents: lsp::HoverContents::Markup(lsp::MarkupContent {
|
||||||
contents: lsp::HoverContents::Markup(lsp::MarkupContent {
|
kind: lsp::MarkupKind::Markdown,
|
||||||
kind: lsp::MarkupKind::Markdown,
|
value: indoc! {"
|
||||||
value: indoc! {"
|
|
||||||
# Some basic docs
|
# Some basic docs
|
||||||
Some test documentation"}
|
Some test documentation"}
|
||||||
.to_string(),
|
.to_string(),
|
||||||
}),
|
}),
|
||||||
range: Some(symbol_range),
|
range: Some(symbol_range),
|
||||||
}))
|
}))
|
||||||
});
|
});
|
||||||
cx.foreground()
|
cx.foreground()
|
||||||
.advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
|
.advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
|
||||||
requests.next().await;
|
requests.next().await;
|
||||||
|
@ -382,7 +480,7 @@ mod tests {
|
||||||
cx.editor(|editor, _| {
|
cx.editor(|editor, _| {
|
||||||
assert!(editor.hover_state.visible());
|
assert!(editor.hover_state.visible());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
editor.hover_state.popover.clone().unwrap().contents,
|
editor.hover_state.info_popover.clone().unwrap().contents,
|
||||||
vec![
|
vec![
|
||||||
HoverBlock {
|
HoverBlock {
|
||||||
text: "Some basic docs".to_string(),
|
text: "Some basic docs".to_string(),
|
||||||
|
@ -400,6 +498,9 @@ mod tests {
|
||||||
let hover_point = cx.display_point(indoc! {"
|
let hover_point = cx.display_point(indoc! {"
|
||||||
fn te|st()
|
fn te|st()
|
||||||
println!();"});
|
println!();"});
|
||||||
|
let mut request = cx
|
||||||
|
.lsp
|
||||||
|
.handle_request::<lsp::request::HoverRequest, _, _>(|_, _| async move { Ok(None) });
|
||||||
cx.update_editor(|editor, cx| {
|
cx.update_editor(|editor, cx| {
|
||||||
hover_at(
|
hover_at(
|
||||||
editor,
|
editor,
|
||||||
|
@ -409,15 +510,24 @@ mod tests {
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
let mut request = cx
|
|
||||||
.lsp
|
|
||||||
.handle_request::<lsp::request::HoverRequest, _, _>(|_, _| async move { Ok(None) });
|
|
||||||
cx.foreground()
|
cx.foreground()
|
||||||
.advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
|
.advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
|
||||||
request.next().await;
|
request.next().await;
|
||||||
cx.editor(|editor, _| {
|
cx.editor(|editor, _| {
|
||||||
assert!(!editor.hover_state.visible());
|
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
|
// Hover with keyboard has no delay
|
||||||
cx.set_state(indoc! {"
|
cx.set_state(indoc! {"
|
||||||
|
@ -427,26 +537,25 @@ mod tests {
|
||||||
let symbol_range = cx.lsp_range(indoc! {"
|
let symbol_range = cx.lsp_range(indoc! {"
|
||||||
[fn] test()
|
[fn] test()
|
||||||
println!();"});
|
println!();"});
|
||||||
cx.lsp
|
cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
|
||||||
.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _| async move {
|
Ok(Some(lsp::Hover {
|
||||||
Ok(Some(lsp::Hover {
|
contents: lsp::HoverContents::Markup(lsp::MarkupContent {
|
||||||
contents: lsp::HoverContents::Markup(lsp::MarkupContent {
|
kind: lsp::MarkupKind::Markdown,
|
||||||
kind: lsp::MarkupKind::Markdown,
|
value: indoc! {"
|
||||||
value: indoc! {"
|
# Some other basic docs
|
||||||
# Some other basic docs
|
Some other test documentation"}
|
||||||
Some other test documentation"}
|
.to_string(),
|
||||||
.to_string(),
|
}),
|
||||||
}),
|
range: Some(symbol_range),
|
||||||
range: Some(symbol_range),
|
}))
|
||||||
}))
|
})
|
||||||
})
|
.next()
|
||||||
.next()
|
.await;
|
||||||
.await;
|
|
||||||
cx.foreground().run_until_parked();
|
cx.condition(|editor, _| editor.hover_state.visible()).await;
|
||||||
cx.editor(|editor, _| {
|
cx.editor(|editor, _| {
|
||||||
assert!(editor.hover_state.visible());
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
editor.hover_state.popover.clone().unwrap().contents,
|
editor.hover_state.info_popover.clone().unwrap().contents,
|
||||||
vec![
|
vec![
|
||||||
HoverBlock {
|
HoverBlock {
|
||||||
text: "Some other basic docs".to_string(),
|
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 indoc::indoc;
|
||||||
|
|
||||||
use collections::BTreeMap;
|
use collections::BTreeMap;
|
||||||
use gpui::{json, keymap::Keystroke, AppContext, ModelHandle, ViewContext, ViewHandle};
|
use gpui::{
|
||||||
use language::{point_to_lsp, FakeLspAdapter, Language, LanguageConfig, Selection};
|
json, keymap::Keystroke, AppContext, ModelContext, ModelHandle, ViewContext, ViewHandle,
|
||||||
use lsp::request;
|
};
|
||||||
|
use language::{
|
||||||
|
point_to_lsp, Buffer, BufferSnapshot, FakeLspAdapter, Language, LanguageConfig, Selection,
|
||||||
|
};
|
||||||
|
use lsp::{notification, request};
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use util::{
|
use util::{
|
||||||
|
@ -119,7 +123,7 @@ impl<'a> EditorTestContext<'a> {
|
||||||
self.editor.condition(self.cx, predicate)
|
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
|
where
|
||||||
F: FnOnce(&Editor, &AppContext) -> T,
|
F: FnOnce(&Editor, &AppContext) -> T,
|
||||||
{
|
{
|
||||||
|
@ -133,9 +137,31 @@ impl<'a> EditorTestContext<'a> {
|
||||||
self.editor.update(self.cx, update)
|
self.editor.update(self.cx, update)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn buffer_text(&mut self) -> String {
|
pub fn multibuffer<F, T>(&self, read: F) -> T
|
||||||
self.editor.read_with(self.cx, |editor, cx| {
|
where
|
||||||
editor.buffer.read(cx).snapshot(cx).text()
|
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) {
|
pub fn simulate_keystroke(&mut self, keystroke_text: &str) {
|
||||||
let keystroke = Keystroke::parse(keystroke_text).unwrap();
|
let keystroke = Keystroke::parse(keystroke_text).unwrap();
|
||||||
self.cx.dispatch_keystroke(self.window_id, keystroke, false);
|
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)
|
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.
|
// Sets the editor state via a marked string.
|
||||||
// `|` characters represent empty selections
|
// `|` characters represent empty selections
|
||||||
// `[` to `}` represents a non empty selection with the head at `}`
|
// `[` to `}` represents a non empty selection with the head at `}`
|
||||||
|
@ -433,7 +485,7 @@ pub struct EditorLspTestContext<'a> {
|
||||||
pub cx: EditorTestContext<'a>,
|
pub cx: EditorTestContext<'a>,
|
||||||
pub lsp: lsp::FakeLanguageServer,
|
pub lsp: lsp::FakeLanguageServer,
|
||||||
pub workspace: ViewHandle<Workspace>,
|
pub workspace: ViewHandle<Workspace>,
|
||||||
pub editor_lsp_url: lsp::Url,
|
pub buffer_lsp_url: lsp::Url,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> EditorLspTestContext<'a> {
|
impl<'a> EditorLspTestContext<'a> {
|
||||||
|
@ -507,7 +559,7 @@ impl<'a> EditorLspTestContext<'a> {
|
||||||
},
|
},
|
||||||
lsp,
|
lsp,
|
||||||
workspace,
|
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
|
// Constructs lsp range using a marked string with '[', ']' range delimiters
|
||||||
pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range {
|
pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range {
|
||||||
let (unmarked, mut ranges) = marked_text_ranges_by(marked_text, vec![('[', ']').into()]);
|
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();
|
let offset_range = ranges.remove(&('[', ']').into()).unwrap()[0].clone();
|
||||||
self.to_lsp_range(offset_range)
|
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,
|
F: 'static + Send + FnMut(lsp::Url, T::Params, gpui::AsyncAppContext) -> Fut,
|
||||||
Fut: 'static + Send + Future<Output = Result<T::Result>>,
|
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| {
|
self.lsp.handle_request::<T, _, _>(move |params, cx| {
|
||||||
let url = url.clone();
|
let url = url.clone();
|
||||||
handler(url, params, cx)
|
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> {
|
impl<'a> Deref for EditorLspTestContext<'a> {
|
||||||
|
|
|
@ -317,15 +317,7 @@ mod tests {
|
||||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
|
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
|
||||||
cx.dispatch_action(window_id, Toggle);
|
cx.dispatch_action(window_id, Toggle);
|
||||||
|
|
||||||
let finder = cx.read(|cx| {
|
let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
|
||||||
workspace
|
|
||||||
.read(cx)
|
|
||||||
.modal()
|
|
||||||
.cloned()
|
|
||||||
.unwrap()
|
|
||||||
.downcast::<FileFinder>()
|
|
||||||
.unwrap()
|
|
||||||
});
|
|
||||||
finder
|
finder
|
||||||
.update(cx, |finder, cx| {
|
.update(cx, |finder, cx| {
|
||||||
finder.update_matches("bna".to_string(), cx)
|
finder.update_matches("bna".to_string(), cx)
|
||||||
|
|
|
@ -2466,11 +2466,11 @@ impl operation_queue::Operation for Operation {
|
||||||
impl Default for Diagnostic {
|
impl Default for Diagnostic {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
code: Default::default(),
|
code: None,
|
||||||
severity: DiagnosticSeverity::ERROR,
|
severity: DiagnosticSeverity::ERROR,
|
||||||
message: Default::default(),
|
message: Default::default(),
|
||||||
group_id: Default::default(),
|
group_id: 0,
|
||||||
is_primary: Default::default(),
|
is_primary: false,
|
||||||
is_valid: true,
|
is_valid: true,
|
||||||
is_disk_based: false,
|
is_disk_based: false,
|
||||||
is_unnecessary: false,
|
is_unnecessary: false,
|
||||||
|
|
|
@ -81,7 +81,7 @@ pub struct TerminalSettings {
|
||||||
pub working_directory: Option<WorkingDirectory>,
|
pub working_directory: Option<WorkingDirectory>,
|
||||||
pub font_size: Option<f32>,
|
pub font_size: Option<f32>,
|
||||||
pub font_family: Option<String>,
|
pub font_family: Option<String>,
|
||||||
pub env: Option<Vec<(String, String)>>,
|
pub env: Option<HashMap<String, String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
|
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||||
|
|
|
@ -8,7 +8,7 @@ path = "src/terminal.rs"
|
||||||
doctest = false
|
doctest = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
alacritty_terminal = "0.16.1"
|
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty", rev = "e9b864860ec79cc1b70042aafce100cdd6985a0a"}
|
||||||
editor = { path = "../editor" }
|
editor = { path = "../editor" }
|
||||||
util = { path = "../util" }
|
util = { path = "../util" }
|
||||||
gpui = { path = "../gpui" }
|
gpui = { path = "../gpui" }
|
||||||
|
@ -23,6 +23,10 @@ ordered-float = "2.1.1"
|
||||||
itertools = "0.10"
|
itertools = "0.10"
|
||||||
dirs = "4.0.0"
|
dirs = "4.0.0"
|
||||||
shellexpand = "2.1.0"
|
shellexpand = "2.1.0"
|
||||||
|
libc = "0.2"
|
||||||
|
anyhow = "1"
|
||||||
|
thiserror = "1.0"
|
||||||
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
gpui = { path = "../gpui", features = ["test-support"] }
|
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() {
|
fn test_rgb_for_index() {
|
||||||
//Test every possible value in the color cube
|
//Test every possible value in the color cube
|
||||||
for i in 16..=231 {
|
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);
|
assert_eq!(i, 16 + 36 * r + 6 * g + b);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,15 +1,6 @@
|
||||||
use alacritty_terminal::term::TermMode;
|
use alacritty_terminal::term::TermMode;
|
||||||
use gpui::keymap::Keystroke;
|
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)]
|
#[derive(Debug)]
|
||||||
pub enum Modifiers {
|
pub enum Modifiers {
|
||||||
None,
|
None,
|
||||||
|
@ -313,6 +304,20 @@ mod test {
|
||||||
assert_eq!(to_esc_str(&pagedown, &any), Some("\x1b[6~".to_string()));
|
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]
|
#[test]
|
||||||
fn test_application_mode() {
|
fn test_application_mode() {
|
||||||
let app_cursor = TermMode::APP_CURSOR;
|
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 connected_el;
|
||||||
pub mod connection;
|
pub mod connected_view;
|
||||||
mod modal;
|
pub mod mappings;
|
||||||
pub mod terminal_element;
|
pub mod modal_view;
|
||||||
|
pub mod model;
|
||||||
|
|
||||||
use alacritty_terminal::{
|
use connected_view::ConnectedView;
|
||||||
event::{Event as AlacTermEvent, EventListener},
|
|
||||||
term::SizeInfo,
|
|
||||||
};
|
|
||||||
|
|
||||||
use connection::{Event, TerminalConnection};
|
|
||||||
use dirs::home_dir;
|
use dirs::home_dir;
|
||||||
use futures::channel::mpsc::UnboundedSender;
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, elements::*, keymap::Keystroke, AppContext, ClipboardItem, Entity, ModelHandle,
|
actions, elements::*, geometry::vector::vec2f, AnyViewHandle, AppContext, Entity, ModelHandle,
|
||||||
MutableAppContext, View, ViewContext,
|
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 project::{LocalWorktree, Project, ProjectPath};
|
||||||
use settings::{Settings, WorkingDirectory};
|
use settings::{Settings, WorkingDirectory};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use terminal_element::TerminalEl;
|
|
||||||
use workspace::{Item, Workspace};
|
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_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_TERMINAL_HEIGHT: f32 = 200.;
|
||||||
const DEBUG_CELL_WIDTH: f32 = 5.;
|
const DEBUG_CELL_WIDTH: f32 = 5.;
|
||||||
const DEBUG_LINE_HEIGHT: f32 = 5.;
|
const DEBUG_LINE_HEIGHT: f32 = 5.;
|
||||||
|
|
||||||
//For bel, use a yellow dot. (equivalent to dirty file with conflict)
|
actions!(terminal, [Deploy, DeployModal]);
|
||||||
//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
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
///Initialize and register all of our action handlers
|
///Initialize and register all of our action handlers
|
||||||
pub fn init(cx: &mut MutableAppContext) {
|
pub fn init(cx: &mut MutableAppContext) {
|
||||||
//Global binding overrrides
|
cx.add_action(TerminalView::deploy);
|
||||||
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(deploy_modal);
|
cx.add_action(deploy_modal);
|
||||||
cx.add_action(Terminal::copy);
|
|
||||||
cx.add_action(Terminal::paste);
|
connected_view::init(cx);
|
||||||
cx.add_action(Terminal::clear);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
///A translation struct for Alacritty to communicate with us from their event loop
|
//Make terminal view an enum, that can give you views for the error and non-error states
|
||||||
#[derive(Clone)]
|
//Take away all the result unwrapping in the current TerminalView by making it 'infallible'
|
||||||
pub struct ZedListener(UnboundedSender<AlacTermEvent>);
|
//Bubble up to deploy(_modal)() calls
|
||||||
|
|
||||||
impl EventListener for ZedListener {
|
enum TerminalContent {
|
||||||
fn send_event(&self, event: AlacTermEvent) {
|
Connected(ViewHandle<ConnectedView>),
|
||||||
self.0.unbounded_send(event).ok();
|
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 TerminalView {
|
||||||
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
|
|
||||||
modal: bool,
|
modal: bool,
|
||||||
|
content: TerminalContent,
|
||||||
|
associated_directory: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Entity for Terminal {
|
pub struct ErrorView {
|
||||||
|
error: TerminalError,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Entity for TerminalView {
|
||||||
type Event = Event;
|
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
|
///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()`
|
///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 {
|
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
|
//The details here don't matter, the terminal will be resized on the first layout
|
||||||
let size_info = SizeInfo::new(
|
let size_info = TermDimensions::new(
|
||||||
DEBUG_TERMINAL_WIDTH,
|
|
||||||
DEBUG_TERMINAL_HEIGHT,
|
|
||||||
DEBUG_CELL_WIDTH,
|
|
||||||
DEBUG_LINE_HEIGHT,
|
DEBUG_LINE_HEIGHT,
|
||||||
0.,
|
DEBUG_CELL_WIDTH,
|
||||||
0.,
|
vec2f(DEBUG_TERMINAL_WIDTH, DEBUG_TERMINAL_HEIGHT),
|
||||||
false,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let (shell, envs) = {
|
let settings = cx.global::<Settings>();
|
||||||
let settings = cx.global::<Settings>();
|
let shell = settings.terminal_overrides.shell.clone();
|
||||||
let shell = settings.terminal_overrides.shell.clone();
|
let envs = settings.terminal_overrides.env.clone(); //Should be short and cheap.
|
||||||
let envs = settings.terminal_overrides.env.clone(); //Should be short and cheap.
|
|
||||||
(shell, envs)
|
let content = match TerminalBuilder::new(working_directory.clone(), shell, envs, size_info)
|
||||||
|
{
|
||||||
|
Ok(terminal) => {
|
||||||
|
let terminal = cx.add_model(|cx| terminal.subscribe(cx));
|
||||||
|
let view = cx.add_view(|cx| ConnectedView::from_terminal(terminal, modal, cx));
|
||||||
|
cx.subscribe(&view, |_this, _content, event, cx| cx.emit(event.clone()))
|
||||||
|
.detach();
|
||||||
|
TerminalContent::Connected(view)
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
let view = cx.add_view(|_| ErrorView {
|
||||||
|
error: error.downcast::<TerminalError>().unwrap(),
|
||||||
|
});
|
||||||
|
TerminalContent::Error(view)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
cx.focus(content.handle());
|
||||||
|
|
||||||
let connection = cx
|
TerminalView {
|
||||||
.add_model(|cx| TerminalConnection::new(working_directory, shell, envs, size_info, cx));
|
modal,
|
||||||
|
content,
|
||||||
Terminal::from_connection(connection, modal, cx)
|
associated_directory: working_directory,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_connection(
|
fn from_terminal(
|
||||||
connection: ModelHandle<TerminalConnection>,
|
terminal: ModelHandle<Terminal>,
|
||||||
modal: bool,
|
modal: bool,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) -> Terminal {
|
) -> Self {
|
||||||
cx.observe(&connection, |_, _, cx| cx.notify()).detach();
|
let connected_view = cx.add_view(|cx| ConnectedView::from_terminal(terminal, modal, cx));
|
||||||
cx.subscribe(&connection, |this, _, event, cx| match event {
|
TerminalView {
|
||||||
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,
|
|
||||||
modal,
|
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 {
|
fn ui_name() -> &'static str {
|
||||||
"Terminal"
|
"Terminal"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox {
|
fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox {
|
||||||
let element = {
|
let child_view = match &self.content {
|
||||||
let connection_handle = self.connection.clone().downgrade();
|
TerminalContent::Connected(connected) => ChildView::new(connected),
|
||||||
let view_id = cx.view_id();
|
TerminalContent::Error(error) => ChildView::new(error),
|
||||||
TerminalEl::new(view_id, connection_handle, self.modal).contained()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if self.modal {
|
if self.modal {
|
||||||
let settings = cx.global::<Settings>();
|
let settings = cx.global::<Settings>();
|
||||||
let container_style = settings.theme.terminal.modal_container;
|
let container_style = settings.theme.terminal.modal_container;
|
||||||
element.with_style(container_style).boxed()
|
child_view.contained().with_style(container_style).boxed()
|
||||||
} else {
|
} else {
|
||||||
element.boxed()
|
child_view.boxed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
|
fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
cx.emit(Event::Activate);
|
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 {
|
fn keymap_context(&self, _: &gpui::AppContext) -> gpui::keymap::Context {
|
||||||
|
@ -261,67 +172,83 @@ impl View for Terminal {
|
||||||
}
|
}
|
||||||
context
|
context
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn selected_text_range(&self, _: &AppContext) -> Option<std::ops::Range<usize>> {
|
impl View for ErrorView {
|
||||||
Some(0..0)
|
fn ui_name() -> &'static str {
|
||||||
|
"Terminal Error"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn replace_text_in_range(
|
fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox {
|
||||||
&mut self,
|
let settings = cx.global::<Settings>();
|
||||||
_: Option<std::ops::Range<usize>>,
|
let style = TerminalEl::make_text_style(cx.font_cache(), settings);
|
||||||
text: &str,
|
|
||||||
cx: &mut ViewContext<Self>,
|
//TODO:
|
||||||
) {
|
//We want markdown style highlighting so we can format the program and working directory with ``
|
||||||
self.input(text, cx);
|
//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(
|
fn tab_content(
|
||||||
&self,
|
&self,
|
||||||
_detail: Option<usize>,
|
_detail: Option<usize>,
|
||||||
tab_theme: &theme::Tab,
|
tab_theme: &theme::Tab,
|
||||||
cx: &gpui::AppContext,
|
cx: &gpui::AppContext,
|
||||||
) -> ElementBox {
|
) -> ElementBox {
|
||||||
let settings = cx.global::<Settings>();
|
let title = match &self.content {
|
||||||
let search_theme = &settings.theme.search; //TODO properly integrate themes
|
TerminalContent::Connected(connected) => {
|
||||||
|
connected.read(cx).handle().read(cx).title.clone()
|
||||||
let mut flex = Flex::row();
|
}
|
||||||
|
TerminalContent::Error(_) => "Terminal".to_string(),
|
||||||
if self.has_bell {
|
|
||||||
flex.add_child(
|
|
||||||
Svg::new("icons/bolt_12.svg") //TODO: Swap out for a better icon, or at least resize this
|
|
||||||
.with_color(tab_theme.label.text.color)
|
|
||||||
.constrained()
|
|
||||||
.with_width(search_theme.tab_icon_width)
|
|
||||||
.aligned()
|
|
||||||
.boxed(),
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
flex.with_child(
|
Flex::row()
|
||||||
Label::new(
|
.with_child(
|
||||||
self.connection.read(cx).title.clone(),
|
Label::new(title, tab_theme.label.clone())
|
||||||
tab_theme.label.clone(),
|
.aligned()
|
||||||
|
.contained()
|
||||||
|
.boxed(),
|
||||||
)
|
)
|
||||||
.aligned()
|
.boxed()
|
||||||
.contained()
|
|
||||||
.with_margin_left(if self.has_bell {
|
|
||||||
search_theme.tab_icon_spacing
|
|
||||||
} else {
|
|
||||||
0.
|
|
||||||
})
|
|
||||||
.boxed(),
|
|
||||||
)
|
|
||||||
.boxed()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self> {
|
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
|
//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
|
//solutions to this, but they are non-trivial and require more IPC
|
||||||
Some(Terminal::new(
|
Some(TerminalView::new(
|
||||||
self.connection.read(cx).associated_directory.clone(),
|
self.associated_directory.clone(),
|
||||||
false,
|
false,
|
||||||
cx,
|
cx,
|
||||||
))
|
))
|
||||||
|
@ -370,8 +297,20 @@ impl Item for Terminal {
|
||||||
gpui::Task::ready(Ok(()))
|
gpui::Task::ready(Ok(()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_dirty(&self, _: &gpui::AppContext) -> bool {
|
fn is_dirty(&self, cx: &gpui::AppContext) -> bool {
|
||||||
self.has_new_content
|
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 {
|
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.
|
///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
|
let wd_setting = cx
|
||||||
.global::<Settings>()
|
.global::<Settings>()
|
||||||
.terminal_overrides
|
.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::CurrentProjectDirectory => current_project_directory(workspace, cx),
|
||||||
WorkingDirectory::FirstProjectDirectory => first_project_directory(workspace, cx),
|
WorkingDirectory::FirstProjectDirectory => first_project_directory(workspace, cx),
|
||||||
WorkingDirectory::AlwaysHome => None,
|
WorkingDirectory::AlwaysHome => None,
|
||||||
WorkingDirectory::Always { directory } => shellexpand::full(&directory)
|
WorkingDirectory::Always { directory } => {
|
||||||
.ok()
|
shellexpand::full(&directory) //TODO handle this better
|
||||||
.map(|dir| Path::new(&dir.to_string()).to_path_buf())
|
.ok()
|
||||||
.filter(|dir| dir.is_dir()),
|
.map(|dir| Path::new(&dir.to_string()).to_path_buf())
|
||||||
|
.filter(|dir| dir.is_dir())
|
||||||
|
}
|
||||||
};
|
};
|
||||||
res.or_else(|| home_dir())
|
res.or_else(|| home_dir())
|
||||||
}
|
}
|
||||||
|
@ -447,7 +388,6 @@ mod tests {
|
||||||
use gpui::TestAppContext;
|
use gpui::TestAppContext;
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use workspace::AppState;
|
|
||||||
|
|
||||||
mod terminal_test_context;
|
mod terminal_test_context;
|
||||||
|
|
||||||
|
@ -455,7 +395,7 @@ mod tests {
|
||||||
//and produce noticable output?
|
//and produce noticable output?
|
||||||
#[gpui::test(retries = 5)]
|
#[gpui::test(retries = 5)]
|
||||||
async fn test_terminal(cx: &mut TestAppContext) {
|
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"))
|
cx.execute_and_wait("expr 3 + 4", |content, _cx| content.contains("7"))
|
||||||
.await;
|
.await;
|
||||||
|
@ -467,12 +407,10 @@ mod tests {
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn no_worktree(cx: &mut TestAppContext) {
|
async fn no_worktree(cx: &mut TestAppContext) {
|
||||||
//Setup variables
|
//Setup variables
|
||||||
let params = cx.update(AppState::test);
|
let mut cx = TerminalTestContext::new(cx, true);
|
||||||
let project = Project::test(params.fs.clone(), [], cx).await;
|
let (project, workspace) = cx.blank_workspace().await;
|
||||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
|
|
||||||
|
|
||||||
//Test
|
//Test
|
||||||
cx.read(|cx| {
|
cx.cx.read(|cx| {
|
||||||
let workspace = workspace.read(cx);
|
let workspace = workspace.read(cx);
|
||||||
let active_entry = project.read(cx).active_entry();
|
let active_entry = project.read(cx).active_entry();
|
||||||
|
|
||||||
|
@ -491,28 +429,12 @@ mod tests {
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn no_active_entry_worktree_is_file(cx: &mut TestAppContext) {
|
async fn no_active_entry_worktree_is_file(cx: &mut TestAppContext) {
|
||||||
//Setup variables
|
//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| {
|
let mut cx = TerminalTestContext::new(cx, true);
|
||||||
wt.update(cx, |wt, cx| {
|
let (project, workspace) = cx.blank_workspace().await;
|
||||||
wt.as_local()
|
cx.create_file_wt(project.clone(), "/root.txt").await;
|
||||||
.unwrap()
|
|
||||||
.create_entry(Path::new(""), false, cx)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
//Test
|
cx.cx.read(|cx| {
|
||||||
cx.read(|cx| {
|
|
||||||
let workspace = workspace.read(cx);
|
let workspace = workspace.read(cx);
|
||||||
let active_entry = project.read(cx).active_entry();
|
let active_entry = project.read(cx).active_entry();
|
||||||
|
|
||||||
|
@ -531,27 +453,12 @@ mod tests {
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn no_active_entry_worktree_is_dir(cx: &mut TestAppContext) {
|
async fn no_active_entry_worktree_is_dir(cx: &mut TestAppContext) {
|
||||||
//Setup variables
|
//Setup variables
|
||||||
let params = cx.update(AppState::test);
|
let mut cx = TerminalTestContext::new(cx, true);
|
||||||
let project = Project::test(params.fs.clone(), [], cx).await;
|
let (project, workspace) = cx.blank_workspace().await;
|
||||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
|
let (_wt, _entry) = cx.create_folder_wt(project.clone(), "/root/").await;
|
||||||
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();
|
|
||||||
|
|
||||||
//Test
|
//Test
|
||||||
cx.update(|cx| {
|
cx.cx.update(|cx| {
|
||||||
let workspace = workspace.read(cx);
|
let workspace = workspace.read(cx);
|
||||||
let active_entry = project.read(cx).active_entry();
|
let active_entry = project.read(cx).active_entry();
|
||||||
|
|
||||||
|
@ -569,53 +476,14 @@ mod tests {
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn active_entry_worktree_is_file(cx: &mut TestAppContext) {
|
async fn active_entry_worktree_is_file(cx: &mut TestAppContext) {
|
||||||
//Setup variables
|
//Setup variables
|
||||||
let params = cx.update(AppState::test);
|
let mut cx = TerminalTestContext::new(cx, true);
|
||||||
let project = Project::test(params.fs.clone(), [], cx).await;
|
let (project, workspace) = cx.blank_workspace().await;
|
||||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
|
let (_wt, _entry) = cx.create_folder_wt(project.clone(), "/root1/").await;
|
||||||
let (wt1, _) = project
|
let (wt2, entry2) = cx.create_file_wt(project.clone(), "/root2.txt").await;
|
||||||
.update(cx, |project, cx| {
|
cx.insert_active_entry_for(wt2, entry2, project.clone());
|
||||||
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));
|
|
||||||
});
|
|
||||||
|
|
||||||
//Test
|
//Test
|
||||||
cx.update(|cx| {
|
cx.cx.update(|cx| {
|
||||||
let workspace = workspace.read(cx);
|
let workspace = workspace.read(cx);
|
||||||
let active_entry = project.read(cx).active_entry();
|
let active_entry = project.read(cx).active_entry();
|
||||||
|
|
||||||
|
@ -632,51 +500,14 @@ mod tests {
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn active_entry_worktree_is_dir(cx: &mut TestAppContext) {
|
async fn active_entry_worktree_is_dir(cx: &mut TestAppContext) {
|
||||||
//Setup variables
|
//Setup variables
|
||||||
let params = cx.update(AppState::test);
|
let mut cx = TerminalTestContext::new(cx, true);
|
||||||
let project = Project::test(params.fs.clone(), [], cx).await;
|
let (project, workspace) = cx.blank_workspace().await;
|
||||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
|
let (_wt, _entry) = cx.create_folder_wt(project.clone(), "/root1/").await;
|
||||||
let (wt1, _) = project
|
let (wt2, entry2) = cx.create_folder_wt(project.clone(), "/root2/").await;
|
||||||
.update(cx, |project, cx| {
|
cx.insert_active_entry_for(wt2, entry2, project.clone());
|
||||||
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));
|
|
||||||
});
|
|
||||||
|
|
||||||
//Test
|
//Test
|
||||||
cx.update(|cx| {
|
cx.cx.update(|cx| {
|
||||||
let workspace = workspace.read(cx);
|
let workspace = workspace.read(cx);
|
||||||
let active_entry = project.read(cx).active_entry();
|
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::{
|
||||||
use gpui::{AppContext, ModelHandle, ReadModelWith, TestAppContext};
|
geometry::vector::vec2f, AppContext, ModelHandle, ReadModelWith, TestAppContext, ViewHandle,
|
||||||
|
};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
use project::{Entry, Project, ProjectPath, Worktree};
|
||||||
|
use workspace::{AppState, Workspace};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
connection::TerminalConnection, DEBUG_CELL_WIDTH, DEBUG_LINE_HEIGHT, DEBUG_TERMINAL_HEIGHT,
|
connected_el::TermDimensions,
|
||||||
DEBUG_TERMINAL_WIDTH,
|
model::{Terminal, TerminalBuilder},
|
||||||
|
DEBUG_CELL_WIDTH, DEBUG_LINE_HEIGHT, DEBUG_TERMINAL_HEIGHT, DEBUG_TERMINAL_WIDTH,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct TerminalTestContext<'a> {
|
pub struct TerminalTestContext<'a> {
|
||||||
pub cx: &'a mut TestAppContext,
|
pub cx: &'a mut TestAppContext,
|
||||||
pub connection: ModelHandle<TerminalConnection>,
|
pub connection: Option<ModelHandle<Terminal>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> TerminalTestContext<'a> {
|
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)));
|
cx.set_condition_duration(Some(Duration::from_secs(5)));
|
||||||
|
|
||||||
let size_info = SizeInfo::new(
|
let size_info = TermDimensions::new(
|
||||||
DEBUG_TERMINAL_WIDTH,
|
|
||||||
DEBUG_TERMINAL_HEIGHT,
|
|
||||||
DEBUG_CELL_WIDTH,
|
DEBUG_CELL_WIDTH,
|
||||||
DEBUG_LINE_HEIGHT,
|
DEBUG_LINE_HEIGHT,
|
||||||
0.,
|
vec2f(DEBUG_TERMINAL_WIDTH, DEBUG_TERMINAL_HEIGHT),
|
||||||
0.,
|
|
||||||
false,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let connection =
|
let connection = term.then(|| {
|
||||||
cx.add_model(|cx| TerminalConnection::new(None, None, None, size_info, cx));
|
cx.add_model(|cx| {
|
||||||
|
TerminalBuilder::new(None, None, None, size_info)
|
||||||
|
.unwrap()
|
||||||
|
.subscribe(cx)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
TerminalTestContext { cx, connection }
|
TerminalTestContext { cx, connection }
|
||||||
}
|
}
|
||||||
|
@ -38,34 +43,112 @@ impl<'a> TerminalTestContext<'a> {
|
||||||
where
|
where
|
||||||
F: Fn(String, &AppContext) -> bool,
|
F: Fn(String, &AppContext) -> bool,
|
||||||
{
|
{
|
||||||
|
let connection = self.connection.take().unwrap();
|
||||||
|
|
||||||
let command = command.to_string();
|
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(command);
|
||||||
connection.write_to_pty("\r".to_string());
|
connection.write_to_pty("\r".to_string());
|
||||||
});
|
});
|
||||||
|
|
||||||
self.connection
|
connection
|
||||||
.condition(self.cx, |conn, cx| {
|
.condition(self.cx, |conn, cx| {
|
||||||
let content = Self::grid_as_str(conn);
|
let content = Self::grid_as_str(conn);
|
||||||
f(content, cx)
|
f(content, cx)
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
self.cx
|
let res = self
|
||||||
.read_model_with(&self.connection, &mut |conn, _: &AppContext| {
|
.cx
|
||||||
|
.read_model_with(&connection, &mut |conn, _: &AppContext| {
|
||||||
Self::grid_as_str(conn)
|
Self::grid_as_str(conn)
|
||||||
})
|
});
|
||||||
|
|
||||||
|
self.connection = Some(connection);
|
||||||
|
|
||||||
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
fn grid_as_str(connection: &TerminalConnection) -> String {
|
///Creates a worktree with 1 file: /root.txt
|
||||||
let term = connection.term.lock();
|
pub async fn blank_workspace(&mut self) -> (ModelHandle<Project>, ViewHandle<Workspace>) {
|
||||||
let grid_iterator = term.renderable_content().display_iter;
|
let params = self.cx.update(AppState::test);
|
||||||
let lines = grid_iterator.group_by(|i| i.point.line.0);
|
|
||||||
lines
|
let project = Project::test(params.fs.clone(), [], self.cx).await;
|
||||||
.into_iter()
|
let (_, workspace) = self.cx.add_window(|cx| Workspace::new(project.clone(), cx));
|
||||||
.map(|(_, line)| line.map(|i| i.c).collect::<String>())
|
|
||||||
.collect::<Vec<String>>()
|
(project, workspace)
|
||||||
.join("\n")
|
}
|
||||||
|
|
||||||
|
///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,
|
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> {
|
impl<T: Copy + Ord> Selection<T> {
|
||||||
|
@ -78,13 +85,6 @@ impl<T: Copy + Ord> Selection<T> {
|
||||||
self.goal = new_goal;
|
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> {
|
pub fn range(&self) -> Range<T> {
|
||||||
self.start..self.end
|
self.start..self.end
|
||||||
}
|
}
|
||||||
|
|
|
@ -630,6 +630,9 @@ impl<'de> Deserialize<'de> for SyntaxTheme {
|
||||||
#[derive(Clone, Deserialize, Default)]
|
#[derive(Clone, Deserialize, Default)]
|
||||||
pub struct HoverPopover {
|
pub struct HoverPopover {
|
||||||
pub container: ContainerStyle,
|
pub container: ContainerStyle,
|
||||||
|
pub info_container: ContainerStyle,
|
||||||
|
pub warning_container: ContainerStyle,
|
||||||
|
pub error_container: ContainerStyle,
|
||||||
pub block_style: ContainerStyle,
|
pub block_style: ContainerStyle,
|
||||||
pub prose: TextStyle,
|
pub prose: TextStyle,
|
||||||
pub highlight: Color,
|
pub highlight: Color,
|
||||||
|
|
|
@ -1224,8 +1224,10 @@ impl Workspace {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn modal(&self) -> Option<&AnyViewHandle> {
|
pub fn modal<V: 'static + View>(&self) -> Option<ViewHandle<V>> {
|
||||||
self.modal.as_ref()
|
self.modal
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|modal| modal.clone().downcast::<V>())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn dismiss_modal(&mut self, cx: &mut ViewContext<Self>) {
|
pub fn dismiss_modal(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
|
|
@ -285,7 +285,7 @@ pub fn menus() -> Vec<Menu<'static>> {
|
||||||
MenuItem::Separator,
|
MenuItem::Separator,
|
||||||
MenuItem::Action {
|
MenuItem::Action {
|
||||||
name: "Next Problem",
|
name: "Next Problem",
|
||||||
action: Box::new(editor::GoToNextDiagnostic),
|
action: Box::new(editor::GoToDiagnostic),
|
||||||
},
|
},
|
||||||
MenuItem::Action {
|
MenuItem::Action {
|
||||||
name: "Previous Problem",
|
name: "Previous Problem",
|
||||||
|
|
|
@ -2,22 +2,48 @@ import Theme from "../themes/common/theme";
|
||||||
import { backgroundColor, border, popoverShadow, text } from "./components";
|
import { backgroundColor, border, popoverShadow, text } from "./components";
|
||||||
|
|
||||||
export default function HoverPopover(theme: Theme) {
|
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 {
|
return {
|
||||||
container: {
|
container: baseContainer,
|
||||||
background: backgroundColor(theme, "on500"),
|
infoContainer: {
|
||||||
cornerRadius: 8,
|
...baseContainer,
|
||||||
padding: {
|
background: backgroundColor(theme, "on500Info"),
|
||||||
left: 8,
|
border: {
|
||||||
right: 8,
|
color: theme.ramps.blue(0).hex(),
|
||||||
top: 4,
|
width: 1,
|
||||||
bottom: 4,
|
|
||||||
},
|
},
|
||||||
shadow: popoverShadow(theme),
|
},
|
||||||
border: border(theme, "primary"),
|
warningContainer: {
|
||||||
margin: {
|
...baseContainer,
|
||||||
left: -8,
|
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: {
|
block_style: {
|
||||||
padding: { top: 4 },
|
padding: { top: 4 },
|
||||||
},
|
},
|
||||||
|
|
|
@ -88,16 +88,31 @@ export function createTheme(
|
||||||
hovered: withOpacity(sample(ramps.red, 0.5), 0.2),
|
hovered: withOpacity(sample(ramps.red, 0.5), 0.2),
|
||||||
active: withOpacity(sample(ramps.red, 0.5), 0.25),
|
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: {
|
warning: {
|
||||||
base: withOpacity(sample(ramps.yellow, 0.5), 0.15),
|
base: withOpacity(sample(ramps.yellow, 0.5), 0.15),
|
||||||
hovered: withOpacity(sample(ramps.yellow, 0.5), 0.2),
|
hovered: withOpacity(sample(ramps.yellow, 0.5), 0.2),
|
||||||
active: withOpacity(sample(ramps.yellow, 0.5), 0.25),
|
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: {
|
info: {
|
||||||
base: withOpacity(sample(ramps.blue, 0.5), 0.15),
|
base: withOpacity(sample(ramps.blue, 0.5), 0.15),
|
||||||
hovered: withOpacity(sample(ramps.blue, 0.5), 0.2),
|
hovered: withOpacity(sample(ramps.blue, 0.5), 0.2),
|
||||||
active: withOpacity(sample(ramps.blue, 0.5), 0.25),
|
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 = {
|
const borderColor = {
|
||||||
|
@ -106,10 +121,10 @@ export function createTheme(
|
||||||
muted: sample(ramps.neutral, isLight ? 1 : 3),
|
muted: sample(ramps.neutral, isLight ? 1 : 3),
|
||||||
active: sample(ramps.neutral, isLight ? 4 : 3),
|
active: sample(ramps.neutral, isLight ? 4 : 3),
|
||||||
onMedia: withOpacity(darkest, 0.1),
|
onMedia: withOpacity(darkest, 0.1),
|
||||||
ok: withOpacity(sample(ramps.green, 0.5), 0.15),
|
ok: sample(ramps.green, 0.3),
|
||||||
error: withOpacity(sample(ramps.red, 0.5), 0.15),
|
error: sample(ramps.red, 0.3),
|
||||||
warning: withOpacity(sample(ramps.yellow, 0.5), 0.15),
|
warning: sample(ramps.yellow, 0.3),
|
||||||
info: withOpacity(sample(ramps.blue, 0.5), 0.15),
|
info: sample(ramps.blue, 0.3),
|
||||||
};
|
};
|
||||||
|
|
||||||
const textColor = {
|
const textColor = {
|
||||||
|
|
|
@ -79,8 +79,11 @@ export default interface Theme {
|
||||||
on500: BackgroundColorSet;
|
on500: BackgroundColorSet;
|
||||||
ok: BackgroundColorSet;
|
ok: BackgroundColorSet;
|
||||||
error: BackgroundColorSet;
|
error: BackgroundColorSet;
|
||||||
|
on500Error: BackgroundColorSet;
|
||||||
warning: BackgroundColorSet;
|
warning: BackgroundColorSet;
|
||||||
|
on500Warning: BackgroundColorSet;
|
||||||
info: BackgroundColorSet;
|
info: BackgroundColorSet;
|
||||||
|
on500Info: BackgroundColorSet;
|
||||||
};
|
};
|
||||||
borderColor: {
|
borderColor: {
|
||||||
primary: string;
|
primary: string;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue