Merge branch 'main' into terminal-modal
This commit is contained in:
commit
d373e4424f
42 changed files with 1684 additions and 742 deletions
4
Cargo.lock
generated
4
Cargo.lock
generated
|
@ -4878,6 +4878,8 @@ name = "terminal"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"alacritty_terminal",
|
"alacritty_terminal",
|
||||||
|
"client",
|
||||||
|
"dirs 4.0.0",
|
||||||
"editor",
|
"editor",
|
||||||
"futures",
|
"futures",
|
||||||
"gpui",
|
"gpui",
|
||||||
|
@ -6159,7 +6161,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zed"
|
name = "zed"
|
||||||
version = "0.44.1"
|
version = "0.45.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"activity_indicator",
|
"activity_indicator",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
|
3
assets/icons/arrow-left.svg
Normal file
3
assets/icons/arrow-left.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M8 3.99999C8 4.31671 7.76023 4.57258 7.44352 4.57258H1.95565L3.8416 6.45853C4.06527 6.6822 4.06527 7.04454 3.8416 7.2682C3.72887 7.38004 3.58215 7.43551 3.43542 7.43551C3.2887 7.43551 3.14233 7.37959 3.03068 7.26776L0.16775 4.40483C-0.0559165 4.18116 -0.0559165 3.81883 0.16775 3.59516L3.03068 0.732233C3.25434 0.508567 3.61668 0.508567 3.84035 0.732233C4.06401 0.955899 4.06401 1.31824 3.84035 1.5419L1.95565 3.42741H7.44352C7.76023 3.42741 8 3.68328 8 3.99999Z" fill="#839496"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 589 B |
3
assets/icons/arrow-right.svg
Normal file
3
assets/icons/arrow-right.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M7.83265 4.40382L4.97532 7.26115C4.8646 7.37365 4.71816 7.42901 4.57172 7.42901C4.42528 7.42901 4.2792 7.37321 4.16777 7.26159C3.94454 7.03836 3.94454 6.67673 4.16777 6.4535L6.05039 4.57169H0.571465C0.255909 4.57169 0 4.31631 0 4.00022C0 3.68413 0.255731 3.42876 0.571287 3.42876H6.05021L4.16795 1.54649C3.94472 1.32326 3.94472 0.961634 4.16795 0.738405C4.39117 0.515177 4.75281 0.515177 4.97603 0.738405L7.83336 3.59573C8.0557 3.81985 8.0557 4.18059 7.83247 4.40382H7.83265Z" fill="#FDF6E3"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 602 B |
|
@ -417,7 +417,8 @@
|
||||||
"up": "terminal::Up",
|
"up": "terminal::Up",
|
||||||
"down": "terminal::Down",
|
"down": "terminal::Down",
|
||||||
"tab": "terminal::Tab",
|
"tab": "terminal::Tab",
|
||||||
"cmd-v": "terminal::Paste"
|
"cmd-v": "terminal::Paste",
|
||||||
|
"cmd-c": "terminal::Copy"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
|
@ -549,7 +549,7 @@ impl Client {
|
||||||
client.respond_with_error(
|
client.respond_with_error(
|
||||||
receipt,
|
receipt,
|
||||||
proto::Error {
|
proto::Error {
|
||||||
message: error.to_string(),
|
message: format!("{:?}", error),
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
Err(error)
|
Err(error)
|
||||||
|
|
|
@ -35,7 +35,7 @@ use project::{
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use rpc::PeerId;
|
use rpc::PeerId;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use settings::Settings;
|
use settings::{FormatOnSave, Settings};
|
||||||
use sqlx::types::time::OffsetDateTime;
|
use sqlx::types::time::OffsetDateTime;
|
||||||
use std::{
|
use std::{
|
||||||
cell::RefCell,
|
cell::RefCell,
|
||||||
|
@ -1912,7 +1912,6 @@ async fn test_reloading_buffer_manually(cx_a: &mut TestAppContext, cx_b: &mut Te
|
||||||
|
|
||||||
#[gpui::test(iterations = 10)]
|
#[gpui::test(iterations = 10)]
|
||||||
async fn test_formatting_buffer(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
async fn test_formatting_buffer(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||||
cx_a.foreground().forbid_parking();
|
|
||||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||||
let client_a = server.create_client(cx_a, "user_a").await;
|
let client_a = server.create_client(cx_a, "user_a").await;
|
||||||
let client_b = server.create_client(cx_b, "user_b").await;
|
let client_b = server.create_client(cx_b, "user_b").await;
|
||||||
|
@ -1932,11 +1931,15 @@ async fn test_formatting_buffer(cx_a: &mut TestAppContext, cx_b: &mut TestAppCon
|
||||||
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
|
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
|
||||||
client_a.language_registry.add(Arc::new(language));
|
client_a.language_registry.add(Arc::new(language));
|
||||||
|
|
||||||
|
// Here we insert a fake tree with a directory that exists on disk. This is needed
|
||||||
|
// because later we'll invoke a command, which requires passing a working directory
|
||||||
|
// that points to a valid location on disk.
|
||||||
|
let directory = env::current_dir().unwrap();
|
||||||
client_a
|
client_a
|
||||||
.fs
|
.fs
|
||||||
.insert_tree("/a", json!({ "a.rs": "let one = two" }))
|
.insert_tree(&directory, json!({ "a.rs": "let one = \"two\"" }))
|
||||||
.await;
|
.await;
|
||||||
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
|
let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await;
|
||||||
let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
|
let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
|
||||||
|
|
||||||
let buffer_b = cx_b
|
let buffer_b = cx_b
|
||||||
|
@ -1967,7 +1970,28 @@ async fn test_formatting_buffer(cx_a: &mut TestAppContext, cx_b: &mut TestAppCon
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
|
buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
|
||||||
"let honey = two"
|
"let honey = \"two\""
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ensure buffer can be formatted using an external command. Notice how the
|
||||||
|
// host's configuration is honored as opposed to using the guest's settings.
|
||||||
|
cx_a.update(|cx| {
|
||||||
|
cx.update_global(|settings: &mut Settings, _| {
|
||||||
|
settings.language_settings.format_on_save = Some(FormatOnSave::External {
|
||||||
|
command: "awk".to_string(),
|
||||||
|
arguments: vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
project_b
|
||||||
|
.update(cx_b, |project, cx| {
|
||||||
|
project.format(HashSet::from_iter([buffer_b.clone()]), true, cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
|
||||||
|
format!("let honey = \"{}/a.rs\"\n", directory.to_str().unwrap())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4065,13 +4065,16 @@ impl Editor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
nav_history.push(Some(NavigationData {
|
nav_history.push(
|
||||||
cursor_anchor: position,
|
Some(NavigationData {
|
||||||
cursor_position: point,
|
cursor_anchor: position,
|
||||||
scroll_position: self.scroll_position,
|
cursor_position: point,
|
||||||
scroll_top_anchor: self.scroll_top_anchor.clone(),
|
scroll_position: self.scroll_position,
|
||||||
scroll_top_row,
|
scroll_top_anchor: self.scroll_top_anchor.clone(),
|
||||||
}));
|
scroll_top_row,
|
||||||
|
}),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4669,7 +4672,7 @@ impl Editor {
|
||||||
definitions: Vec<LocationLink>,
|
definitions: Vec<LocationLink>,
|
||||||
cx: &mut ViewContext<Workspace>,
|
cx: &mut ViewContext<Workspace>,
|
||||||
) {
|
) {
|
||||||
let nav_history = workspace.active_pane().read(cx).nav_history().clone();
|
let pane = workspace.active_pane().clone();
|
||||||
for definition in definitions {
|
for definition in definitions {
|
||||||
let range = definition
|
let range = definition
|
||||||
.target
|
.target
|
||||||
|
@ -4681,13 +4684,13 @@ impl Editor {
|
||||||
// When selecting a definition in a different buffer, disable the nav history
|
// When selecting a definition in a different buffer, disable the nav history
|
||||||
// to avoid creating a history entry at the previous cursor location.
|
// to avoid creating a history entry at the previous cursor location.
|
||||||
if editor_handle != target_editor_handle {
|
if editor_handle != target_editor_handle {
|
||||||
nav_history.borrow_mut().disable();
|
pane.update(cx, |pane, _| pane.disable_history());
|
||||||
}
|
}
|
||||||
target_editor.change_selections(Some(Autoscroll::Center), cx, |s| {
|
target_editor.change_selections(Some(Autoscroll::Center), cx, |s| {
|
||||||
s.select_ranges([range]);
|
s.select_ranges([range]);
|
||||||
});
|
});
|
||||||
|
|
||||||
nav_history.borrow_mut().enable();
|
pane.update(cx, |pane, _| pane.enable_history());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5641,8 +5644,8 @@ impl Editor {
|
||||||
editor_handle.update(cx, |editor, cx| {
|
editor_handle.update(cx, |editor, cx| {
|
||||||
editor.push_to_nav_history(editor.selections.newest_anchor().head(), None, cx);
|
editor.push_to_nav_history(editor.selections.newest_anchor().head(), None, cx);
|
||||||
});
|
});
|
||||||
let nav_history = workspace.active_pane().read(cx).nav_history().clone();
|
let pane = workspace.active_pane().clone();
|
||||||
nav_history.borrow_mut().disable();
|
pane.update(cx, |pane, _| pane.disable_history());
|
||||||
|
|
||||||
// We defer the pane interaction because we ourselves are a workspace item
|
// We defer the pane interaction because we ourselves are a workspace item
|
||||||
// and activating a new item causes the pane to call a method on us reentrantly,
|
// and activating a new item causes the pane to call a method on us reentrantly,
|
||||||
|
@ -5657,7 +5660,7 @@ impl Editor {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
nav_history.borrow_mut().enable();
|
pane.update(cx, |pane, _| pane.enable_history());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6241,7 +6244,7 @@ mod tests {
|
||||||
assert_set_eq,
|
assert_set_eq,
|
||||||
test::{marked_text_by, marked_text_ranges, marked_text_ranges_by, sample_text},
|
test::{marked_text_by, marked_text_ranges, marked_text_ranges_by, sample_text},
|
||||||
};
|
};
|
||||||
use workspace::{FollowableItem, ItemHandle};
|
use workspace::{FollowableItem, ItemHandle, NavigationEntry, Pane};
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_edit_events(cx: &mut MutableAppContext) {
|
fn test_edit_events(cx: &mut MutableAppContext) {
|
||||||
|
@ -6589,12 +6592,20 @@ mod tests {
|
||||||
fn test_navigation_history(cx: &mut gpui::MutableAppContext) {
|
fn test_navigation_history(cx: &mut gpui::MutableAppContext) {
|
||||||
cx.set_global(Settings::test(cx));
|
cx.set_global(Settings::test(cx));
|
||||||
use workspace::Item;
|
use workspace::Item;
|
||||||
let nav_history = Rc::new(RefCell::new(workspace::NavHistory::default()));
|
let pane = cx.add_view(Default::default(), |cx| Pane::new(cx));
|
||||||
let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
|
let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
|
||||||
|
|
||||||
cx.add_window(Default::default(), |cx| {
|
cx.add_window(Default::default(), |cx| {
|
||||||
let mut editor = build_editor(buffer.clone(), cx);
|
let mut editor = build_editor(buffer.clone(), cx);
|
||||||
editor.nav_history = Some(ItemNavHistory::new(nav_history.clone(), &cx.handle()));
|
let handle = cx.handle();
|
||||||
|
editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
|
||||||
|
|
||||||
|
fn pop_history(
|
||||||
|
editor: &mut Editor,
|
||||||
|
cx: &mut MutableAppContext,
|
||||||
|
) -> Option<NavigationEntry> {
|
||||||
|
editor.nav_history.as_mut().unwrap().pop_backward(cx)
|
||||||
|
}
|
||||||
|
|
||||||
// Move the cursor a small distance.
|
// Move the cursor a small distance.
|
||||||
// Nothing is added to the navigation history.
|
// Nothing is added to the navigation history.
|
||||||
|
@ -6604,21 +6615,21 @@ mod tests {
|
||||||
editor.change_selections(None, cx, |s| {
|
editor.change_selections(None, cx, |s| {
|
||||||
s.select_display_ranges([DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)])
|
s.select_display_ranges([DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)])
|
||||||
});
|
});
|
||||||
assert!(nav_history.borrow_mut().pop_backward().is_none());
|
assert!(pop_history(&mut editor, cx).is_none());
|
||||||
|
|
||||||
// Move the cursor a large distance.
|
// Move the cursor a large distance.
|
||||||
// The history can jump back to the previous position.
|
// The history can jump back to the previous position.
|
||||||
editor.change_selections(None, cx, |s| {
|
editor.change_selections(None, cx, |s| {
|
||||||
s.select_display_ranges([DisplayPoint::new(13, 0)..DisplayPoint::new(13, 3)])
|
s.select_display_ranges([DisplayPoint::new(13, 0)..DisplayPoint::new(13, 3)])
|
||||||
});
|
});
|
||||||
let nav_entry = nav_history.borrow_mut().pop_backward().unwrap();
|
let nav_entry = pop_history(&mut editor, cx).unwrap();
|
||||||
editor.navigate(nav_entry.data.unwrap(), cx);
|
editor.navigate(nav_entry.data.unwrap(), cx);
|
||||||
assert_eq!(nav_entry.item.id(), cx.view_id());
|
assert_eq!(nav_entry.item.id(), cx.view_id());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
editor.selections.display_ranges(cx),
|
editor.selections.display_ranges(cx),
|
||||||
&[DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)]
|
&[DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)]
|
||||||
);
|
);
|
||||||
assert!(nav_history.borrow_mut().pop_backward().is_none());
|
assert!(pop_history(&mut editor, cx).is_none());
|
||||||
|
|
||||||
// Move the cursor a small distance via the mouse.
|
// Move the cursor a small distance via the mouse.
|
||||||
// Nothing is added to the navigation history.
|
// Nothing is added to the navigation history.
|
||||||
|
@ -6628,7 +6639,7 @@ mod tests {
|
||||||
editor.selections.display_ranges(cx),
|
editor.selections.display_ranges(cx),
|
||||||
&[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)]
|
&[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)]
|
||||||
);
|
);
|
||||||
assert!(nav_history.borrow_mut().pop_backward().is_none());
|
assert!(pop_history(&mut editor, cx).is_none());
|
||||||
|
|
||||||
// Move the cursor a large distance via the mouse.
|
// Move the cursor a large distance via the mouse.
|
||||||
// The history can jump back to the previous position.
|
// The history can jump back to the previous position.
|
||||||
|
@ -6638,14 +6649,14 @@ mod tests {
|
||||||
editor.selections.display_ranges(cx),
|
editor.selections.display_ranges(cx),
|
||||||
&[DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)]
|
&[DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)]
|
||||||
);
|
);
|
||||||
let nav_entry = nav_history.borrow_mut().pop_backward().unwrap();
|
let nav_entry = pop_history(&mut editor, cx).unwrap();
|
||||||
editor.navigate(nav_entry.data.unwrap(), cx);
|
editor.navigate(nav_entry.data.unwrap(), cx);
|
||||||
assert_eq!(nav_entry.item.id(), cx.view_id());
|
assert_eq!(nav_entry.item.id(), cx.view_id());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
editor.selections.display_ranges(cx),
|
editor.selections.display_ranges(cx),
|
||||||
&[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)]
|
&[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)]
|
||||||
);
|
);
|
||||||
assert!(nav_history.borrow_mut().pop_backward().is_none());
|
assert!(pop_history(&mut editor, cx).is_none());
|
||||||
|
|
||||||
// Set scroll position to check later
|
// Set scroll position to check later
|
||||||
editor.set_scroll_position(Vector2F::new(5.5, 5.5), cx);
|
editor.set_scroll_position(Vector2F::new(5.5, 5.5), cx);
|
||||||
|
@ -6658,7 +6669,7 @@ mod tests {
|
||||||
assert_ne!(editor.scroll_position, original_scroll_position);
|
assert_ne!(editor.scroll_position, original_scroll_position);
|
||||||
assert_ne!(editor.scroll_top_anchor, original_scroll_top_anchor);
|
assert_ne!(editor.scroll_top_anchor, original_scroll_top_anchor);
|
||||||
|
|
||||||
let nav_entry = nav_history.borrow_mut().pop_backward().unwrap();
|
let nav_entry = pop_history(&mut editor, cx).unwrap();
|
||||||
editor.navigate(nav_entry.data.unwrap(), cx);
|
editor.navigate(nav_entry.data.unwrap(), cx);
|
||||||
assert_eq!(editor.scroll_position, original_scroll_position);
|
assert_eq!(editor.scroll_position, original_scroll_position);
|
||||||
assert_eq!(editor.scroll_top_anchor, original_scroll_top_anchor);
|
assert_eq!(editor.scroll_top_anchor, original_scroll_top_anchor);
|
||||||
|
@ -8221,7 +8232,7 @@ mod tests {
|
||||||
fox ju|mps over
|
fox ju|mps over
|
||||||
the lazy dog"});
|
the lazy dog"});
|
||||||
cx.update_editor(|e, cx| e.copy(&Copy, cx));
|
cx.update_editor(|e, cx| e.copy(&Copy, cx));
|
||||||
cx.assert_clipboard_content(Some("fox jumps over\n"));
|
cx.cx.assert_clipboard_content(Some("fox jumps over\n"));
|
||||||
|
|
||||||
// Paste with three selections, noticing how the copied full-line selection is inserted
|
// Paste with three selections, noticing how the copied full-line selection is inserted
|
||||||
// before the empty selections but replaces the selection that is non-empty.
|
// before the empty selections but replaces the selection that is non-empty.
|
||||||
|
|
|
@ -23,8 +23,9 @@ use gpui::{
|
||||||
json::{self, ToJson},
|
json::{self, ToJson},
|
||||||
platform::CursorStyle,
|
platform::CursorStyle,
|
||||||
text_layout::{self, Line, RunStyle, TextLayoutCache},
|
text_layout::{self, Line, RunStyle, TextLayoutCache},
|
||||||
AppContext, Axis, Border, CursorRegion, Element, ElementBox, Event, EventContext,
|
AppContext, Axis, Border, CursorRegion, Element, ElementBox, Event, EventContext, KeyDownEvent,
|
||||||
LayoutContext, MutableAppContext, PaintContext, Quad, Scene, SizeConstraint, ViewContext,
|
LayoutContext, ModifiersChangedEvent, MouseButton, MouseEvent, MouseMovedEvent,
|
||||||
|
MutableAppContext, PaintContext, Quad, Scene, ScrollWheelEvent, SizeConstraint, ViewContext,
|
||||||
WeakViewHandle,
|
WeakViewHandle,
|
||||||
};
|
};
|
||||||
use json::json;
|
use json::json;
|
||||||
|
@ -1463,14 +1464,15 @@ impl Element for EditorElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
match event {
|
match event {
|
||||||
Event::LeftMouseDown {
|
Event::MouseDown(MouseEvent {
|
||||||
|
button: MouseButton::Left,
|
||||||
position,
|
position,
|
||||||
cmd,
|
cmd,
|
||||||
alt,
|
alt,
|
||||||
shift,
|
shift,
|
||||||
click_count,
|
click_count,
|
||||||
..
|
..
|
||||||
} => self.mouse_down(
|
}) => self.mouse_down(
|
||||||
*position,
|
*position,
|
||||||
*cmd,
|
*cmd,
|
||||||
*alt,
|
*alt,
|
||||||
|
@ -1480,18 +1482,26 @@ impl Element for EditorElement {
|
||||||
paint,
|
paint,
|
||||||
cx,
|
cx,
|
||||||
),
|
),
|
||||||
Event::LeftMouseUp { position, .. } => self.mouse_up(*position, cx),
|
Event::MouseUp(MouseEvent {
|
||||||
Event::LeftMouseDragged { position, .. } => {
|
button: MouseButton::Left,
|
||||||
self.mouse_dragged(*position, layout, paint, cx)
|
position,
|
||||||
}
|
..
|
||||||
Event::ScrollWheel {
|
}) => self.mouse_up(*position, cx),
|
||||||
|
Event::MouseMoved(MouseMovedEvent {
|
||||||
|
pressed_button: Some(MouseButton::Left),
|
||||||
|
position,
|
||||||
|
..
|
||||||
|
}) => self.mouse_dragged(*position, layout, paint, cx),
|
||||||
|
Event::ScrollWheel(ScrollWheelEvent {
|
||||||
position,
|
position,
|
||||||
delta,
|
delta,
|
||||||
precise,
|
precise,
|
||||||
} => self.scroll(*position, *delta, *precise, layout, paint, cx),
|
}) => self.scroll(*position, *delta, *precise, layout, paint, cx),
|
||||||
Event::KeyDown { input, .. } => self.key_down(input.as_deref(), cx),
|
Event::KeyDown(KeyDownEvent { input, .. }) => self.key_down(input.as_deref(), cx),
|
||||||
Event::ModifiersChanged { cmd, .. } => self.modifiers_changed(*cmd, cx),
|
Event::ModifiersChanged(ModifiersChangedEvent { cmd, .. }) => {
|
||||||
Event::MouseMoved { position, cmd, .. } => {
|
self.modifiers_changed(*cmd, cx)
|
||||||
|
}
|
||||||
|
Event::MouseMoved(MouseMovedEvent { position, cmd, .. }) => {
|
||||||
self.mouse_moved(*position, *cmd, layout, paint, cx)
|
self.mouse_moved(*position, *cmd, layout, paint, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1685,22 +1695,22 @@ impl Cursor {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct HighlightedRange {
|
pub struct HighlightedRange {
|
||||||
start_y: f32,
|
pub start_y: f32,
|
||||||
line_height: f32,
|
pub line_height: f32,
|
||||||
lines: Vec<HighlightedRangeLine>,
|
pub lines: Vec<HighlightedRangeLine>,
|
||||||
color: Color,
|
pub color: Color,
|
||||||
corner_radius: f32,
|
pub corner_radius: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct HighlightedRangeLine {
|
pub struct HighlightedRangeLine {
|
||||||
start_x: f32,
|
pub start_x: f32,
|
||||||
end_x: f32,
|
pub end_x: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HighlightedRange {
|
impl HighlightedRange {
|
||||||
fn paint(&self, bounds: RectF, scene: &mut Scene) {
|
pub fn paint(&self, bounds: RectF, scene: &mut Scene) {
|
||||||
if self.lines.len() >= 2 && self.lines[0].start_x > self.lines[1].end_x {
|
if self.lines.len() >= 2 && self.lines[0].start_x > self.lines[1].end_x {
|
||||||
self.paint_lines(self.start_y, &self.lines[0..1], bounds, scene);
|
self.paint_lines(self.start_y, &self.lines[0..1], bounds, scene);
|
||||||
self.paint_lines(
|
self.paint_lines(
|
||||||
|
|
|
@ -352,13 +352,8 @@ impl Item for Editor {
|
||||||
project: ModelHandle<Project>,
|
project: ModelHandle<Project>,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) -> Task<Result<()>> {
|
) -> Task<Result<()>> {
|
||||||
let settings = cx.global::<Settings>();
|
|
||||||
let buffer = self.buffer().clone();
|
let buffer = self.buffer().clone();
|
||||||
let mut buffers = buffer.read(cx).all_buffers();
|
let buffers = buffer.read(cx).all_buffers();
|
||||||
buffers.retain(|buffer| {
|
|
||||||
let language_name = buffer.read(cx).language().map(|l| l.name());
|
|
||||||
settings.format_on_save(language_name.as_deref())
|
|
||||||
});
|
|
||||||
let mut timeout = cx.background().timer(FORMAT_TIMEOUT).fuse();
|
let mut timeout = cx.background().timer(FORMAT_TIMEOUT).fuse();
|
||||||
let format = project.update(cx, |project, cx| project.format(buffers, true, cx));
|
let format = project.update(cx, |project, cx| project.format(buffers, true, cx));
|
||||||
cx.spawn(|this, mut cx| async move {
|
cx.spawn(|this, mut cx| async move {
|
||||||
|
|
|
@ -404,14 +404,6 @@ impl<'a> EditorTestContext<'a> {
|
||||||
|
|
||||||
editor_text_with_selections
|
editor_text_with_selections
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn assert_clipboard_content(&mut self, expected_content: Option<&str>) {
|
|
||||||
self.cx.update(|cx| {
|
|
||||||
let actual_content = cx.read_from_clipboard().map(|item| item.text().to_owned());
|
|
||||||
let expected_content = expected_content.map(|content| content.to_owned());
|
|
||||||
assert_eq!(actual_content, expected_content);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Deref for EditorTestContext<'a> {
|
impl<'a> Deref for EditorTestContext<'a> {
|
||||||
|
|
|
@ -4,7 +4,7 @@ use crate::{
|
||||||
elements::ElementBox,
|
elements::ElementBox,
|
||||||
executor::{self, Task},
|
executor::{self, Task},
|
||||||
keymap::{self, Binding, Keystroke},
|
keymap::{self, Binding, Keystroke},
|
||||||
platform::{self, Platform, PromptLevel, WindowOptions},
|
platform::{self, KeyDownEvent, Platform, PromptLevel, WindowOptions},
|
||||||
presenter::Presenter,
|
presenter::Presenter,
|
||||||
util::post_inc,
|
util::post_inc,
|
||||||
AssetCache, AssetSource, ClipboardItem, FontCache, MouseRegionId, PathPromptOptions,
|
AssetCache, AssetSource, ClipboardItem, FontCache, MouseRegionId, PathPromptOptions,
|
||||||
|
@ -151,6 +151,7 @@ pub struct AsyncAppContext(Rc<RefCell<MutableAppContext>>);
|
||||||
pub struct TestAppContext {
|
pub struct TestAppContext {
|
||||||
cx: Rc<RefCell<MutableAppContext>>,
|
cx: Rc<RefCell<MutableAppContext>>,
|
||||||
foreground_platform: Rc<platform::test::ForegroundPlatform>,
|
foreground_platform: Rc<platform::test::ForegroundPlatform>,
|
||||||
|
condition_duration: Option<Duration>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
|
@ -337,6 +338,7 @@ impl TestAppContext {
|
||||||
let cx = TestAppContext {
|
let cx = TestAppContext {
|
||||||
cx: Rc::new(RefCell::new(cx)),
|
cx: Rc::new(RefCell::new(cx)),
|
||||||
foreground_platform,
|
foreground_platform,
|
||||||
|
condition_duration: None,
|
||||||
};
|
};
|
||||||
cx.cx.borrow_mut().weak_self = Some(Rc::downgrade(&cx.cx));
|
cx.cx.borrow_mut().weak_self = Some(Rc::downgrade(&cx.cx));
|
||||||
cx
|
cx
|
||||||
|
@ -377,11 +379,11 @@ impl TestAppContext {
|
||||||
|
|
||||||
if !cx.dispatch_keystroke(window_id, dispatch_path, &keystroke) {
|
if !cx.dispatch_keystroke(window_id, dispatch_path, &keystroke) {
|
||||||
presenter.borrow_mut().dispatch_event(
|
presenter.borrow_mut().dispatch_event(
|
||||||
Event::KeyDown {
|
Event::KeyDown(KeyDownEvent {
|
||||||
keystroke,
|
keystroke,
|
||||||
input,
|
input,
|
||||||
is_held,
|
is_held,
|
||||||
},
|
}),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -612,6 +614,27 @@ impl TestAppContext {
|
||||||
test_window
|
test_window
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_condition_duration(&mut self, duration: Duration) {
|
||||||
|
self.condition_duration = Some(duration);
|
||||||
|
}
|
||||||
|
pub fn condition_duration(&self) -> Duration {
|
||||||
|
self.condition_duration.unwrap_or_else(|| {
|
||||||
|
if std::env::var("CI").is_ok() {
|
||||||
|
Duration::from_secs(2)
|
||||||
|
} else {
|
||||||
|
Duration::from_millis(500)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn assert_clipboard_content(&mut self, expected_content: Option<&str>) {
|
||||||
|
self.update(|cx| {
|
||||||
|
let actual_content = cx.read_from_clipboard().map(|item| item.text().to_owned());
|
||||||
|
let expected_content = expected_content.map(|content| content.to_owned());
|
||||||
|
assert_eq!(actual_content, expected_content);
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsyncAppContext {
|
impl AsyncAppContext {
|
||||||
|
@ -1820,7 +1843,7 @@ impl MutableAppContext {
|
||||||
window.on_event(Box::new(move |event| {
|
window.on_event(Box::new(move |event| {
|
||||||
app.update(|cx| {
|
app.update(|cx| {
|
||||||
if let Some(presenter) = presenter.upgrade() {
|
if let Some(presenter) = presenter.upgrade() {
|
||||||
if let Event::KeyDown { keystroke, .. } = &event {
|
if let Event::KeyDown(KeyDownEvent { keystroke, .. }) = &event {
|
||||||
if cx.dispatch_keystroke(
|
if cx.dispatch_keystroke(
|
||||||
window_id,
|
window_id,
|
||||||
presenter.borrow().dispatch_path(cx.as_ref()),
|
presenter.borrow().dispatch_path(cx.as_ref()),
|
||||||
|
@ -4424,6 +4447,7 @@ impl<T: View> ViewHandle<T> {
|
||||||
use postage::prelude::{Sink as _, Stream as _};
|
use postage::prelude::{Sink as _, Stream as _};
|
||||||
|
|
||||||
let (tx, mut rx) = postage::mpsc::channel(1024);
|
let (tx, mut rx) = postage::mpsc::channel(1024);
|
||||||
|
let timeout_duration = cx.condition_duration();
|
||||||
|
|
||||||
let mut cx = cx.cx.borrow_mut();
|
let mut cx = cx.cx.borrow_mut();
|
||||||
let subscriptions = self.update(&mut *cx, |_, cx| {
|
let subscriptions = self.update(&mut *cx, |_, cx| {
|
||||||
|
@ -4445,14 +4469,9 @@ impl<T: View> ViewHandle<T> {
|
||||||
|
|
||||||
let cx = cx.weak_self.as_ref().unwrap().upgrade().unwrap();
|
let cx = cx.weak_self.as_ref().unwrap().upgrade().unwrap();
|
||||||
let handle = self.downgrade();
|
let handle = self.downgrade();
|
||||||
let duration = if std::env::var("CI").is_ok() {
|
|
||||||
Duration::from_secs(2)
|
|
||||||
} else {
|
|
||||||
Duration::from_millis(500)
|
|
||||||
};
|
|
||||||
|
|
||||||
async move {
|
async move {
|
||||||
crate::util::timeout(duration, async move {
|
crate::util::timeout(timeout_duration, async move {
|
||||||
loop {
|
loop {
|
||||||
{
|
{
|
||||||
let cx = cx.borrow();
|
let cx = cx.borrow();
|
||||||
|
@ -5381,7 +5400,7 @@ impl RefCounts {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{actions, elements::*, impl_actions};
|
use crate::{actions, elements::*, impl_actions, MouseButton, MouseEvent};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use smol::future::poll_once;
|
use smol::future::poll_once;
|
||||||
use std::{
|
use std::{
|
||||||
|
@ -5734,14 +5753,15 @@ mod tests {
|
||||||
let presenter = cx.presenters_and_platform_windows[&window_id].0.clone();
|
let presenter = cx.presenters_and_platform_windows[&window_id].0.clone();
|
||||||
// Ensure window's root element is in a valid lifecycle state.
|
// Ensure window's root element is in a valid lifecycle state.
|
||||||
presenter.borrow_mut().dispatch_event(
|
presenter.borrow_mut().dispatch_event(
|
||||||
Event::LeftMouseDown {
|
Event::MouseDown(MouseEvent {
|
||||||
position: Default::default(),
|
position: Default::default(),
|
||||||
|
button: MouseButton::Left,
|
||||||
ctrl: false,
|
ctrl: false,
|
||||||
alt: false,
|
alt: false,
|
||||||
shift: false,
|
shift: false,
|
||||||
cmd: false,
|
cmd: false,
|
||||||
click_count: 1,
|
click_count: 1,
|
||||||
},
|
}),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
assert_eq!(mouse_down_count.load(SeqCst), 1);
|
assert_eq!(mouse_down_count.load(SeqCst), 1);
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
geometry::vector::Vector2F, CursorRegion, DebugContext, Element, ElementBox, Event,
|
geometry::vector::Vector2F, CursorRegion, DebugContext, Element, ElementBox, Event,
|
||||||
EventContext, LayoutContext, MouseRegion, NavigationDirection, PaintContext, SizeConstraint,
|
EventContext, LayoutContext, MouseButton, MouseEvent, MouseRegion, NavigationDirection,
|
||||||
|
PaintContext, SizeConstraint,
|
||||||
};
|
};
|
||||||
use pathfinder_geometry::rect::RectF;
|
use pathfinder_geometry::rect::RectF;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
@ -90,7 +91,7 @@ impl Element for EventHandler {
|
||||||
click: Some(Rc::new(|_, _, _| {})),
|
click: Some(Rc::new(|_, _, _| {})),
|
||||||
right_mouse_down: Some(Rc::new(|_, _| {})),
|
right_mouse_down: Some(Rc::new(|_, _| {})),
|
||||||
right_click: Some(Rc::new(|_, _, _| {})),
|
right_click: Some(Rc::new(|_, _, _| {})),
|
||||||
drag: Some(Rc::new(|_, _| {})),
|
drag: Some(Rc::new(|_, _, _| {})),
|
||||||
mouse_down_out: Some(Rc::new(|_, _| {})),
|
mouse_down_out: Some(Rc::new(|_, _| {})),
|
||||||
right_mouse_down_out: Some(Rc::new(|_, _| {})),
|
right_mouse_down_out: Some(Rc::new(|_, _| {})),
|
||||||
});
|
});
|
||||||
|
@ -116,7 +117,11 @@ impl Element for EventHandler {
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
match event {
|
match event {
|
||||||
Event::LeftMouseDown { position, .. } => {
|
Event::MouseDown(MouseEvent {
|
||||||
|
button: MouseButton::Left,
|
||||||
|
position,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
if let Some(callback) = self.mouse_down.as_mut() {
|
if let Some(callback) = self.mouse_down.as_mut() {
|
||||||
if visible_bounds.contains_point(*position) {
|
if visible_bounds.contains_point(*position) {
|
||||||
return callback(cx);
|
return callback(cx);
|
||||||
|
@ -124,7 +129,11 @@ impl Element for EventHandler {
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
Event::RightMouseDown { position, .. } => {
|
Event::MouseDown(MouseEvent {
|
||||||
|
button: MouseButton::Right,
|
||||||
|
position,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
if let Some(callback) = self.right_mouse_down.as_mut() {
|
if let Some(callback) = self.right_mouse_down.as_mut() {
|
||||||
if visible_bounds.contains_point(*position) {
|
if visible_bounds.contains_point(*position) {
|
||||||
return callback(cx);
|
return callback(cx);
|
||||||
|
@ -132,11 +141,11 @@ impl Element for EventHandler {
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
Event::NavigateMouseDown {
|
Event::MouseDown(MouseEvent {
|
||||||
|
button: MouseButton::Navigate(direction),
|
||||||
position,
|
position,
|
||||||
direction,
|
|
||||||
..
|
..
|
||||||
} => {
|
}) => {
|
||||||
if let Some(callback) = self.navigate_mouse_down.as_mut() {
|
if let Some(callback) = self.navigate_mouse_down.as_mut() {
|
||||||
if visible_bounds.contains_point(*position) {
|
if visible_bounds.contains_point(*position) {
|
||||||
return callback(*direction, cx);
|
return callback(*direction, cx);
|
||||||
|
|
|
@ -3,7 +3,8 @@ use std::{any::Any, f32::INFINITY};
|
||||||
use crate::{
|
use crate::{
|
||||||
json::{self, ToJson, Value},
|
json::{self, ToJson, Value},
|
||||||
Axis, DebugContext, Element, ElementBox, ElementStateHandle, Event, EventContext,
|
Axis, DebugContext, Element, ElementBox, ElementStateHandle, Event, EventContext,
|
||||||
LayoutContext, PaintContext, RenderContext, SizeConstraint, Vector2FExt, View,
|
LayoutContext, MouseMovedEvent, PaintContext, RenderContext, ScrollWheelEvent, SizeConstraint,
|
||||||
|
Vector2FExt, View,
|
||||||
};
|
};
|
||||||
use pathfinder_geometry::{
|
use pathfinder_geometry::{
|
||||||
rect::RectF,
|
rect::RectF,
|
||||||
|
@ -287,11 +288,11 @@ impl Element for Flex {
|
||||||
handled = child.dispatch_event(event, cx) || handled;
|
handled = child.dispatch_event(event, cx) || handled;
|
||||||
}
|
}
|
||||||
if !handled {
|
if !handled {
|
||||||
if let &Event::ScrollWheel {
|
if let &Event::ScrollWheel(ScrollWheelEvent {
|
||||||
position,
|
position,
|
||||||
delta,
|
delta,
|
||||||
precise,
|
precise,
|
||||||
} = event
|
}) = event
|
||||||
{
|
{
|
||||||
if *remaining_space < 0. && bounds.contains_point(position) {
|
if *remaining_space < 0. && bounds.contains_point(position) {
|
||||||
if let Some(scroll_state) = self.scroll_state.as_ref() {
|
if let Some(scroll_state) = self.scroll_state.as_ref() {
|
||||||
|
@ -321,7 +322,7 @@ impl Element for Flex {
|
||||||
}
|
}
|
||||||
|
|
||||||
if !handled {
|
if !handled {
|
||||||
if let &Event::MouseMoved { position, .. } = event {
|
if let &Event::MouseMoved(MouseMovedEvent { position, .. }) = event {
|
||||||
// If this is a scrollable flex, and the mouse is over it, eat the scroll event to prevent
|
// If this is a scrollable flex, and the mouse is over it, eat the scroll event to prevent
|
||||||
// propogating it to the element below.
|
// propogating it to the element below.
|
||||||
if self.scroll_state.is_some() && bounds.contains_point(position) {
|
if self.scroll_state.is_some() && bounds.contains_point(position) {
|
||||||
|
|
|
@ -5,7 +5,7 @@ use crate::{
|
||||||
},
|
},
|
||||||
json::json,
|
json::json,
|
||||||
DebugContext, Element, ElementBox, ElementRc, Event, EventContext, LayoutContext, PaintContext,
|
DebugContext, Element, ElementBox, ElementRc, Event, EventContext, LayoutContext, PaintContext,
|
||||||
RenderContext, SizeConstraint, View, ViewContext,
|
RenderContext, ScrollWheelEvent, SizeConstraint, View, ViewContext,
|
||||||
};
|
};
|
||||||
use std::{cell::RefCell, collections::VecDeque, ops::Range, rc::Rc};
|
use std::{cell::RefCell, collections::VecDeque, ops::Range, rc::Rc};
|
||||||
use sum_tree::{Bias, SumTree};
|
use sum_tree::{Bias, SumTree};
|
||||||
|
@ -311,11 +311,11 @@ impl Element for List {
|
||||||
state.items = new_items;
|
state.items = new_items;
|
||||||
|
|
||||||
match event {
|
match event {
|
||||||
Event::ScrollWheel {
|
Event::ScrollWheel(ScrollWheelEvent {
|
||||||
position,
|
position,
|
||||||
delta,
|
delta,
|
||||||
precise,
|
precise,
|
||||||
} => {
|
}) => {
|
||||||
if bounds.contains_point(*position) {
|
if bounds.contains_point(*position) {
|
||||||
if state.scroll(scroll_top, bounds.height(), *delta, *precise, cx) {
|
if state.scroll(scroll_top, bounds.height(), *delta, *precise, cx) {
|
||||||
handled = true;
|
handled = true;
|
||||||
|
|
|
@ -24,7 +24,7 @@ pub struct MouseEventHandler {
|
||||||
right_click: Option<Rc<dyn Fn(Vector2F, usize, &mut EventContext)>>,
|
right_click: Option<Rc<dyn Fn(Vector2F, usize, &mut EventContext)>>,
|
||||||
mouse_down_out: Option<Rc<dyn Fn(Vector2F, &mut EventContext)>>,
|
mouse_down_out: Option<Rc<dyn Fn(Vector2F, &mut EventContext)>>,
|
||||||
right_mouse_down_out: Option<Rc<dyn Fn(Vector2F, &mut EventContext)>>,
|
right_mouse_down_out: Option<Rc<dyn Fn(Vector2F, &mut EventContext)>>,
|
||||||
drag: Option<Rc<dyn Fn(Vector2F, &mut EventContext)>>,
|
drag: Option<Rc<dyn Fn(Vector2F, Vector2F, &mut EventContext)>>,
|
||||||
hover: Option<Rc<dyn Fn(Vector2F, bool, &mut EventContext)>>,
|
hover: Option<Rc<dyn Fn(Vector2F, bool, &mut EventContext)>>,
|
||||||
padding: Padding,
|
padding: Padding,
|
||||||
}
|
}
|
||||||
|
@ -106,7 +106,10 @@ impl MouseEventHandler {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_drag(mut self, handler: impl Fn(Vector2F, &mut EventContext) + 'static) -> Self {
|
pub fn on_drag(
|
||||||
|
mut self,
|
||||||
|
handler: impl Fn(Vector2F, Vector2F, &mut EventContext) + 'static,
|
||||||
|
) -> Self {
|
||||||
self.drag = Some(Rc::new(handler));
|
self.drag = Some(Rc::new(handler));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ use crate::{
|
||||||
vector::{vec2f, Vector2F},
|
vector::{vec2f, Vector2F},
|
||||||
},
|
},
|
||||||
json::{self, json},
|
json::{self, json},
|
||||||
ElementBox, RenderContext, View,
|
ElementBox, RenderContext, ScrollWheelEvent, View,
|
||||||
};
|
};
|
||||||
use json::ToJson;
|
use json::ToJson;
|
||||||
use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
|
use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
|
||||||
|
@ -310,11 +310,11 @@ impl Element for UniformList {
|
||||||
}
|
}
|
||||||
|
|
||||||
match event {
|
match event {
|
||||||
Event::ScrollWheel {
|
Event::ScrollWheel(ScrollWheelEvent {
|
||||||
position,
|
position,
|
||||||
delta,
|
delta,
|
||||||
precise,
|
precise,
|
||||||
} => {
|
}) => {
|
||||||
if bounds.contains_point(*position) {
|
if bounds.contains_point(*position) {
|
||||||
if self.scroll(*position, *delta, *precise, layout.scroll_max, cx) {
|
if self.scroll(*position, *delta, *precise, layout.scroll_max, cx) {
|
||||||
handled = true;
|
handled = true;
|
||||||
|
|
|
@ -28,8 +28,7 @@ pub mod json;
|
||||||
pub mod keymap;
|
pub mod keymap;
|
||||||
pub mod platform;
|
pub mod platform;
|
||||||
pub use gpui_macros::test;
|
pub use gpui_macros::test;
|
||||||
pub use platform::FontSystem;
|
pub use platform::*;
|
||||||
pub use platform::{Event, NavigationDirection, PathPromptOptions, Platform, PromptLevel};
|
|
||||||
pub use presenter::{
|
pub use presenter::{
|
||||||
Axis, DebugContext, EventContext, LayoutContext, PaintContext, SizeConstraint, Vector2FExt,
|
Axis, DebugContext, EventContext, LayoutContext, PaintContext, SizeConstraint, Vector2FExt,
|
||||||
};
|
};
|
||||||
|
|
|
@ -20,7 +20,7 @@ use crate::{
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use async_task::Runnable;
|
use async_task::Runnable;
|
||||||
pub use event::{Event, NavigationDirection};
|
pub use event::*;
|
||||||
use postage::oneshot;
|
use postage::oneshot;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::{
|
use std::{
|
||||||
|
|
|
@ -1,85 +1,77 @@
|
||||||
use crate::{geometry::vector::Vector2F, keymap::Keystroke};
|
use crate::{geometry::vector::Vector2F, keymap::Keystroke};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct KeyDownEvent {
|
||||||
|
pub keystroke: Keystroke,
|
||||||
|
pub input: Option<String>,
|
||||||
|
pub is_held: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct KeyUpEvent {
|
||||||
|
pub keystroke: Keystroke,
|
||||||
|
pub input: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct ModifiersChangedEvent {
|
||||||
|
pub ctrl: bool,
|
||||||
|
pub alt: bool,
|
||||||
|
pub shift: bool,
|
||||||
|
pub cmd: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct ScrollWheelEvent {
|
||||||
|
pub position: Vector2F,
|
||||||
|
pub delta: Vector2F,
|
||||||
|
pub precise: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
pub enum NavigationDirection {
|
pub enum NavigationDirection {
|
||||||
Back,
|
Back,
|
||||||
Forward,
|
Forward,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
pub enum MouseButton {
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
Middle,
|
||||||
|
Navigate(NavigationDirection),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct MouseEvent {
|
||||||
|
pub button: MouseButton,
|
||||||
|
pub position: Vector2F,
|
||||||
|
pub ctrl: bool,
|
||||||
|
pub alt: bool,
|
||||||
|
pub shift: bool,
|
||||||
|
pub cmd: bool,
|
||||||
|
pub click_count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct MouseMovedEvent {
|
||||||
|
pub position: Vector2F,
|
||||||
|
pub pressed_button: Option<MouseButton>,
|
||||||
|
pub ctrl: bool,
|
||||||
|
pub cmd: bool,
|
||||||
|
pub alt: bool,
|
||||||
|
pub shift: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum Event {
|
pub enum Event {
|
||||||
KeyDown {
|
KeyDown(KeyDownEvent),
|
||||||
keystroke: Keystroke,
|
KeyUp(KeyUpEvent),
|
||||||
input: Option<String>,
|
ModifiersChanged(ModifiersChangedEvent),
|
||||||
is_held: bool,
|
MouseDown(MouseEvent),
|
||||||
},
|
MouseUp(MouseEvent),
|
||||||
KeyUp {
|
MouseMoved(MouseMovedEvent),
|
||||||
keystroke: Keystroke,
|
ScrollWheel(ScrollWheelEvent),
|
||||||
input: Option<String>,
|
|
||||||
},
|
|
||||||
ModifiersChanged {
|
|
||||||
ctrl: bool,
|
|
||||||
alt: bool,
|
|
||||||
shift: bool,
|
|
||||||
cmd: bool,
|
|
||||||
},
|
|
||||||
ScrollWheel {
|
|
||||||
position: Vector2F,
|
|
||||||
delta: Vector2F,
|
|
||||||
precise: bool,
|
|
||||||
},
|
|
||||||
LeftMouseDown {
|
|
||||||
position: Vector2F,
|
|
||||||
ctrl: bool,
|
|
||||||
alt: bool,
|
|
||||||
shift: bool,
|
|
||||||
cmd: bool,
|
|
||||||
click_count: usize,
|
|
||||||
},
|
|
||||||
LeftMouseUp {
|
|
||||||
position: Vector2F,
|
|
||||||
click_count: usize,
|
|
||||||
},
|
|
||||||
LeftMouseDragged {
|
|
||||||
position: Vector2F,
|
|
||||||
ctrl: bool,
|
|
||||||
alt: bool,
|
|
||||||
shift: bool,
|
|
||||||
cmd: bool,
|
|
||||||
},
|
|
||||||
RightMouseDown {
|
|
||||||
position: Vector2F,
|
|
||||||
ctrl: bool,
|
|
||||||
alt: bool,
|
|
||||||
shift: bool,
|
|
||||||
cmd: bool,
|
|
||||||
click_count: usize,
|
|
||||||
},
|
|
||||||
RightMouseUp {
|
|
||||||
position: Vector2F,
|
|
||||||
click_count: usize,
|
|
||||||
},
|
|
||||||
NavigateMouseDown {
|
|
||||||
position: Vector2F,
|
|
||||||
direction: NavigationDirection,
|
|
||||||
ctrl: bool,
|
|
||||||
alt: bool,
|
|
||||||
shift: bool,
|
|
||||||
cmd: bool,
|
|
||||||
click_count: usize,
|
|
||||||
},
|
|
||||||
NavigateMouseUp {
|
|
||||||
position: Vector2F,
|
|
||||||
direction: NavigationDirection,
|
|
||||||
},
|
|
||||||
MouseMoved {
|
|
||||||
position: Vector2F,
|
|
||||||
left_mouse_down: bool,
|
|
||||||
ctrl: bool,
|
|
||||||
cmd: bool,
|
|
||||||
alt: bool,
|
|
||||||
shift: bool,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Event {
|
impl Event {
|
||||||
|
@ -88,15 +80,9 @@ impl Event {
|
||||||
Event::KeyDown { .. } => None,
|
Event::KeyDown { .. } => None,
|
||||||
Event::KeyUp { .. } => None,
|
Event::KeyUp { .. } => None,
|
||||||
Event::ModifiersChanged { .. } => None,
|
Event::ModifiersChanged { .. } => None,
|
||||||
Event::ScrollWheel { position, .. }
|
Event::MouseDown(event) | Event::MouseUp(event) => Some(event.position),
|
||||||
| Event::LeftMouseDown { position, .. }
|
Event::MouseMoved(event) => Some(event.position),
|
||||||
| Event::LeftMouseUp { position, .. }
|
Event::ScrollWheel(event) => Some(event.position),
|
||||||
| Event::LeftMouseDragged { position, .. }
|
|
||||||
| Event::RightMouseDown { position, .. }
|
|
||||||
| Event::RightMouseUp { position, .. }
|
|
||||||
| Event::NavigateMouseDown { position, .. }
|
|
||||||
| Event::NavigateMouseUp { position, .. }
|
|
||||||
| Event::MouseMoved { position, .. } => Some(*position),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,10 +2,12 @@ use crate::{
|
||||||
geometry::vector::vec2f,
|
geometry::vector::vec2f,
|
||||||
keymap::Keystroke,
|
keymap::Keystroke,
|
||||||
platform::{Event, NavigationDirection},
|
platform::{Event, NavigationDirection},
|
||||||
|
KeyDownEvent, KeyUpEvent, ModifiersChangedEvent, MouseButton, MouseEvent, MouseMovedEvent,
|
||||||
|
ScrollWheelEvent,
|
||||||
};
|
};
|
||||||
use cocoa::{
|
use cocoa::{
|
||||||
appkit::{NSEvent, NSEventModifierFlags, NSEventType},
|
appkit::{NSEvent, NSEventModifierFlags, NSEventType},
|
||||||
base::{id, nil, YES},
|
base::{id, YES},
|
||||||
foundation::NSString as _,
|
foundation::NSString as _,
|
||||||
};
|
};
|
||||||
use std::{borrow::Cow, ffi::CStr, os::raw::c_char};
|
use std::{borrow::Cow, ffi::CStr, os::raw::c_char};
|
||||||
|
@ -59,12 +61,12 @@ impl Event {
|
||||||
let shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask);
|
let shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask);
|
||||||
let cmd = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask);
|
let cmd = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask);
|
||||||
|
|
||||||
Some(Self::ModifiersChanged {
|
Some(Self::ModifiersChanged(ModifiersChangedEvent {
|
||||||
ctrl,
|
ctrl,
|
||||||
alt,
|
alt,
|
||||||
shift,
|
shift,
|
||||||
cmd,
|
cmd,
|
||||||
})
|
}))
|
||||||
}
|
}
|
||||||
NSEventType::NSKeyDown => {
|
NSEventType::NSKeyDown => {
|
||||||
let modifiers = native_event.modifierFlags();
|
let modifiers = native_event.modifierFlags();
|
||||||
|
@ -76,7 +78,7 @@ impl Event {
|
||||||
|
|
||||||
let (unmodified_chars, input) = get_key_text(native_event, cmd, ctrl, function)?;
|
let (unmodified_chars, input) = get_key_text(native_event, cmd, ctrl, function)?;
|
||||||
|
|
||||||
Some(Self::KeyDown {
|
Some(Self::KeyDown(KeyDownEvent {
|
||||||
keystroke: Keystroke {
|
keystroke: Keystroke {
|
||||||
ctrl,
|
ctrl,
|
||||||
alt,
|
alt,
|
||||||
|
@ -86,7 +88,7 @@ impl Event {
|
||||||
},
|
},
|
||||||
input,
|
input,
|
||||||
is_held: native_event.isARepeat() == YES,
|
is_held: native_event.isARepeat() == YES,
|
||||||
})
|
}))
|
||||||
}
|
}
|
||||||
NSEventType::NSKeyUp => {
|
NSEventType::NSKeyUp => {
|
||||||
let modifiers = native_event.modifierFlags();
|
let modifiers = native_event.modifierFlags();
|
||||||
|
@ -98,7 +100,7 @@ impl Event {
|
||||||
|
|
||||||
let (unmodified_chars, input) = get_key_text(native_event, cmd, ctrl, function)?;
|
let (unmodified_chars, input) = get_key_text(native_event, cmd, ctrl, function)?;
|
||||||
|
|
||||||
Some(Self::KeyUp {
|
Some(Self::KeyUp(KeyUpEvent {
|
||||||
keystroke: Keystroke {
|
keystroke: Keystroke {
|
||||||
ctrl,
|
ctrl,
|
||||||
alt,
|
alt,
|
||||||
|
@ -107,125 +109,120 @@ impl Event {
|
||||||
key: unmodified_chars.into(),
|
key: unmodified_chars.into(),
|
||||||
},
|
},
|
||||||
input,
|
input,
|
||||||
})
|
}))
|
||||||
}
|
}
|
||||||
NSEventType::NSLeftMouseDown => {
|
NSEventType::NSLeftMouseDown
|
||||||
|
| NSEventType::NSRightMouseDown
|
||||||
|
| NSEventType::NSOtherMouseDown => {
|
||||||
|
let button = match native_event.buttonNumber() {
|
||||||
|
0 => MouseButton::Left,
|
||||||
|
1 => MouseButton::Right,
|
||||||
|
2 => MouseButton::Middle,
|
||||||
|
3 => MouseButton::Navigate(NavigationDirection::Back),
|
||||||
|
4 => MouseButton::Navigate(NavigationDirection::Forward),
|
||||||
|
// Other mouse buttons aren't tracked currently
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
let modifiers = native_event.modifierFlags();
|
let modifiers = native_event.modifierFlags();
|
||||||
window_height.map(|window_height| Self::LeftMouseDown {
|
|
||||||
position: vec2f(
|
window_height.map(|window_height| {
|
||||||
native_event.locationInWindow().x as f32,
|
Self::MouseDown(MouseEvent {
|
||||||
window_height - native_event.locationInWindow().y as f32,
|
button,
|
||||||
),
|
position: vec2f(
|
||||||
ctrl: modifiers.contains(NSEventModifierFlags::NSControlKeyMask),
|
native_event.locationInWindow().x as f32,
|
||||||
alt: modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask),
|
window_height - native_event.locationInWindow().y as f32,
|
||||||
shift: modifiers.contains(NSEventModifierFlags::NSShiftKeyMask),
|
),
|
||||||
cmd: modifiers.contains(NSEventModifierFlags::NSCommandKeyMask),
|
ctrl: modifiers.contains(NSEventModifierFlags::NSControlKeyMask),
|
||||||
click_count: native_event.clickCount() as usize,
|
alt: modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask),
|
||||||
|
shift: modifiers.contains(NSEventModifierFlags::NSShiftKeyMask),
|
||||||
|
cmd: modifiers.contains(NSEventModifierFlags::NSCommandKeyMask),
|
||||||
|
click_count: native_event.clickCount() as usize,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
NSEventType::NSLeftMouseUp => window_height.map(|window_height| Self::LeftMouseUp {
|
NSEventType::NSLeftMouseUp
|
||||||
position: vec2f(
|
| NSEventType::NSRightMouseUp
|
||||||
native_event.locationInWindow().x as f32,
|
| NSEventType::NSOtherMouseUp => {
|
||||||
window_height - native_event.locationInWindow().y as f32,
|
let button = match native_event.buttonNumber() {
|
||||||
),
|
0 => MouseButton::Left,
|
||||||
click_count: native_event.clickCount() as usize,
|
1 => MouseButton::Right,
|
||||||
}),
|
2 => MouseButton::Middle,
|
||||||
NSEventType::NSRightMouseDown => {
|
3 => MouseButton::Navigate(NavigationDirection::Back),
|
||||||
let modifiers = native_event.modifierFlags();
|
4 => MouseButton::Navigate(NavigationDirection::Forward),
|
||||||
window_height.map(|window_height| Self::RightMouseDown {
|
|
||||||
position: vec2f(
|
|
||||||
native_event.locationInWindow().x as f32,
|
|
||||||
window_height - native_event.locationInWindow().y as f32,
|
|
||||||
),
|
|
||||||
ctrl: modifiers.contains(NSEventModifierFlags::NSControlKeyMask),
|
|
||||||
alt: modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask),
|
|
||||||
shift: modifiers.contains(NSEventModifierFlags::NSShiftKeyMask),
|
|
||||||
cmd: modifiers.contains(NSEventModifierFlags::NSCommandKeyMask),
|
|
||||||
click_count: native_event.clickCount() as usize,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
NSEventType::NSRightMouseUp => window_height.map(|window_height| Self::RightMouseUp {
|
|
||||||
position: vec2f(
|
|
||||||
native_event.locationInWindow().x as f32,
|
|
||||||
window_height - native_event.locationInWindow().y as f32,
|
|
||||||
),
|
|
||||||
click_count: native_event.clickCount() as usize,
|
|
||||||
}),
|
|
||||||
NSEventType::NSOtherMouseDown => {
|
|
||||||
let direction = match native_event.buttonNumber() {
|
|
||||||
3 => NavigationDirection::Back,
|
|
||||||
4 => NavigationDirection::Forward,
|
|
||||||
// Other mouse buttons aren't tracked currently
|
// Other mouse buttons aren't tracked currently
|
||||||
_ => return None,
|
_ => return None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let modifiers = native_event.modifierFlags();
|
window_height.map(|window_height| {
|
||||||
window_height.map(|window_height| Self::NavigateMouseDown {
|
let modifiers = native_event.modifierFlags();
|
||||||
|
Self::MouseUp(MouseEvent {
|
||||||
|
button,
|
||||||
|
position: vec2f(
|
||||||
|
native_event.locationInWindow().x as f32,
|
||||||
|
window_height - native_event.locationInWindow().y as f32,
|
||||||
|
),
|
||||||
|
ctrl: modifiers.contains(NSEventModifierFlags::NSControlKeyMask),
|
||||||
|
alt: modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask),
|
||||||
|
shift: modifiers.contains(NSEventModifierFlags::NSShiftKeyMask),
|
||||||
|
cmd: modifiers.contains(NSEventModifierFlags::NSCommandKeyMask),
|
||||||
|
click_count: native_event.clickCount() as usize,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
NSEventType::NSScrollWheel => window_height.map(|window_height| {
|
||||||
|
Self::ScrollWheel(ScrollWheelEvent {
|
||||||
position: vec2f(
|
position: vec2f(
|
||||||
native_event.locationInWindow().x as f32,
|
native_event.locationInWindow().x as f32,
|
||||||
window_height - native_event.locationInWindow().y as f32,
|
window_height - native_event.locationInWindow().y as f32,
|
||||||
),
|
),
|
||||||
direction,
|
delta: vec2f(
|
||||||
ctrl: modifiers.contains(NSEventModifierFlags::NSControlKeyMask),
|
native_event.scrollingDeltaX() as f32,
|
||||||
alt: modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask),
|
native_event.scrollingDeltaY() as f32,
|
||||||
shift: modifiers.contains(NSEventModifierFlags::NSShiftKeyMask),
|
),
|
||||||
cmd: modifiers.contains(NSEventModifierFlags::NSCommandKeyMask),
|
precise: native_event.hasPreciseScrollingDeltas() == YES,
|
||||||
click_count: native_event.clickCount() as usize,
|
|
||||||
})
|
})
|
||||||
}
|
}),
|
||||||
NSEventType::NSOtherMouseUp => {
|
NSEventType::NSLeftMouseDragged
|
||||||
let direction = match native_event.buttonNumber() {
|
| NSEventType::NSRightMouseDragged
|
||||||
3 => NavigationDirection::Back,
|
| NSEventType::NSOtherMouseDragged => {
|
||||||
4 => NavigationDirection::Forward,
|
let pressed_button = match native_event.buttonNumber() {
|
||||||
|
0 => MouseButton::Left,
|
||||||
|
1 => MouseButton::Right,
|
||||||
|
2 => MouseButton::Middle,
|
||||||
|
3 => MouseButton::Navigate(NavigationDirection::Back),
|
||||||
|
4 => MouseButton::Navigate(NavigationDirection::Forward),
|
||||||
// Other mouse buttons aren't tracked currently
|
// Other mouse buttons aren't tracked currently
|
||||||
_ => return None,
|
_ => return None,
|
||||||
};
|
};
|
||||||
|
|
||||||
window_height.map(|window_height| Self::NavigateMouseUp {
|
window_height.map(|window_height| {
|
||||||
position: vec2f(
|
let modifiers = native_event.modifierFlags();
|
||||||
native_event.locationInWindow().x as f32,
|
Self::MouseMoved(MouseMovedEvent {
|
||||||
window_height - native_event.locationInWindow().y as f32,
|
pressed_button: Some(pressed_button),
|
||||||
),
|
position: vec2f(
|
||||||
direction,
|
native_event.locationInWindow().x as f32,
|
||||||
|
window_height - native_event.locationInWindow().y as f32,
|
||||||
|
),
|
||||||
|
ctrl: modifiers.contains(NSEventModifierFlags::NSControlKeyMask),
|
||||||
|
alt: modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask),
|
||||||
|
shift: modifiers.contains(NSEventModifierFlags::NSShiftKeyMask),
|
||||||
|
cmd: modifiers.contains(NSEventModifierFlags::NSCommandKeyMask),
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
NSEventType::NSLeftMouseDragged => window_height.map(|window_height| {
|
|
||||||
let modifiers = native_event.modifierFlags();
|
|
||||||
Self::LeftMouseDragged {
|
|
||||||
position: vec2f(
|
|
||||||
native_event.locationInWindow().x as f32,
|
|
||||||
window_height - native_event.locationInWindow().y as f32,
|
|
||||||
),
|
|
||||||
ctrl: modifiers.contains(NSEventModifierFlags::NSControlKeyMask),
|
|
||||||
alt: modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask),
|
|
||||||
shift: modifiers.contains(NSEventModifierFlags::NSShiftKeyMask),
|
|
||||||
cmd: modifiers.contains(NSEventModifierFlags::NSCommandKeyMask),
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
NSEventType::NSScrollWheel => window_height.map(|window_height| Self::ScrollWheel {
|
|
||||||
position: vec2f(
|
|
||||||
native_event.locationInWindow().x as f32,
|
|
||||||
window_height - native_event.locationInWindow().y as f32,
|
|
||||||
),
|
|
||||||
delta: vec2f(
|
|
||||||
native_event.scrollingDeltaX() as f32,
|
|
||||||
native_event.scrollingDeltaY() as f32,
|
|
||||||
),
|
|
||||||
precise: native_event.hasPreciseScrollingDeltas() == YES,
|
|
||||||
}),
|
|
||||||
NSEventType::NSMouseMoved => window_height.map(|window_height| {
|
NSEventType::NSMouseMoved => window_height.map(|window_height| {
|
||||||
let modifiers = native_event.modifierFlags();
|
let modifiers = native_event.modifierFlags();
|
||||||
Self::MouseMoved {
|
Self::MouseMoved(MouseMovedEvent {
|
||||||
position: vec2f(
|
position: vec2f(
|
||||||
native_event.locationInWindow().x as f32,
|
native_event.locationInWindow().x as f32,
|
||||||
window_height - native_event.locationInWindow().y as f32,
|
window_height - native_event.locationInWindow().y as f32,
|
||||||
),
|
),
|
||||||
left_mouse_down: NSEvent::pressedMouseButtons(nil) & 1 != 0,
|
pressed_button: None,
|
||||||
ctrl: modifiers.contains(NSEventModifierFlags::NSControlKeyMask),
|
ctrl: modifiers.contains(NSEventModifierFlags::NSControlKeyMask),
|
||||||
alt: modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask),
|
alt: modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask),
|
||||||
shift: modifiers.contains(NSEventModifierFlags::NSShiftKeyMask),
|
shift: modifiers.contains(NSEventModifierFlags::NSShiftKeyMask),
|
||||||
cmd: modifiers.contains(NSEventModifierFlags::NSCommandKeyMask),
|
cmd: modifiers.contains(NSEventModifierFlags::NSCommandKeyMask),
|
||||||
}
|
})
|
||||||
}),
|
}),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ use crate::{
|
||||||
},
|
},
|
||||||
keymap::Keystroke,
|
keymap::Keystroke,
|
||||||
platform::{self, Event, WindowBounds, WindowContext},
|
platform::{self, Event, WindowBounds, WindowContext},
|
||||||
Scene,
|
KeyDownEvent, ModifiersChangedEvent, MouseButton, MouseEvent, MouseMovedEvent, Scene,
|
||||||
};
|
};
|
||||||
use block::ConcreteBlock;
|
use block::ConcreteBlock;
|
||||||
use cocoa::{
|
use cocoa::{
|
||||||
|
@ -562,11 +562,11 @@ extern "C" fn handle_key_equivalent(this: &Object, _: Sel, native_event: id) ->
|
||||||
let event = unsafe { Event::from_native(native_event, Some(window_state_borrow.size().y())) };
|
let event = unsafe { Event::from_native(native_event, Some(window_state_borrow.size().y())) };
|
||||||
if let Some(event) = event {
|
if let Some(event) = event {
|
||||||
match &event {
|
match &event {
|
||||||
Event::KeyDown {
|
Event::KeyDown(KeyDownEvent {
|
||||||
keystroke,
|
keystroke,
|
||||||
input,
|
input,
|
||||||
is_held,
|
is_held,
|
||||||
} => {
|
}) => {
|
||||||
let keydown = (keystroke.clone(), input.clone());
|
let keydown = (keystroke.clone(), input.clone());
|
||||||
// Ignore events from held-down keys after some of the initially-pressed keys
|
// Ignore events from held-down keys after some of the initially-pressed keys
|
||||||
// were released.
|
// were released.
|
||||||
|
@ -603,33 +603,41 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
|
||||||
|
|
||||||
if let Some(event) = event {
|
if let Some(event) = event {
|
||||||
match &event {
|
match &event {
|
||||||
Event::LeftMouseDragged { position, .. } => {
|
Event::MouseMoved(
|
||||||
|
event @ MouseMovedEvent {
|
||||||
|
pressed_button: Some(_),
|
||||||
|
..
|
||||||
|
},
|
||||||
|
) => {
|
||||||
window_state_borrow.synthetic_drag_counter += 1;
|
window_state_borrow.synthetic_drag_counter += 1;
|
||||||
window_state_borrow
|
window_state_borrow
|
||||||
.executor
|
.executor
|
||||||
.spawn(synthetic_drag(
|
.spawn(synthetic_drag(
|
||||||
weak_window_state,
|
weak_window_state,
|
||||||
window_state_borrow.synthetic_drag_counter,
|
window_state_borrow.synthetic_drag_counter,
|
||||||
*position,
|
*event,
|
||||||
))
|
))
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
Event::LeftMouseUp { .. } => {
|
Event::MouseUp(MouseEvent {
|
||||||
|
button: MouseButton::Left,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
window_state_borrow.synthetic_drag_counter += 1;
|
window_state_borrow.synthetic_drag_counter += 1;
|
||||||
}
|
}
|
||||||
Event::ModifiersChanged {
|
Event::ModifiersChanged(ModifiersChangedEvent {
|
||||||
ctrl,
|
ctrl,
|
||||||
alt,
|
alt,
|
||||||
shift,
|
shift,
|
||||||
cmd,
|
cmd,
|
||||||
} => {
|
}) => {
|
||||||
// Only raise modifiers changed event when they have actually changed
|
// Only raise modifiers changed event when they have actually changed
|
||||||
if let Some(Event::ModifiersChanged {
|
if let Some(Event::ModifiersChanged(ModifiersChangedEvent {
|
||||||
ctrl: prev_ctrl,
|
ctrl: prev_ctrl,
|
||||||
alt: prev_alt,
|
alt: prev_alt,
|
||||||
shift: prev_shift,
|
shift: prev_shift,
|
||||||
cmd: prev_cmd,
|
cmd: prev_cmd,
|
||||||
}) = &window_state_borrow.previous_modifiers_changed_event
|
})) = &window_state_borrow.previous_modifiers_changed_event
|
||||||
{
|
{
|
||||||
if prev_ctrl == ctrl
|
if prev_ctrl == ctrl
|
||||||
&& prev_alt == alt
|
&& prev_alt == alt
|
||||||
|
@ -667,11 +675,11 @@ extern "C" fn cancel_operation(this: &Object, _sel: Sel, _sender: id) {
|
||||||
shift: false,
|
shift: false,
|
||||||
key: chars.clone(),
|
key: chars.clone(),
|
||||||
};
|
};
|
||||||
let event = Event::KeyDown {
|
let event = Event::KeyDown(KeyDownEvent {
|
||||||
keystroke: keystroke.clone(),
|
keystroke: keystroke.clone(),
|
||||||
input: Some(chars.clone()),
|
input: Some(chars.clone()),
|
||||||
is_held: false,
|
is_held: false,
|
||||||
};
|
});
|
||||||
|
|
||||||
window_state_borrow.last_fresh_keydown = Some((keystroke, Some(chars)));
|
window_state_borrow.last_fresh_keydown = Some((keystroke, Some(chars)));
|
||||||
if let Some(mut callback) = window_state_borrow.event_callback.take() {
|
if let Some(mut callback) = window_state_borrow.event_callback.take() {
|
||||||
|
@ -835,7 +843,7 @@ extern "C" fn display_layer(this: &Object, _: Sel, _: id) {
|
||||||
async fn synthetic_drag(
|
async fn synthetic_drag(
|
||||||
window_state: Weak<RefCell<WindowState>>,
|
window_state: Weak<RefCell<WindowState>>,
|
||||||
drag_id: usize,
|
drag_id: usize,
|
||||||
position: Vector2F,
|
event: MouseMovedEvent,
|
||||||
) {
|
) {
|
||||||
loop {
|
loop {
|
||||||
Timer::after(Duration::from_millis(16)).await;
|
Timer::after(Duration::from_millis(16)).await;
|
||||||
|
@ -844,14 +852,7 @@ async fn synthetic_drag(
|
||||||
if window_state_borrow.synthetic_drag_counter == drag_id {
|
if window_state_borrow.synthetic_drag_counter == drag_id {
|
||||||
if let Some(mut callback) = window_state_borrow.event_callback.take() {
|
if let Some(mut callback) = window_state_borrow.event_callback.take() {
|
||||||
drop(window_state_borrow);
|
drop(window_state_borrow);
|
||||||
callback(Event::LeftMouseDragged {
|
callback(Event::MouseMoved(event));
|
||||||
// TODO: Make sure empty modifiers is correct for this
|
|
||||||
position,
|
|
||||||
shift: false,
|
|
||||||
ctrl: false,
|
|
||||||
alt: false,
|
|
||||||
cmd: false,
|
|
||||||
});
|
|
||||||
window_state.borrow_mut().event_callback = Some(callback);
|
window_state.borrow_mut().event_callback = Some(callback);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -9,9 +9,9 @@ use crate::{
|
||||||
scene::CursorRegion,
|
scene::CursorRegion,
|
||||||
text_layout::TextLayoutCache,
|
text_layout::TextLayoutCache,
|
||||||
Action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AssetCache, ElementBox, Entity,
|
Action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AssetCache, ElementBox, Entity,
|
||||||
FontSystem, ModelHandle, MouseRegion, MouseRegionId, ReadModel, ReadView, RenderContext,
|
FontSystem, ModelHandle, MouseButton, MouseEvent, MouseMovedEvent, MouseRegion, MouseRegionId,
|
||||||
RenderParams, Scene, UpgradeModelHandle, UpgradeViewHandle, View, ViewHandle, WeakModelHandle,
|
ReadModel, ReadView, RenderContext, RenderParams, Scene, UpgradeModelHandle, UpgradeViewHandle,
|
||||||
WeakViewHandle,
|
View, ViewHandle, WeakModelHandle, WeakViewHandle,
|
||||||
};
|
};
|
||||||
use pathfinder_geometry::vector::{vec2f, Vector2F};
|
use pathfinder_geometry::vector::{vec2f, Vector2F};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
@ -235,7 +235,11 @@ impl Presenter {
|
||||||
let mut dragged_region = None;
|
let mut dragged_region = None;
|
||||||
|
|
||||||
match event {
|
match event {
|
||||||
Event::LeftMouseDown { position, .. } => {
|
Event::MouseDown(MouseEvent {
|
||||||
|
position,
|
||||||
|
button: MouseButton::Left,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
let mut hit = false;
|
let mut hit = false;
|
||||||
for (region, _) in self.mouse_regions.iter().rev() {
|
for (region, _) in self.mouse_regions.iter().rev() {
|
||||||
if region.bounds.contains_point(position) {
|
if region.bounds.contains_point(position) {
|
||||||
|
@ -251,11 +255,12 @@ impl Presenter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Event::LeftMouseUp {
|
Event::MouseUp(MouseEvent {
|
||||||
position,
|
position,
|
||||||
click_count,
|
click_count,
|
||||||
|
button: MouseButton::Left,
|
||||||
..
|
..
|
||||||
} => {
|
}) => {
|
||||||
self.prev_drag_position.take();
|
self.prev_drag_position.take();
|
||||||
if let Some(region) = self.clicked_region.take() {
|
if let Some(region) = self.clicked_region.take() {
|
||||||
invalidated_views.push(region.view_id);
|
invalidated_views.push(region.view_id);
|
||||||
|
@ -264,7 +269,11 @@ impl Presenter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Event::RightMouseDown { position, .. } => {
|
Event::MouseDown(MouseEvent {
|
||||||
|
position,
|
||||||
|
button: MouseButton::Right,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
let mut hit = false;
|
let mut hit = false;
|
||||||
for (region, _) in self.mouse_regions.iter().rev() {
|
for (region, _) in self.mouse_regions.iter().rev() {
|
||||||
if region.bounds.contains_point(position) {
|
if region.bounds.contains_point(position) {
|
||||||
|
@ -279,11 +288,12 @@ impl Presenter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Event::RightMouseUp {
|
Event::MouseUp(MouseEvent {
|
||||||
position,
|
position,
|
||||||
click_count,
|
click_count,
|
||||||
|
button: MouseButton::Right,
|
||||||
..
|
..
|
||||||
} => {
|
}) => {
|
||||||
if let Some(region) = self.right_clicked_region.take() {
|
if let Some(region) = self.right_clicked_region.take() {
|
||||||
invalidated_views.push(region.view_id);
|
invalidated_views.push(region.view_id);
|
||||||
if region.bounds.contains_point(position) {
|
if region.bounds.contains_point(position) {
|
||||||
|
@ -291,34 +301,37 @@ impl Presenter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Event::MouseMoved { .. } => {
|
Event::MouseMoved(MouseMovedEvent {
|
||||||
self.last_mouse_moved_event = Some(event.clone());
|
pressed_button,
|
||||||
}
|
|
||||||
Event::LeftMouseDragged {
|
|
||||||
position,
|
position,
|
||||||
shift,
|
shift,
|
||||||
ctrl,
|
ctrl,
|
||||||
alt,
|
alt,
|
||||||
cmd,
|
cmd,
|
||||||
} => {
|
..
|
||||||
if let Some((clicked_region, prev_drag_position)) = self
|
}) => {
|
||||||
.clicked_region
|
if let Some(MouseButton::Left) = pressed_button {
|
||||||
.as_ref()
|
if let Some((clicked_region, prev_drag_position)) = self
|
||||||
.zip(self.prev_drag_position.as_mut())
|
.clicked_region
|
||||||
{
|
.as_ref()
|
||||||
dragged_region =
|
.zip(self.prev_drag_position.as_mut())
|
||||||
Some((clicked_region.clone(), position - *prev_drag_position));
|
{
|
||||||
*prev_drag_position = position;
|
dragged_region =
|
||||||
|
Some((clicked_region.clone(), *prev_drag_position, position));
|
||||||
|
*prev_drag_position = position;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.last_mouse_moved_event = Some(Event::MouseMoved(MouseMovedEvent {
|
||||||
|
position,
|
||||||
|
pressed_button: Some(MouseButton::Left),
|
||||||
|
shift,
|
||||||
|
ctrl,
|
||||||
|
alt,
|
||||||
|
cmd,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.last_mouse_moved_event = Some(Event::MouseMoved {
|
self.last_mouse_moved_event = Some(event.clone());
|
||||||
position,
|
|
||||||
left_mouse_down: true,
|
|
||||||
shift,
|
|
||||||
ctrl,
|
|
||||||
alt,
|
|
||||||
cmd,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
@ -366,11 +379,11 @@ impl Presenter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some((dragged_region, delta)) = dragged_region {
|
if let Some((dragged_region, prev_position, position)) = dragged_region {
|
||||||
handled = true;
|
handled = true;
|
||||||
if let Some(drag_callback) = dragged_region.drag {
|
if let Some(drag_callback) = dragged_region.drag {
|
||||||
event_cx.with_current_view(dragged_region.view_id, |event_cx| {
|
event_cx.with_current_view(dragged_region.view_id, |event_cx| {
|
||||||
drag_callback(delta, event_cx);
|
drag_callback(prev_position, position, event_cx);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -410,13 +423,13 @@ impl Presenter {
|
||||||
let mut unhovered_regions = Vec::new();
|
let mut unhovered_regions = Vec::new();
|
||||||
let mut hovered_regions = Vec::new();
|
let mut hovered_regions = Vec::new();
|
||||||
|
|
||||||
if let Event::MouseMoved {
|
if let Event::MouseMoved(MouseMovedEvent {
|
||||||
position,
|
position,
|
||||||
left_mouse_down,
|
pressed_button,
|
||||||
..
|
..
|
||||||
} = event
|
}) = event
|
||||||
{
|
{
|
||||||
if !left_mouse_down {
|
if let None = pressed_button {
|
||||||
let mut style_to_assign = CursorStyle::Arrow;
|
let mut style_to_assign = CursorStyle::Arrow;
|
||||||
for region in self.cursor_regions.iter().rev() {
|
for region in self.cursor_regions.iter().rev() {
|
||||||
if region.bounds.contains_point(*position) {
|
if region.bounds.contains_point(*position) {
|
||||||
|
@ -648,6 +661,16 @@ impl<'a> PaintContext<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn paint_layer<F>(&mut self, clip_bounds: Option<RectF>, f: F)
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut Self) -> (),
|
||||||
|
{
|
||||||
|
self.scene.push_layer(clip_bounds);
|
||||||
|
f(self);
|
||||||
|
self.scene.pop_layer();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn current_view_id(&self) -> usize {
|
pub fn current_view_id(&self) -> usize {
|
||||||
*self.view_stack.last().unwrap()
|
*self.view_stack.last().unwrap()
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ use crate::{
|
||||||
geometry::{rect::RectF, vector::Vector2F},
|
geometry::{rect::RectF, vector::Vector2F},
|
||||||
json::ToJson,
|
json::ToJson,
|
||||||
platform::CursorStyle,
|
platform::CursorStyle,
|
||||||
EventContext, ImageData,
|
EventContext, ImageData, MouseEvent, MouseMovedEvent, ScrollWheelEvent,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct Scene {
|
pub struct Scene {
|
||||||
|
@ -44,17 +44,28 @@ pub struct CursorRegion {
|
||||||
pub style: CursorStyle,
|
pub style: CursorStyle,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub enum MouseRegionEvent {
|
||||||
|
Moved(MouseMovedEvent),
|
||||||
|
Hover(MouseEvent),
|
||||||
|
Down(MouseEvent),
|
||||||
|
Up(MouseEvent),
|
||||||
|
Click(MouseEvent),
|
||||||
|
DownOut(MouseEvent),
|
||||||
|
ScrollWheel(ScrollWheelEvent),
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
pub struct MouseRegion {
|
pub struct MouseRegion {
|
||||||
pub view_id: usize,
|
pub view_id: usize,
|
||||||
pub discriminant: Option<(TypeId, usize)>,
|
pub discriminant: Option<(TypeId, usize)>,
|
||||||
pub bounds: RectF,
|
pub bounds: RectF,
|
||||||
|
|
||||||
pub hover: Option<Rc<dyn Fn(Vector2F, bool, &mut EventContext)>>,
|
pub hover: Option<Rc<dyn Fn(Vector2F, bool, &mut EventContext)>>,
|
||||||
pub mouse_down: Option<Rc<dyn Fn(Vector2F, &mut EventContext)>>,
|
pub mouse_down: Option<Rc<dyn Fn(Vector2F, &mut EventContext)>>,
|
||||||
pub click: Option<Rc<dyn Fn(Vector2F, usize, &mut EventContext)>>,
|
pub click: Option<Rc<dyn Fn(Vector2F, usize, &mut EventContext)>>,
|
||||||
pub right_mouse_down: Option<Rc<dyn Fn(Vector2F, &mut EventContext)>>,
|
pub right_mouse_down: Option<Rc<dyn Fn(Vector2F, &mut EventContext)>>,
|
||||||
pub right_click: Option<Rc<dyn Fn(Vector2F, usize, &mut EventContext)>>,
|
pub right_click: Option<Rc<dyn Fn(Vector2F, usize, &mut EventContext)>>,
|
||||||
pub drag: Option<Rc<dyn Fn(Vector2F, &mut EventContext)>>,
|
pub drag: Option<Rc<dyn Fn(Vector2F, Vector2F, &mut EventContext)>>,
|
||||||
pub mouse_down_out: Option<Rc<dyn Fn(Vector2F, &mut EventContext)>>,
|
pub mouse_down_out: Option<Rc<dyn Fn(Vector2F, &mut EventContext)>>,
|
||||||
pub right_mouse_down_out: Option<Rc<dyn Fn(Vector2F, &mut EventContext)>>,
|
pub right_mouse_down_out: Option<Rc<dyn Fn(Vector2F, &mut EventContext)>>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -273,7 +273,7 @@ pub struct Chunk<'a> {
|
||||||
pub is_unnecessary: bool,
|
pub is_unnecessary: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct Diff {
|
pub struct Diff {
|
||||||
base_version: clock::Global,
|
base_version: clock::Global,
|
||||||
new_text: Arc<str>,
|
new_text: Arc<str>,
|
||||||
changes: Vec<(ChangeTag, usize)>,
|
changes: Vec<(ChangeTag, usize)>,
|
||||||
|
@ -958,7 +958,7 @@ impl Buffer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn diff(&self, mut new_text: String, cx: &AppContext) -> Task<Diff> {
|
pub fn diff(&self, mut new_text: String, cx: &AppContext) -> Task<Diff> {
|
||||||
let old_text = self.as_rope().clone();
|
let old_text = self.as_rope().clone();
|
||||||
let base_version = self.version();
|
let base_version = self.version();
|
||||||
cx.background().spawn(async move {
|
cx.background().spawn(async move {
|
||||||
|
@ -979,11 +979,7 @@ impl Buffer {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn apply_diff(
|
pub fn apply_diff(&mut self, diff: Diff, cx: &mut ModelContext<Self>) -> Option<&Transaction> {
|
||||||
&mut self,
|
|
||||||
diff: Diff,
|
|
||||||
cx: &mut ModelContext<Self>,
|
|
||||||
) -> Option<&Transaction> {
|
|
||||||
if self.version == diff.base_version {
|
if self.version == diff.base_version {
|
||||||
self.finalize_last_transaction();
|
self.finalize_last_transaction();
|
||||||
self.start_transaction();
|
self.start_transaction();
|
||||||
|
|
|
@ -334,28 +334,6 @@ impl FakeFs {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn insert_dir(&self, path: impl AsRef<Path>) {
|
|
||||||
let mut state = self.state.lock().await;
|
|
||||||
let path = path.as_ref();
|
|
||||||
state.validate_path(path).unwrap();
|
|
||||||
|
|
||||||
let inode = state.next_inode;
|
|
||||||
state.next_inode += 1;
|
|
||||||
state.entries.insert(
|
|
||||||
path.to_path_buf(),
|
|
||||||
FakeFsEntry {
|
|
||||||
metadata: Metadata {
|
|
||||||
inode,
|
|
||||||
mtime: SystemTime::now(),
|
|
||||||
is_dir: true,
|
|
||||||
is_symlink: false,
|
|
||||||
},
|
|
||||||
content: None,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
state.emit_event(&[path]).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn insert_file(&self, path: impl AsRef<Path>, content: String) {
|
pub async fn insert_file(&self, path: impl AsRef<Path>, content: String) {
|
||||||
let mut state = self.state.lock().await;
|
let mut state = self.state.lock().await;
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
|
@ -392,7 +370,7 @@ impl FakeFs {
|
||||||
|
|
||||||
match tree {
|
match tree {
|
||||||
Object(map) => {
|
Object(map) => {
|
||||||
self.insert_dir(path).await;
|
self.create_dir(path).await.unwrap();
|
||||||
for (name, contents) in map {
|
for (name, contents) in map {
|
||||||
let mut path = PathBuf::from(path);
|
let mut path = PathBuf::from(path);
|
||||||
path.push(name);
|
path.push(name);
|
||||||
|
@ -400,7 +378,7 @@ impl FakeFs {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Null => {
|
Null => {
|
||||||
self.insert_dir(&path).await;
|
self.create_dir(&path).await.unwrap();
|
||||||
}
|
}
|
||||||
String(contents) => {
|
String(contents) => {
|
||||||
self.insert_file(&path, contents).await;
|
self.insert_file(&path, contents).await;
|
||||||
|
|
|
@ -12,7 +12,7 @@ use anyhow::{anyhow, Context, Result};
|
||||||
use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore};
|
use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore};
|
||||||
use clock::ReplicaId;
|
use clock::ReplicaId;
|
||||||
use collections::{hash_map, BTreeMap, HashMap, HashSet};
|
use collections::{hash_map, BTreeMap, HashMap, HashSet};
|
||||||
use futures::{future::Shared, Future, FutureExt, StreamExt, TryFutureExt};
|
use futures::{future::Shared, AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt};
|
||||||
use fuzzy::{PathMatch, PathMatchCandidate, PathMatchCandidateSet};
|
use fuzzy::{PathMatch, PathMatchCandidate, PathMatchCandidateSet};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyModelHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
|
AnyModelHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
|
||||||
|
@ -51,10 +51,12 @@ use std::{
|
||||||
ffi::OsString,
|
ffi::OsString,
|
||||||
hash::Hash,
|
hash::Hash,
|
||||||
mem,
|
mem,
|
||||||
|
num::NonZeroU32,
|
||||||
ops::Range,
|
ops::Range,
|
||||||
os::unix::{ffi::OsStrExt, prelude::OsStringExt},
|
os::unix::{ffi::OsStrExt, prelude::OsStringExt},
|
||||||
path::{Component, Path, PathBuf},
|
path::{Component, Path, PathBuf},
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
|
str,
|
||||||
sync::{
|
sync::{
|
||||||
atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
|
atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
|
||||||
Arc,
|
Arc,
|
||||||
|
@ -3025,78 +3027,50 @@ impl Project {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (buffer, buffer_abs_path, language_server) in local_buffers {
|
for (buffer, buffer_abs_path, language_server) in local_buffers {
|
||||||
let text_document = lsp::TextDocumentIdentifier::new(
|
let (format_on_save, tab_size) = buffer.read_with(&cx, |buffer, cx| {
|
||||||
lsp::Url::from_file_path(&buffer_abs_path).unwrap(),
|
let settings = cx.global::<Settings>();
|
||||||
);
|
let language_name = buffer.language().map(|language| language.name());
|
||||||
let capabilities = &language_server.capabilities();
|
(
|
||||||
let tab_size = cx.update(|cx| {
|
settings.format_on_save(language_name.as_deref()),
|
||||||
let language_name = buffer.read(cx).language().map(|language| language.name());
|
settings.tab_size(language_name.as_deref()),
|
||||||
cx.global::<Settings>().tab_size(language_name.as_deref())
|
)
|
||||||
});
|
});
|
||||||
let lsp_edits = if capabilities
|
|
||||||
.document_formatting_provider
|
let transaction = match format_on_save {
|
||||||
.as_ref()
|
settings::FormatOnSave::Off => continue,
|
||||||
.map_or(false, |provider| *provider != lsp::OneOf::Left(false))
|
settings::FormatOnSave::LanguageServer => Self::format_via_lsp(
|
||||||
{
|
&this,
|
||||||
language_server
|
&buffer,
|
||||||
.request::<lsp::request::Formatting>(lsp::DocumentFormattingParams {
|
&buffer_abs_path,
|
||||||
text_document,
|
&language_server,
|
||||||
options: lsp::FormattingOptions {
|
tab_size,
|
||||||
tab_size: tab_size.into(),
|
&mut cx,
|
||||||
insert_spaces: true,
|
)
|
||||||
insert_final_newline: Some(true),
|
.await
|
||||||
..Default::default()
|
.context("failed to format via language server")?,
|
||||||
},
|
settings::FormatOnSave::External { command, arguments } => {
|
||||||
work_done_progress_params: Default::default(),
|
Self::format_via_external_command(
|
||||||
})
|
&buffer,
|
||||||
.await?
|
&buffer_abs_path,
|
||||||
} else if capabilities
|
&command,
|
||||||
.document_range_formatting_provider
|
&arguments,
|
||||||
.as_ref()
|
&mut cx,
|
||||||
.map_or(false, |provider| *provider != lsp::OneOf::Left(false))
|
|
||||||
{
|
|
||||||
let buffer_start = lsp::Position::new(0, 0);
|
|
||||||
let buffer_end =
|
|
||||||
buffer.read_with(&cx, |buffer, _| point_to_lsp(buffer.max_point_utf16()));
|
|
||||||
language_server
|
|
||||||
.request::<lsp::request::RangeFormatting>(
|
|
||||||
lsp::DocumentRangeFormattingParams {
|
|
||||||
text_document,
|
|
||||||
range: lsp::Range::new(buffer_start, buffer_end),
|
|
||||||
options: lsp::FormattingOptions {
|
|
||||||
tab_size: tab_size.into(),
|
|
||||||
insert_spaces: true,
|
|
||||||
insert_final_newline: Some(true),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
work_done_progress_params: Default::default(),
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
.await?
|
.await
|
||||||
} else {
|
.context(format!(
|
||||||
continue;
|
"failed to format via external command {:?}",
|
||||||
|
command
|
||||||
|
))?
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(lsp_edits) = lsp_edits {
|
if let Some(transaction) = transaction {
|
||||||
let edits = this
|
if !push_to_history {
|
||||||
.update(&mut cx, |this, cx| {
|
buffer.update(&mut cx, |buffer, _| {
|
||||||
this.edits_from_lsp(&buffer, lsp_edits, None, cx)
|
buffer.forget_transaction(transaction.id)
|
||||||
})
|
});
|
||||||
.await?;
|
}
|
||||||
buffer.update(&mut cx, |buffer, cx| {
|
project_transaction.0.insert(buffer, transaction);
|
||||||
buffer.finalize_last_transaction();
|
|
||||||
buffer.start_transaction();
|
|
||||||
for (range, text) in edits {
|
|
||||||
buffer.edit([(range, text)], cx);
|
|
||||||
}
|
|
||||||
if buffer.end_transaction(cx).is_some() {
|
|
||||||
let transaction = buffer.finalize_last_transaction().unwrap().clone();
|
|
||||||
if !push_to_history {
|
|
||||||
buffer.forget_transaction(transaction.id);
|
|
||||||
}
|
|
||||||
project_transaction.0.insert(cx.handle(), transaction);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3104,6 +3078,141 @@ impl Project {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn format_via_lsp(
|
||||||
|
this: &ModelHandle<Self>,
|
||||||
|
buffer: &ModelHandle<Buffer>,
|
||||||
|
abs_path: &Path,
|
||||||
|
language_server: &Arc<LanguageServer>,
|
||||||
|
tab_size: NonZeroU32,
|
||||||
|
cx: &mut AsyncAppContext,
|
||||||
|
) -> Result<Option<Transaction>> {
|
||||||
|
let text_document =
|
||||||
|
lsp::TextDocumentIdentifier::new(lsp::Url::from_file_path(abs_path).unwrap());
|
||||||
|
let capabilities = &language_server.capabilities();
|
||||||
|
let lsp_edits = if capabilities
|
||||||
|
.document_formatting_provider
|
||||||
|
.as_ref()
|
||||||
|
.map_or(false, |provider| *provider != lsp::OneOf::Left(false))
|
||||||
|
{
|
||||||
|
language_server
|
||||||
|
.request::<lsp::request::Formatting>(lsp::DocumentFormattingParams {
|
||||||
|
text_document,
|
||||||
|
options: lsp::FormattingOptions {
|
||||||
|
tab_size: tab_size.into(),
|
||||||
|
insert_spaces: true,
|
||||||
|
insert_final_newline: Some(true),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
work_done_progress_params: Default::default(),
|
||||||
|
})
|
||||||
|
.await?
|
||||||
|
} else if capabilities
|
||||||
|
.document_range_formatting_provider
|
||||||
|
.as_ref()
|
||||||
|
.map_or(false, |provider| *provider != lsp::OneOf::Left(false))
|
||||||
|
{
|
||||||
|
let buffer_start = lsp::Position::new(0, 0);
|
||||||
|
let buffer_end =
|
||||||
|
buffer.read_with(cx, |buffer, _| point_to_lsp(buffer.max_point_utf16()));
|
||||||
|
language_server
|
||||||
|
.request::<lsp::request::RangeFormatting>(lsp::DocumentRangeFormattingParams {
|
||||||
|
text_document,
|
||||||
|
range: lsp::Range::new(buffer_start, buffer_end),
|
||||||
|
options: lsp::FormattingOptions {
|
||||||
|
tab_size: tab_size.into(),
|
||||||
|
insert_spaces: true,
|
||||||
|
insert_final_newline: Some(true),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
work_done_progress_params: Default::default(),
|
||||||
|
})
|
||||||
|
.await?
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(lsp_edits) = lsp_edits {
|
||||||
|
let edits = this
|
||||||
|
.update(cx, |this, cx| {
|
||||||
|
this.edits_from_lsp(&buffer, lsp_edits, None, cx)
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
buffer.update(cx, |buffer, cx| {
|
||||||
|
buffer.finalize_last_transaction();
|
||||||
|
buffer.start_transaction();
|
||||||
|
for (range, text) in edits {
|
||||||
|
buffer.edit([(range, text)], cx);
|
||||||
|
}
|
||||||
|
if buffer.end_transaction(cx).is_some() {
|
||||||
|
let transaction = buffer.finalize_last_transaction().unwrap().clone();
|
||||||
|
Ok(Some(transaction))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn format_via_external_command(
|
||||||
|
buffer: &ModelHandle<Buffer>,
|
||||||
|
buffer_abs_path: &Path,
|
||||||
|
command: &str,
|
||||||
|
arguments: &[String],
|
||||||
|
cx: &mut AsyncAppContext,
|
||||||
|
) -> Result<Option<Transaction>> {
|
||||||
|
let working_dir_path = buffer.read_with(cx, |buffer, cx| {
|
||||||
|
let file = File::from_dyn(buffer.file())?;
|
||||||
|
let worktree = file.worktree.read(cx).as_local()?;
|
||||||
|
let mut worktree_path = worktree.abs_path().to_path_buf();
|
||||||
|
if worktree.root_entry()?.is_file() {
|
||||||
|
worktree_path.pop();
|
||||||
|
}
|
||||||
|
Some(worktree_path)
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(working_dir_path) = working_dir_path {
|
||||||
|
let mut child =
|
||||||
|
smol::process::Command::new(command)
|
||||||
|
.args(arguments.iter().map(|arg| {
|
||||||
|
arg.replace("{buffer_path}", &buffer_abs_path.to_string_lossy())
|
||||||
|
}))
|
||||||
|
.current_dir(&working_dir_path)
|
||||||
|
.stdin(smol::process::Stdio::piped())
|
||||||
|
.stdout(smol::process::Stdio::piped())
|
||||||
|
.stderr(smol::process::Stdio::piped())
|
||||||
|
.spawn()?;
|
||||||
|
let stdin = child
|
||||||
|
.stdin
|
||||||
|
.as_mut()
|
||||||
|
.ok_or_else(|| anyhow!("failed to acquire stdin"))?;
|
||||||
|
let text = buffer.read_with(cx, |buffer, _| buffer.as_rope().clone());
|
||||||
|
for chunk in text.chunks() {
|
||||||
|
stdin.write_all(chunk.as_bytes()).await?;
|
||||||
|
}
|
||||||
|
stdin.flush().await?;
|
||||||
|
|
||||||
|
let output = child.output().await?;
|
||||||
|
if !output.status.success() {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"command failed with exit code {:?}:\nstdout: {}\nstderr: {}",
|
||||||
|
output.status.code(),
|
||||||
|
String::from_utf8_lossy(&output.stdout),
|
||||||
|
String::from_utf8_lossy(&output.stderr),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let stdout = String::from_utf8(output.stdout)?;
|
||||||
|
let diff = buffer
|
||||||
|
.read_with(cx, |buffer, cx| buffer.diff(stdout, cx))
|
||||||
|
.await;
|
||||||
|
Ok(buffer.update(cx, |buffer, cx| buffer.apply_diff(diff, cx).cloned()))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn definition<T: ToPointUtf16>(
|
pub fn definition<T: ToPointUtf16>(
|
||||||
&self,
|
&self,
|
||||||
buffer: &ModelHandle<Buffer>,
|
buffer: &ModelHandle<Buffer>,
|
||||||
|
|
|
@ -38,7 +38,7 @@ pub struct LanguageSettings {
|
||||||
pub hard_tabs: Option<bool>,
|
pub hard_tabs: Option<bool>,
|
||||||
pub soft_wrap: Option<SoftWrap>,
|
pub soft_wrap: Option<SoftWrap>,
|
||||||
pub preferred_line_length: Option<u32>,
|
pub preferred_line_length: Option<u32>,
|
||||||
pub format_on_save: Option<bool>,
|
pub format_on_save: Option<FormatOnSave>,
|
||||||
pub enable_language_server: Option<bool>,
|
pub enable_language_server: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,6 +50,17 @@ pub enum SoftWrap {
|
||||||
PreferredLineLength,
|
PreferredLineLength,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum FormatOnSave {
|
||||||
|
Off,
|
||||||
|
LanguageServer,
|
||||||
|
External {
|
||||||
|
command: String,
|
||||||
|
arguments: Vec<String>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
|
#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum Autosave {
|
pub enum Autosave {
|
||||||
|
@ -72,7 +83,7 @@ pub struct SettingsFileContent {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub vim_mode: Option<bool>,
|
pub vim_mode: Option<bool>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub format_on_save: Option<bool>,
|
pub format_on_save: Option<FormatOnSave>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub autosave: Option<Autosave>,
|
pub autosave: Option<Autosave>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
@ -136,9 +147,9 @@ impl Settings {
|
||||||
.unwrap_or(80)
|
.unwrap_or(80)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn format_on_save(&self, language: Option<&str>) -> bool {
|
pub fn format_on_save(&self, language: Option<&str>) -> FormatOnSave {
|
||||||
self.language_setting(language, |settings| settings.format_on_save)
|
self.language_setting(language, |settings| settings.format_on_save.clone())
|
||||||
.unwrap_or(true)
|
.unwrap_or(FormatOnSave::LanguageServer)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn enable_language_server(&self, language: Option<&str>) -> bool {
|
pub fn enable_language_server(&self, language: Option<&str>) -> bool {
|
||||||
|
@ -215,7 +226,7 @@ impl Settings {
|
||||||
merge(&mut self.autosave, data.autosave);
|
merge(&mut self.autosave, data.autosave);
|
||||||
merge_option(
|
merge_option(
|
||||||
&mut self.language_settings.format_on_save,
|
&mut self.language_settings.format_on_save,
|
||||||
data.format_on_save,
|
data.format_on_save.clone(),
|
||||||
);
|
);
|
||||||
merge_option(
|
merge_option(
|
||||||
&mut self.language_settings.enable_language_server,
|
&mut self.language_settings.enable_language_server,
|
||||||
|
@ -339,7 +350,7 @@ fn merge<T: Copy>(target: &mut T, value: Option<T>) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn merge_option<T: Copy>(target: &mut Option<T>, value: Option<T>) {
|
fn merge_option<T>(target: &mut Option<T>, value: Option<T>) {
|
||||||
if value.is_some() {
|
if value.is_some() {
|
||||||
*target = value;
|
*target = value;
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,11 @@ mio-extras = "2.0.6"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
ordered-float = "2.1.1"
|
ordered-float = "2.1.1"
|
||||||
itertools = "0.10"
|
itertools = "0.10"
|
||||||
|
dirs = "4.0.0"
|
||||||
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
gpui = { path = "../gpui", features = ["test-support"] }
|
gpui = { path = "../gpui", features = ["test-support"] }
|
||||||
|
client = { path = "../client", features = ["test-support"]}
|
||||||
|
project = { path = "../project", features = ["test-support"]}
|
||||||
|
|
||||||
|
|
134
crates/terminal/src/color_translation.rs
Normal file
134
crates/terminal/src/color_translation.rs
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
use alacritty_terminal::{ansi::Color as AnsiColor, term::color::Rgb as AlacRgb};
|
||||||
|
use gpui::color::Color;
|
||||||
|
use theme::TerminalStyle;
|
||||||
|
|
||||||
|
///Converts a 2, 8, or 24 bit color ANSI color to the GPUI equivalent
|
||||||
|
pub fn convert_color(alac_color: &AnsiColor, style: &TerminalStyle) -> Color {
|
||||||
|
match alac_color {
|
||||||
|
//Named and theme defined colors
|
||||||
|
alacritty_terminal::ansi::Color::Named(n) => match n {
|
||||||
|
alacritty_terminal::ansi::NamedColor::Black => style.black,
|
||||||
|
alacritty_terminal::ansi::NamedColor::Red => style.red,
|
||||||
|
alacritty_terminal::ansi::NamedColor::Green => style.green,
|
||||||
|
alacritty_terminal::ansi::NamedColor::Yellow => style.yellow,
|
||||||
|
alacritty_terminal::ansi::NamedColor::Blue => style.blue,
|
||||||
|
alacritty_terminal::ansi::NamedColor::Magenta => style.magenta,
|
||||||
|
alacritty_terminal::ansi::NamedColor::Cyan => style.cyan,
|
||||||
|
alacritty_terminal::ansi::NamedColor::White => style.white,
|
||||||
|
alacritty_terminal::ansi::NamedColor::BrightBlack => style.bright_black,
|
||||||
|
alacritty_terminal::ansi::NamedColor::BrightRed => style.bright_red,
|
||||||
|
alacritty_terminal::ansi::NamedColor::BrightGreen => style.bright_green,
|
||||||
|
alacritty_terminal::ansi::NamedColor::BrightYellow => style.bright_yellow,
|
||||||
|
alacritty_terminal::ansi::NamedColor::BrightBlue => style.bright_blue,
|
||||||
|
alacritty_terminal::ansi::NamedColor::BrightMagenta => style.bright_magenta,
|
||||||
|
alacritty_terminal::ansi::NamedColor::BrightCyan => style.bright_cyan,
|
||||||
|
alacritty_terminal::ansi::NamedColor::BrightWhite => style.bright_white,
|
||||||
|
alacritty_terminal::ansi::NamedColor::Foreground => style.foreground,
|
||||||
|
alacritty_terminal::ansi::NamedColor::Background => style.background,
|
||||||
|
alacritty_terminal::ansi::NamedColor::Cursor => style.cursor,
|
||||||
|
alacritty_terminal::ansi::NamedColor::DimBlack => style.dim_black,
|
||||||
|
alacritty_terminal::ansi::NamedColor::DimRed => style.dim_red,
|
||||||
|
alacritty_terminal::ansi::NamedColor::DimGreen => style.dim_green,
|
||||||
|
alacritty_terminal::ansi::NamedColor::DimYellow => style.dim_yellow,
|
||||||
|
alacritty_terminal::ansi::NamedColor::DimBlue => style.dim_blue,
|
||||||
|
alacritty_terminal::ansi::NamedColor::DimMagenta => style.dim_magenta,
|
||||||
|
alacritty_terminal::ansi::NamedColor::DimCyan => style.dim_cyan,
|
||||||
|
alacritty_terminal::ansi::NamedColor::DimWhite => style.dim_white,
|
||||||
|
alacritty_terminal::ansi::NamedColor::BrightForeground => style.bright_foreground,
|
||||||
|
alacritty_terminal::ansi::NamedColor::DimForeground => style.dim_foreground,
|
||||||
|
},
|
||||||
|
//'True' colors
|
||||||
|
alacritty_terminal::ansi::Color::Spec(rgb) => Color::new(rgb.r, rgb.g, rgb.b, u8::MAX),
|
||||||
|
//8 bit, indexed colors
|
||||||
|
alacritty_terminal::ansi::Color::Indexed(i) => get_color_at_index(&(*i as usize), style),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///Converts an 8 bit ANSI color to it's GPUI equivalent.
|
||||||
|
///Accepts usize for compatability with the alacritty::Colors interface,
|
||||||
|
///Other than that use case, should only be called with values in the [0,255] range
|
||||||
|
pub fn get_color_at_index(index: &usize, style: &TerminalStyle) -> Color {
|
||||||
|
match index {
|
||||||
|
//0-15 are the same as the named colors above
|
||||||
|
0 => style.black,
|
||||||
|
1 => style.red,
|
||||||
|
2 => style.green,
|
||||||
|
3 => style.yellow,
|
||||||
|
4 => style.blue,
|
||||||
|
5 => style.magenta,
|
||||||
|
6 => style.cyan,
|
||||||
|
7 => style.white,
|
||||||
|
8 => style.bright_black,
|
||||||
|
9 => style.bright_red,
|
||||||
|
10 => style.bright_green,
|
||||||
|
11 => style.bright_yellow,
|
||||||
|
12 => style.bright_blue,
|
||||||
|
13 => style.bright_magenta,
|
||||||
|
14 => style.bright_cyan,
|
||||||
|
15 => style.bright_white,
|
||||||
|
//16-231 are mapped to their RGB colors on a 0-5 range per channel
|
||||||
|
16..=231 => {
|
||||||
|
let (r, g, b) = rgb_for_index(&(*index as u8)); //Split the index into it's ANSI-RGB components
|
||||||
|
let step = (u8::MAX as f32 / 5.).floor() as u8; //Split the RGB range into 5 chunks, with floor so no overflow
|
||||||
|
Color::new(r * step, g * step, b * step, u8::MAX) //Map the ANSI-RGB components to an RGB color
|
||||||
|
}
|
||||||
|
//232-255 are a 24 step grayscale from black to white
|
||||||
|
232..=255 => {
|
||||||
|
let i = *index as u8 - 232; //Align index to 0..24
|
||||||
|
let step = (u8::MAX as f32 / 24.).floor() as u8; //Split the RGB grayscale values into 24 chunks
|
||||||
|
Color::new(i * step, i * step, i * step, u8::MAX) //Map the ANSI-grayscale components to the RGB-grayscale
|
||||||
|
}
|
||||||
|
//For compatability with the alacritty::Colors interface
|
||||||
|
256 => style.foreground,
|
||||||
|
257 => style.background,
|
||||||
|
258 => style.cursor,
|
||||||
|
259 => style.dim_black,
|
||||||
|
260 => style.dim_red,
|
||||||
|
261 => style.dim_green,
|
||||||
|
262 => style.dim_yellow,
|
||||||
|
263 => style.dim_blue,
|
||||||
|
264 => style.dim_magenta,
|
||||||
|
265 => style.dim_cyan,
|
||||||
|
266 => style.dim_white,
|
||||||
|
267 => style.bright_foreground,
|
||||||
|
268 => style.black, //'Dim Background', non-standard color
|
||||||
|
_ => Color::new(0, 0, 0, 255),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
///Generates the rgb channels in [0, 5] for a given index into the 6x6x6 ANSI color cube
|
||||||
|
///See: [8 bit ansi color](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit).
|
||||||
|
///
|
||||||
|
///Wikipedia gives a formula for calculating the index for a given color:
|
||||||
|
///
|
||||||
|
///index = 16 + 36 × r + 6 × g + b (0 ≤ r, g, b ≤ 5)
|
||||||
|
///
|
||||||
|
///This function does the reverse, calculating the r, g, and b components from a given index.
|
||||||
|
fn rgb_for_index(i: &u8) -> (u8, u8, u8) {
|
||||||
|
debug_assert!(i >= &16 && i <= &231);
|
||||||
|
let i = i - 16;
|
||||||
|
let r = (i - (i % 36)) / 36;
|
||||||
|
let g = ((i % 36) - (i % 6)) / 6;
|
||||||
|
let b = (i % 36) % 6;
|
||||||
|
(r, g, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Convenience method to convert from a GPUI color to an alacritty Rgb
|
||||||
|
pub fn to_alac_rgb(color: Color) -> AlacRgb {
|
||||||
|
AlacRgb {
|
||||||
|
r: color.r,
|
||||||
|
g: color.g,
|
||||||
|
b: color.g,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#[test]
|
||||||
|
fn test_rgb_for_index() {
|
||||||
|
//Test every possible value in the color cube
|
||||||
|
for i in 16..=231 {
|
||||||
|
let (r, g, b) = crate::color_translation::rgb_for_index(&(i as u8));
|
||||||
|
assert_eq!(i, 16 + 36 * r + 6 * g + b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,32 +3,33 @@ mod modal;
|
||||||
pub mod terminal_element;
|
pub mod terminal_element;
|
||||||
|
|
||||||
use alacritty_terminal::{
|
use alacritty_terminal::{
|
||||||
config::{Config, Program, PtyConfig},
|
config::{Config, PtyConfig},
|
||||||
event::{Event as AlacTermEvent, EventListener, Notify},
|
event::{Event as AlacTermEvent, EventListener, Notify},
|
||||||
event_loop::{EventLoop, Msg, Notifier},
|
event_loop::{EventLoop, Msg, Notifier},
|
||||||
grid::Scroll,
|
grid::Scroll,
|
||||||
sync::FairMutex,
|
sync::FairMutex,
|
||||||
term::{color::Rgb as AlacRgb, SizeInfo},
|
term::SizeInfo,
|
||||||
tty::{self, setup_env},
|
tty::{self, setup_env},
|
||||||
Term,
|
Term,
|
||||||
};
|
};
|
||||||
|
use color_translation::{get_color_at_index, to_alac_rgb};
|
||||||
|
use dirs::home_dir;
|
||||||
use futures::{
|
use futures::{
|
||||||
channel::mpsc::{unbounded, UnboundedSender},
|
channel::mpsc::{unbounded, UnboundedSender},
|
||||||
StreamExt,
|
StreamExt,
|
||||||
};
|
};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, color::Color, elements::*, impl_internal_actions, platform::CursorStyle,
|
actions, elements::*, impl_internal_actions, platform::CursorStyle, ClipboardItem, Entity,
|
||||||
ClipboardItem, Entity, MutableAppContext, View, ViewContext,
|
MutableAppContext, View, ViewContext,
|
||||||
};
|
};
|
||||||
use modal::deploy_modal;
|
use modal::deploy_modal;
|
||||||
use project::{Project, ProjectPath};
|
use project::{LocalWorktree, Project, ProjectPath};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use std::{collections::HashMap, path::PathBuf, sync::Arc};
|
use std::{collections::HashMap, path::PathBuf, sync::Arc};
|
||||||
use workspace::{Item, Workspace};
|
use workspace::{Item, Workspace};
|
||||||
|
|
||||||
use crate::terminal_element::{get_color_at_index, TerminalEl};
|
use crate::terminal_element::TerminalEl;
|
||||||
|
|
||||||
//ASCII Control characters on a keyboard
|
//ASCII Control characters on a keyboard
|
||||||
const ETX_CHAR: char = 3_u8 as char; //'End of text', the control code for 'ctrl-c'
|
const ETX_CHAR: char = 3_u8 as char; //'End of text', the control code for 'ctrl-c'
|
||||||
|
@ -41,6 +42,14 @@ const RIGHT_SEQ: &str = "\x1b[C";
|
||||||
const UP_SEQ: &str = "\x1b[A";
|
const UP_SEQ: &str = "\x1b[A";
|
||||||
const DOWN_SEQ: &str = "\x1b[B";
|
const DOWN_SEQ: &str = "\x1b[B";
|
||||||
const DEFAULT_TITLE: &str = "Terminal";
|
const DEFAULT_TITLE: &str = "Terminal";
|
||||||
|
const DEBUG_TERMINAL_WIDTH: f32 = 1000.; //This needs to be wide enough that the prompt can fill the whole space.
|
||||||
|
const DEBUG_TERMINAL_HEIGHT: f32 = 200.;
|
||||||
|
const DEBUG_CELL_WIDTH: f32 = 5.;
|
||||||
|
const DEBUG_LINE_HEIGHT: f32 = 5.;
|
||||||
|
|
||||||
|
pub mod color_translation;
|
||||||
|
pub mod gpui_func_tools;
|
||||||
|
pub mod terminal_element;
|
||||||
|
|
||||||
///Action for carrying the input to the PTY
|
///Action for carrying the input to the PTY
|
||||||
#[derive(Clone, Default, Debug, PartialEq, Eq)]
|
#[derive(Clone, Default, Debug, PartialEq, Eq)]
|
||||||
|
@ -63,6 +72,7 @@ actions!(
|
||||||
Down,
|
Down,
|
||||||
Tab,
|
Tab,
|
||||||
Clear,
|
Clear,
|
||||||
|
Copy,
|
||||||
Paste,
|
Paste,
|
||||||
Deploy,
|
Deploy,
|
||||||
Quit,
|
Quit,
|
||||||
|
@ -79,12 +89,13 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||||
cx.add_action(Terminal::escape);
|
cx.add_action(Terminal::escape);
|
||||||
cx.add_action(Terminal::quit);
|
cx.add_action(Terminal::quit);
|
||||||
cx.add_action(Terminal::del);
|
cx.add_action(Terminal::del);
|
||||||
cx.add_action(Terminal::carriage_return); //TODO figure out how to do this properly. Should we be checking the terminal mode?
|
cx.add_action(Terminal::carriage_return);
|
||||||
cx.add_action(Terminal::left);
|
cx.add_action(Terminal::left);
|
||||||
cx.add_action(Terminal::right);
|
cx.add_action(Terminal::right);
|
||||||
cx.add_action(Terminal::up);
|
cx.add_action(Terminal::up);
|
||||||
cx.add_action(Terminal::down);
|
cx.add_action(Terminal::down);
|
||||||
cx.add_action(Terminal::tab);
|
cx.add_action(Terminal::tab);
|
||||||
|
cx.add_action(Terminal::copy);
|
||||||
cx.add_action(Terminal::paste);
|
cx.add_action(Terminal::paste);
|
||||||
cx.add_action(Terminal::scroll_terminal);
|
cx.add_action(Terminal::scroll_terminal);
|
||||||
cx.add_action(deploy_modal);
|
cx.add_action(deploy_modal);
|
||||||
|
@ -109,6 +120,7 @@ pub struct Terminal {
|
||||||
has_bell: bool, //Currently using iTerm bell, show bell emoji in tab until input is received
|
has_bell: bool, //Currently using iTerm bell, show bell emoji in tab until input is received
|
||||||
cur_size: SizeInfo,
|
cur_size: SizeInfo,
|
||||||
modal: bool,
|
modal: bool,
|
||||||
|
associated_directory: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
///Upward flowing events, for changing the title and such
|
///Upward flowing events, for changing the title and such
|
||||||
|
@ -143,12 +155,11 @@ impl Terminal {
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
let pty_config = PtyConfig {
|
let pty_config = PtyConfig {
|
||||||
shell: Some(Program::Just("zsh".to_string())),
|
shell: None, //Use the users default shell
|
||||||
working_directory,
|
working_directory: working_directory.clone(),
|
||||||
hold: false,
|
hold: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
//Does this mangle the zed Env? I'm guessing it does... do child processes have a seperate ENV?
|
|
||||||
let mut env: HashMap<String, String> = HashMap::new();
|
let mut env: HashMap<String, String> = HashMap::new();
|
||||||
//TODO: Properly set the current locale,
|
//TODO: Properly set the current locale,
|
||||||
env.insert("LC_ALL".to_string(), "en_US.UTF-8".to_string());
|
env.insert("LC_ALL".to_string(), "en_US.UTF-8".to_string());
|
||||||
|
@ -162,8 +173,15 @@ impl Terminal {
|
||||||
setup_env(&config);
|
setup_env(&config);
|
||||||
|
|
||||||
//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
|
||||||
//Set to something small for easier debugging
|
let size_info = SizeInfo::new(
|
||||||
let size_info = SizeInfo::new(200., 100.0, 5., 5., 0., 0., false);
|
DEBUG_TERMINAL_WIDTH,
|
||||||
|
DEBUG_TERMINAL_HEIGHT,
|
||||||
|
DEBUG_CELL_WIDTH,
|
||||||
|
DEBUG_LINE_HEIGHT,
|
||||||
|
0.,
|
||||||
|
0.,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
//Set up the terminal...
|
//Set up the terminal...
|
||||||
let term = Term::new(&config, size_info, ZedListener(events_tx.clone()));
|
let term = Term::new(&config, size_info, ZedListener(events_tx.clone()));
|
||||||
|
@ -192,6 +210,7 @@ impl Terminal {
|
||||||
has_bell: false,
|
has_bell: false,
|
||||||
cur_size: size_info,
|
cur_size: size_info,
|
||||||
modal,
|
modal,
|
||||||
|
associated_directory: working_directory,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -238,25 +257,8 @@ impl Terminal {
|
||||||
),
|
),
|
||||||
AlacTermEvent::ColorRequest(index, format) => {
|
AlacTermEvent::ColorRequest(index, format) => {
|
||||||
let color = self.term.lock().colors()[index].unwrap_or_else(|| {
|
let color = self.term.lock().colors()[index].unwrap_or_else(|| {
|
||||||
let term_style = &cx.global::<Settings>().theme.terminal.colors;
|
let term_style = &cx.global::<Settings>().theme.terminal;
|
||||||
match index {
|
to_alac_rgb(get_color_at_index(&index, term_style))
|
||||||
0..=255 => to_alac_rgb(get_color_at_index(&(index as u8), term_style)),
|
|
||||||
//These additional values are required to match the Alacritty Colors object's behavior
|
|
||||||
256 => to_alac_rgb(term_style.foreground),
|
|
||||||
257 => to_alac_rgb(term_style.background),
|
|
||||||
258 => to_alac_rgb(term_style.cursor),
|
|
||||||
259 => to_alac_rgb(term_style.dim_black),
|
|
||||||
260 => to_alac_rgb(term_style.dim_red),
|
|
||||||
261 => to_alac_rgb(term_style.dim_green),
|
|
||||||
262 => to_alac_rgb(term_style.dim_yellow),
|
|
||||||
263 => to_alac_rgb(term_style.dim_blue),
|
|
||||||
264 => to_alac_rgb(term_style.dim_magenta),
|
|
||||||
265 => to_alac_rgb(term_style.dim_cyan),
|
|
||||||
266 => to_alac_rgb(term_style.dim_white),
|
|
||||||
267 => to_alac_rgb(term_style.bright_foreground),
|
|
||||||
268 => to_alac_rgb(term_style.black), //Dim Background, non-standard
|
|
||||||
_ => AlacRgb { r: 0, g: 0, b: 0 },
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
self.write_to_pty(&Input(format(color)), cx)
|
self.write_to_pty(&Input(format(color)), cx)
|
||||||
}
|
}
|
||||||
|
@ -288,11 +290,12 @@ impl Terminal {
|
||||||
///Create a new Terminal in the current working directory or the user's home directory
|
///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>) {
|
fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
|
||||||
let project = workspace.project().read(cx);
|
let project = workspace.project().read(cx);
|
||||||
|
|
||||||
let abs_path = project
|
let abs_path = project
|
||||||
.active_entry()
|
.active_entry()
|
||||||
.and_then(|entry_id| project.worktree_for_entry(entry_id, cx))
|
.and_then(|entry_id| project.worktree_for_entry(entry_id, cx))
|
||||||
.and_then(|worktree_handle| worktree_handle.read(cx).as_local())
|
.and_then(|worktree_handle| worktree_handle.read(cx).as_local())
|
||||||
.map(|wt| wt.abs_path().to_path_buf());
|
.and_then(get_working_directory);
|
||||||
|
|
||||||
workspace.add_item(
|
workspace.add_item(
|
||||||
Box::new(cx.add_view(|cx| Terminal::new(cx, abs_path, false))),
|
Box::new(cx.add_view(|cx| Terminal::new(cx, abs_path, false))),
|
||||||
|
@ -310,6 +313,16 @@ impl Terminal {
|
||||||
cx.emit(Event::CloseTerminal);
|
cx.emit(Event::CloseTerminal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///Attempt to paste the clipboard into the terminal
|
||||||
|
fn copy(&mut self, _: &Copy, cx: &mut ViewContext<Self>) {
|
||||||
|
let term = self.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
|
///Attempt to paste the clipboard into the terminal
|
||||||
fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
|
fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
|
||||||
if let Some(item) = cx.read_from_clipboard() {
|
if let Some(item) = cx.read_from_clipboard() {
|
||||||
|
@ -444,6 +457,13 @@ impl Item for Terminal {
|
||||||
.boxed()
|
.boxed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self> {
|
||||||
|
//From what I can tell, there's no way to tell the current working
|
||||||
|
//Directory of the terminal from outside the terminal. There might be
|
||||||
|
//solutions to this, but they are non-trivial and require more IPC
|
||||||
|
Some(Terminal::new(cx, self.associated_directory.clone()))
|
||||||
|
}
|
||||||
|
|
||||||
fn project_path(&self, _cx: &gpui::AppContext) -> Option<ProjectPath> {
|
fn project_path(&self, _cx: &gpui::AppContext) -> Option<ProjectPath> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
@ -504,27 +524,134 @@ impl Item for Terminal {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//Convenience method for less lines
|
fn get_working_directory(wt: &LocalWorktree) -> Option<PathBuf> {
|
||||||
fn to_alac_rgb(color: Color) -> AlacRgb {
|
Some(wt.abs_path().to_path_buf())
|
||||||
AlacRgb {
|
.filter(|path| path.is_dir())
|
||||||
r: color.r,
|
.or_else(|| home_dir())
|
||||||
g: color.g,
|
|
||||||
b: color.g,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use alacritty_terminal::{grid::GridIterator, term::cell::Cell};
|
use alacritty_terminal::{
|
||||||
|
grid::GridIterator,
|
||||||
|
index::{Column, Line, Point, Side},
|
||||||
|
selection::{Selection, SelectionType},
|
||||||
|
term::cell::Cell,
|
||||||
|
};
|
||||||
use gpui::TestAppContext;
|
use gpui::TestAppContext;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
use project::{FakeFs, Fs, RealFs, RemoveOptions, Worktree};
|
||||||
|
use std::{path::Path, sync::atomic::AtomicUsize, time::Duration};
|
||||||
|
|
||||||
///Basic integration test, can we get the terminal to show up, execute a command,
|
///Basic integration test, can we get the terminal to show up, execute a command,
|
||||||
//and produce noticable output?
|
//and produce noticable output?
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_terminal(cx: &mut TestAppContext) {
|
async fn test_terminal(cx: &mut TestAppContext) {
|
||||||
let terminal = cx.add_view(Default::default(), |cx| Terminal::new(cx, None, false));
|
let terminal = cx.add_view(Default::default(), |cx| Terminal::new(cx, None, false));
|
||||||
|
cx.set_condition_duration(Duration::from_secs(2));
|
||||||
|
terminal.update(cx, |terminal, cx| {
|
||||||
|
terminal.write_to_pty(&Input(("expr 3 + 4".to_string()).to_string()), cx);
|
||||||
|
terminal.carriage_return(&Return, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
terminal
|
||||||
|
.condition(cx, |terminal, _cx| {
|
||||||
|
let term = terminal.term.clone();
|
||||||
|
let content = grid_as_str(term.lock().renderable_content().display_iter);
|
||||||
|
dbg!(&content);
|
||||||
|
content.contains("7")
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn single_file_worktree(cx: &mut TestAppContext) {
|
||||||
|
let mut async_cx = cx.to_async();
|
||||||
|
let http_client = client::test::FakeHttpClient::with_404_response();
|
||||||
|
let client = client::Client::new(http_client.clone());
|
||||||
|
let fake_fs = FakeFs::new(cx.background().clone());
|
||||||
|
|
||||||
|
let path = Path::new("/file/");
|
||||||
|
fake_fs.insert_file(path, "a".to_string()).await;
|
||||||
|
|
||||||
|
let worktree_handle = Worktree::local(
|
||||||
|
client,
|
||||||
|
path,
|
||||||
|
true,
|
||||||
|
fake_fs,
|
||||||
|
Arc::new(AtomicUsize::new(0)),
|
||||||
|
&mut async_cx,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
async_cx.update(|cx| {
|
||||||
|
let wt = worktree_handle.read(cx).as_local().unwrap();
|
||||||
|
let wd = get_working_directory(wt);
|
||||||
|
assert!(wd.is_some());
|
||||||
|
let path = wd.unwrap();
|
||||||
|
//This should be the system's working directory, so querying the real file system is probably ok.
|
||||||
|
assert!(path.is_dir());
|
||||||
|
assert_eq!(path, home_dir().unwrap());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_worktree_directory(cx: &mut TestAppContext) {
|
||||||
|
let mut async_cx = cx.to_async();
|
||||||
|
let http_client = client::test::FakeHttpClient::with_404_response();
|
||||||
|
let client = client::Client::new(http_client.clone());
|
||||||
|
|
||||||
|
let fs = RealFs;
|
||||||
|
let mut test_wd = home_dir().unwrap();
|
||||||
|
test_wd.push("dir");
|
||||||
|
|
||||||
|
fs.create_dir(test_wd.as_path())
|
||||||
|
.await
|
||||||
|
.expect("File could not be created");
|
||||||
|
|
||||||
|
let worktree_handle = Worktree::local(
|
||||||
|
client,
|
||||||
|
test_wd.clone(),
|
||||||
|
true,
|
||||||
|
Arc::new(RealFs),
|
||||||
|
Arc::new(AtomicUsize::new(0)),
|
||||||
|
&mut async_cx,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
async_cx.update(|cx| {
|
||||||
|
let wt = worktree_handle.read(cx).as_local().unwrap();
|
||||||
|
let wd = get_working_directory(wt);
|
||||||
|
assert!(wd.is_some());
|
||||||
|
let path = wd.unwrap();
|
||||||
|
assert!(path.is_dir());
|
||||||
|
assert_eq!(path, test_wd);
|
||||||
|
});
|
||||||
|
|
||||||
|
//Clean up after ourselves.
|
||||||
|
fs.remove_dir(
|
||||||
|
test_wd.as_path(),
|
||||||
|
RemoveOptions {
|
||||||
|
recursive: false,
|
||||||
|
ignore_if_not_exists: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.expect("Could not remove test directory");
|
||||||
|
}
|
||||||
|
|
||||||
|
///If this test is failing for you, check that DEBUG_TERMINAL_WIDTH is wide enough to fit your entire command prompt!
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_copy(cx: &mut TestAppContext) {
|
||||||
|
let terminal = cx.add_view(Default::default(), |cx| Terminal::new(cx, None));
|
||||||
|
cx.set_condition_duration(Duration::from_secs(2));
|
||||||
|
|
||||||
terminal.update(cx, |terminal, cx| {
|
terminal.update(cx, |terminal, cx| {
|
||||||
terminal.write_to_pty(&Input(("expr 3 + 4".to_string()).to_string()), cx);
|
terminal.write_to_pty(&Input(("expr 3 + 4".to_string()).to_string()), cx);
|
||||||
|
@ -538,6 +665,19 @@ mod tests {
|
||||||
content.contains("7")
|
content.contains("7")
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
terminal.update(cx, |terminal, cx| {
|
||||||
|
let mut term = terminal.term.lock();
|
||||||
|
term.selection = Some(Selection::new(
|
||||||
|
SelectionType::Semantic,
|
||||||
|
Point::new(Line(2), Column(0)),
|
||||||
|
Side::Right,
|
||||||
|
));
|
||||||
|
drop(term);
|
||||||
|
terminal.copy(&Copy, cx)
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.assert_clipboard_content(Some(&"7"));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn grid_as_str(grid_iterator: GridIterator<Cell>) -> String {
|
pub(crate) fn grid_as_str(grid_iterator: GridIterator<Cell>) -> String {
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
use alacritty_terminal::{
|
use alacritty_terminal::{
|
||||||
ansi::Color as AnsiColor,
|
|
||||||
grid::{Dimensions, GridIterator, Indexed},
|
grid::{Dimensions, GridIterator, Indexed},
|
||||||
index::Point,
|
index::{Column as GridCol, Line as GridLine, Point, Side},
|
||||||
|
selection::{Selection, SelectionRange, SelectionType},
|
||||||
|
sync::FairMutex,
|
||||||
term::{
|
term::{
|
||||||
cell::{Cell, Flags},
|
cell::{Cell, Flags},
|
||||||
SizeInfo,
|
SizeInfo,
|
||||||
},
|
},
|
||||||
|
Term,
|
||||||
};
|
};
|
||||||
use editor::{Cursor, CursorShape};
|
use editor::{Cursor, CursorShape, HighlightedRange, HighlightedRangeLine};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
color::Color,
|
color::Color,
|
||||||
elements::*,
|
elements::*,
|
||||||
|
@ -18,16 +20,21 @@ use gpui::{
|
||||||
},
|
},
|
||||||
json::json,
|
json::json,
|
||||||
text_layout::{Line, RunStyle},
|
text_layout::{Line, RunStyle},
|
||||||
Event, FontCache, MouseRegion, PaintContext, Quad, SizeConstraint, TextLayoutCache,
|
Event, FontCache, KeyDownEvent, MouseRegion, PaintContext, Quad, ScrollWheelEvent,
|
||||||
WeakViewHandle,
|
SizeConstraint, TextLayoutCache, WeakViewHandle,
|
||||||
};
|
};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use ordered_float::OrderedFloat;
|
use ordered_float::OrderedFloat;
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use std::rc::Rc;
|
|
||||||
use theme::{TerminalColors, TerminalStyle};
|
use theme::{TerminalColors, TerminalStyle};
|
||||||
|
|
||||||
use crate::{gpui_func_tools::paint_layer, Input, ScrollTerminal, Terminal};
|
use std::{cmp::min, ops::Range, rc::Rc, sync::Arc};
|
||||||
|
use std::{fmt::Debug, ops::Sub};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
color_translation::convert_color, gpui_func_tools::paint_layer, Input, ScrollTerminal,
|
||||||
|
Terminal, ZedListener,
|
||||||
|
};
|
||||||
|
|
||||||
///Scrolling is unbearably sluggish by default. Alacritty supports a configurable
|
///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
|
///Scroll multiplier that is set to 3 by default. This will be removed when I
|
||||||
|
@ -44,14 +51,27 @@ pub struct TerminalEl {
|
||||||
view: WeakViewHandle<Terminal>,
|
view: WeakViewHandle<Terminal>,
|
||||||
}
|
}
|
||||||
|
|
||||||
///Helper types so I don't mix these two up
|
///New type pattern so I don't mix these two up
|
||||||
struct CellWidth(f32);
|
struct CellWidth(f32);
|
||||||
struct LineHeight(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)]
|
#[derive(Clone, Debug, Default)]
|
||||||
struct LayoutCell {
|
struct LayoutCell {
|
||||||
point: Point<i32, i32>,
|
point: Point<i32, i32>,
|
||||||
text: Line,
|
text: Line, //NOTE TO SELF THIS IS BAD PERFORMANCE RN!
|
||||||
background_color: Color,
|
background_color: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,13 +87,14 @@ impl LayoutCell {
|
||||||
|
|
||||||
///The information generated during layout that is nescessary for painting
|
///The information generated during layout that is nescessary for painting
|
||||||
pub struct LayoutState {
|
pub struct LayoutState {
|
||||||
cells: Vec<(Point<i32, i32>, Line)>,
|
layout_lines: Vec<LayoutLine>,
|
||||||
background_rects: Vec<(RectF, Color)>, //Vec index == Line index for the LineSpan
|
|
||||||
line_height: LineHeight,
|
line_height: LineHeight,
|
||||||
em_width: CellWidth,
|
em_width: CellWidth,
|
||||||
cursor: Option<Cursor>,
|
cursor: Option<Cursor>,
|
||||||
background_color: Color,
|
background_color: Color,
|
||||||
cur_size: SizeInfo,
|
cur_size: SizeInfo,
|
||||||
|
terminal: Arc<FairMutex<Term<ZedListener>>>,
|
||||||
|
selection_color: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TerminalEl {
|
impl TerminalEl {
|
||||||
|
@ -105,48 +126,32 @@ impl Element for TerminalEl {
|
||||||
//Tell the view our new size. Requires a mutable borrow of cx and the view
|
//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);
|
let cur_size = make_new_size(constraint, &cell_width, &line_height);
|
||||||
//Note that set_size locks and mutates the terminal.
|
//Note that set_size locks and mutates the terminal.
|
||||||
//TODO: Would be nice to lock once for the whole of layout
|
|
||||||
view_handle.update(cx.app, |view, _cx| view.set_size(cur_size));
|
view_handle.update(cx.app, |view, _cx| view.set_size(cur_size));
|
||||||
|
|
||||||
//Now that we're done with the mutable portion, grab the immutable settings and view again
|
//Now that we're done with the mutable portion, grab the immutable settings and view again
|
||||||
let terminal_theme = &(cx.global::<Settings>()).theme.terminal;
|
|
||||||
let view = view_handle.read(cx);
|
let view = view_handle.read(cx);
|
||||||
let term = view.term.lock();
|
let term = view.term.lock();
|
||||||
|
let (selection_color, terminal_theme) = {
|
||||||
|
let theme = &(cx.global::<Settings>()).theme;
|
||||||
|
(theme.editor.selection.selection, &theme.terminal)
|
||||||
|
};
|
||||||
|
let terminal_mutex = view_handle.read(cx).term.clone();
|
||||||
|
let term = terminal_mutex.lock();
|
||||||
let grid = term.grid();
|
let grid = term.grid();
|
||||||
let cursor_point = grid.cursor.point;
|
let cursor_point = grid.cursor.point;
|
||||||
let cursor_text = grid[cursor_point.line][cursor_point.column].c.to_string();
|
let cursor_text = grid[cursor_point.line][cursor_point.column].c.to_string();
|
||||||
|
|
||||||
let content = term.renderable_content();
|
let content = term.renderable_content();
|
||||||
|
|
||||||
let layout_cells = layout_cells(
|
let layout_lines = layout_lines(
|
||||||
content.display_iter,
|
content.display_iter,
|
||||||
&text_style,
|
&text_style,
|
||||||
terminal_theme,
|
terminal_theme,
|
||||||
cx.text_layout_cache,
|
cx.text_layout_cache,
|
||||||
view.modal,
|
view.modal,
|
||||||
|
content.selection,
|
||||||
);
|
);
|
||||||
|
|
||||||
let cells = layout_cells
|
|
||||||
.iter()
|
|
||||||
.map(|c| (c.point, c.text.clone()))
|
|
||||||
.collect::<Vec<(Point<i32, i32>, Line)>>();
|
|
||||||
let background_rects = layout_cells
|
|
||||||
.iter()
|
|
||||||
.map(|cell| {
|
|
||||||
(
|
|
||||||
RectF::new(
|
|
||||||
vec2f(
|
|
||||||
cell.point.column as f32 * cell_width.0,
|
|
||||||
cell.point.line as f32 * line_height.0,
|
|
||||||
),
|
|
||||||
vec2f(cell_width.0, line_height.0),
|
|
||||||
),
|
|
||||||
cell.background_color,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect::<Vec<(RectF, Color)>>();
|
|
||||||
|
|
||||||
let block_text = cx.text_layout_cache.layout_str(
|
let block_text = cx.text_layout_cache.layout_str(
|
||||||
&cursor_text,
|
&cursor_text,
|
||||||
text_style.font_size,
|
text_style.font_size,
|
||||||
|
@ -185,6 +190,7 @@ impl Element for TerminalEl {
|
||||||
Some(block_text.clone()),
|
Some(block_text.clone()),
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
drop(term);
|
||||||
|
|
||||||
let background_color = if view.modal {
|
let background_color = if view.modal {
|
||||||
terminal_theme.colors.modal_background
|
terminal_theme.colors.modal_background
|
||||||
|
@ -195,13 +201,14 @@ impl Element for TerminalEl {
|
||||||
(
|
(
|
||||||
constraint.max,
|
constraint.max,
|
||||||
LayoutState {
|
LayoutState {
|
||||||
cells,
|
layout_lines,
|
||||||
line_height,
|
line_height,
|
||||||
em_width: cell_width,
|
em_width: cell_width,
|
||||||
cursor,
|
cursor,
|
||||||
cur_size,
|
cur_size,
|
||||||
background_rects,
|
|
||||||
background_color,
|
background_color,
|
||||||
|
terminal: terminal_mutex,
|
||||||
|
selection_color,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -215,17 +222,21 @@ impl Element for TerminalEl {
|
||||||
) -> Self::PaintState {
|
) -> Self::PaintState {
|
||||||
//Setup element stuff
|
//Setup element stuff
|
||||||
let clip_bounds = Some(visible_bounds);
|
let clip_bounds = Some(visible_bounds);
|
||||||
paint_layer(cx, clip_bounds, |cx| {
|
|
||||||
//Elements are ephemeral, only at paint time do we know what could be clicked by a mouse
|
|
||||||
cx.scene.push_mouse_region(MouseRegion {
|
|
||||||
view_id: self.view.id(),
|
|
||||||
mouse_down: Some(Rc::new(|_, cx| cx.focus_parent_view())),
|
|
||||||
bounds: visible_bounds,
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
|
|
||||||
|
paint_layer(cx, clip_bounds, |cx| {
|
||||||
|
let cur_size = layout.cur_size.clone();
|
||||||
let origin = bounds.origin() + vec2f(layout.em_width.0, 0.);
|
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,
|
||||||
|
);
|
||||||
|
|
||||||
paint_layer(cx, clip_bounds, |cx| {
|
paint_layer(cx, clip_bounds, |cx| {
|
||||||
//Start with a background color
|
//Start with a background color
|
||||||
cx.scene.push_quad(Quad {
|
cx.scene.push_quad(Quad {
|
||||||
|
@ -236,25 +247,83 @@ impl Element for TerminalEl {
|
||||||
});
|
});
|
||||||
|
|
||||||
//Draw cell backgrounds
|
//Draw cell backgrounds
|
||||||
for background_rect in &layout.background_rects {
|
for layout_line in &layout.layout_lines {
|
||||||
let new_origin = origin + background_rect.0.origin();
|
for layout_cell in &layout_line.cells {
|
||||||
cx.scene.push_quad(Quad {
|
let position = vec2f(
|
||||||
bounds: RectF::new(new_origin, background_rect.0.size()),
|
origin.x() + layout_cell.point.column as f32 * layout.em_width.0,
|
||||||
background: Some(background_rect.1),
|
origin.y() + layout_cell.point.line as f32 * layout.line_height.0,
|
||||||
border: Default::default(),
|
);
|
||||||
corner_radius: 0.,
|
let size = vec2f(layout.em_width.0, 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
|
||||||
|
paint_layer(cx, 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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
//Draw text
|
//Draw text
|
||||||
paint_layer(cx, clip_bounds, |cx| {
|
paint_layer(cx, clip_bounds, |cx| {
|
||||||
for (point, cell) in &layout.cells {
|
for layout_line in &layout.layout_lines {
|
||||||
let cell_origin = vec2f(
|
for layout_cell in &layout_line.cells {
|
||||||
origin.x() + point.column as f32 * layout.em_width.0,
|
let point = layout_cell.point;
|
||||||
origin.y() + point.line as f32 * layout.line_height.0,
|
|
||||||
);
|
//Don't actually know the start_x for a line, until here:
|
||||||
cell.paint(cell_origin, visible_bounds, layout.line_height.0, cx);
|
let cell_origin = vec2f(
|
||||||
|
origin.x() + point.column as f32 * layout.em_width.0,
|
||||||
|
origin.y() + point.line as f32 * layout.line_height.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
layout_cell.text.paint(
|
||||||
|
cell_origin,
|
||||||
|
visible_bounds,
|
||||||
|
layout.line_height.0,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -284,9 +353,9 @@ impl Element for TerminalEl {
|
||||||
cx: &mut gpui::EventContext,
|
cx: &mut gpui::EventContext,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
match event {
|
match event {
|
||||||
Event::ScrollWheel {
|
Event::ScrollWheel(ScrollWheelEvent {
|
||||||
delta, position, ..
|
delta, position, ..
|
||||||
} => visible_bounds
|
}) => visible_bounds
|
||||||
.contains_point(*position)
|
.contains_point(*position)
|
||||||
.then(|| {
|
.then(|| {
|
||||||
let vertical_scroll =
|
let vertical_scroll =
|
||||||
|
@ -294,9 +363,9 @@ impl Element for TerminalEl {
|
||||||
cx.dispatch_action(ScrollTerminal(vertical_scroll.round() as i32));
|
cx.dispatch_action(ScrollTerminal(vertical_scroll.round() as i32));
|
||||||
})
|
})
|
||||||
.is_some(),
|
.is_some(),
|
||||||
Event::KeyDown {
|
Event::KeyDown(KeyDownEvent {
|
||||||
input: Some(input), ..
|
input: Some(input), ..
|
||||||
} => cx
|
}) => cx
|
||||||
.is_parent_view_focused()
|
.is_parent_view_focused()
|
||||||
.then(|| {
|
.then(|| {
|
||||||
cx.dispatch_action(Input(input.to_string()));
|
cx.dispatch_action(Input(input.to_string()));
|
||||||
|
@ -319,6 +388,18 @@ impl Element for TerminalEl {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.
|
///Configures a text style from the current settings.
|
||||||
fn make_text_style(font_cache: &FontCache, settings: &Settings) -> TextStyle {
|
fn make_text_style(font_cache: &FontCache, settings: &Settings) -> TextStyle {
|
||||||
TextStyle {
|
TextStyle {
|
||||||
|
@ -351,39 +432,57 @@ fn make_new_size(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn layout_cells(
|
fn layout_lines(
|
||||||
grid: GridIterator<Cell>,
|
grid: GridIterator<Cell>,
|
||||||
text_style: &TextStyle,
|
text_style: &TextStyle,
|
||||||
terminal_theme: &TerminalStyle,
|
terminal_theme: &TerminalStyle,
|
||||||
text_layout_cache: &TextLayoutCache,
|
text_layout_cache: &TextLayoutCache,
|
||||||
modal: bool,
|
modal: bool,
|
||||||
) -> Vec<LayoutCell> {
|
selection_range: Option<SelectionRange>,
|
||||||
let mut line_count: i32 = 0;
|
) -> Vec<LayoutLine> {
|
||||||
let lines = grid.group_by(|i| i.point.line);
|
let lines = grid.group_by(|i| i.point.line);
|
||||||
lines
|
lines
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(_, line)| {
|
.enumerate()
|
||||||
line_count += 1;
|
.map(|(line_index, (_, line))| {
|
||||||
line.map(|indexed_cell| {
|
let mut highlighted_range = None;
|
||||||
let cell_text = &indexed_cell.c.to_string();
|
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_style = cell_style(&indexed_cell, terminal_theme, text_style, modal);
|
let cell_text = &indexed_cell.c.to_string();
|
||||||
|
|
||||||
let layout_cell = text_layout_cache.layout_str(
|
let cell_style = cell_style(&indexed_cell, terminal_theme, text_style, modal);
|
||||||
cell_text,
|
|
||||||
text_style.font_size,
|
//This is where we might be able to get better performance
|
||||||
&[(cell_text.len(), cell_style)],
|
let layout_cell = text_layout_cache.layout_str(
|
||||||
);
|
cell_text,
|
||||||
LayoutCell::new(
|
text_style.font_size,
|
||||||
Point::new(line_count - 1, indexed_cell.point.column.0 as i32),
|
&[(cell_text.len(), cell_style)],
|
||||||
layout_cell,
|
);
|
||||||
convert_color(&indexed_cell.bg, &terminal_theme.colors, modal),
|
|
||||||
)
|
LayoutCell::new(
|
||||||
})
|
Point::new(line_index as i32, indexed_cell.point.column.0 as i32),
|
||||||
.collect::<Vec<LayoutCell>>()
|
layout_cell,
|
||||||
|
convert_color(&indexed_cell.bg, &terminal_theme.colors, modal),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<Vec<LayoutCell>>();
|
||||||
|
|
||||||
|
LayoutLine {
|
||||||
|
cells,
|
||||||
|
highlighted_range,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.flatten()
|
.collect::<Vec<LayoutLine>>()
|
||||||
.collect::<Vec<LayoutCell>>()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute the cursor position and expected block width, may return a zero width if x_for_index returns
|
// Compute the cursor position and expected block width, may return a zero width if x_for_index returns
|
||||||
|
@ -492,56 +591,113 @@ fn convert_color(alac_color: &AnsiColor, colors: &TerminalColors, modal: bool) -
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
///Converts an 8 bit ANSI color to it's GPUI equivalent.
|
fn attach_mouse_handlers(
|
||||||
pub fn get_color_at_index(index: &u8, colors: &TerminalColors) -> Color {
|
origin: Vector2F,
|
||||||
match index {
|
cur_size: SizeInfo,
|
||||||
//0-15 are the same as the named colors above
|
view_id: usize,
|
||||||
0 => colors.black,
|
terminal_mutex: &Arc<FairMutex<Term<ZedListener>>>,
|
||||||
1 => colors.red,
|
visible_bounds: RectF,
|
||||||
2 => colors.green,
|
cx: &mut PaintContext,
|
||||||
3 => colors.yellow,
|
) {
|
||||||
4 => colors.blue,
|
let click_mutex = terminal_mutex.clone();
|
||||||
5 => colors.magenta,
|
let drag_mutex = terminal_mutex.clone();
|
||||||
6 => colors.cyan,
|
let mouse_down_mutex = terminal_mutex.clone();
|
||||||
7 => colors.white,
|
|
||||||
8 => colors.bright_black,
|
cx.scene.push_mouse_region(MouseRegion {
|
||||||
9 => colors.bright_red,
|
view_id,
|
||||||
10 => colors.bright_green,
|
mouse_down: Some(Rc::new(move |pos, _| {
|
||||||
11 => colors.bright_yellow,
|
let mut term = mouse_down_mutex.lock();
|
||||||
12 => colors.bright_blue,
|
let (point, side) = mouse_to_cell_data(
|
||||||
13 => colors.bright_magenta,
|
pos,
|
||||||
14 => colors.bright_cyan,
|
origin,
|
||||||
15 => colors.bright_white,
|
cur_size,
|
||||||
//16-231 are mapped to their RGB colors on a 0-5 range per channel
|
term.renderable_content().display_offset,
|
||||||
16..=231 => {
|
);
|
||||||
let (r, g, b) = rgb_for_index(index); //Split the index into it's ANSI-RGB components
|
term.selection = Some(Selection::new(SelectionType::Simple, point, side))
|
||||||
let step = (u8::MAX as f32 / 5.).floor() as u8; //Split the RGB range into 5 chunks, with floor so no overflow
|
})),
|
||||||
Color::new(r * step, g * step, b * step, u8::MAX) //Map the ANSI-RGB components to an RGB color
|
click: Some(Rc::new(move |pos, click_count, cx| {
|
||||||
}
|
let mut term = click_mutex.lock();
|
||||||
//232-255 are a 24 step grayscale from black to white
|
|
||||||
232..=255 => {
|
let (point, side) = mouse_to_cell_data(
|
||||||
let i = index - 232; //Align index to 0..24
|
pos,
|
||||||
let step = (u8::MAX as f32 / 24.).floor() as u8; //Split the RGB grayscale values into 24 chunks
|
origin,
|
||||||
Color::new(i * step, i * step, i * step, u8::MAX) //Map the ANSI-grayscale components to the RGB-grayscale
|
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();
|
||||||
|
})),
|
||||||
|
bounds: visible_bounds,
|
||||||
|
drag: Some(Rc::new(move |_delta, pos, cx| {
|
||||||
|
let mut term = drag_mutex.lock();
|
||||||
|
|
||||||
|
let (point, side) = mouse_to_cell_data(
|
||||||
|
pos,
|
||||||
|
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();
|
||||||
|
})),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
///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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
///Generates the rgb channels in [0, 5] for a given index into the 6x6x6 ANSI color cube
|
///Copied (with modifications) from alacritty/src/event.rs > Mouse::point()
|
||||||
///See: [8 bit ansi color](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit).
|
///Position is a pane-relative position. That means the top left corner of the mouse
|
||||||
///
|
///Region should be (0,0)
|
||||||
///Wikipedia gives a formula for calculating the index for a given color:
|
fn grid_cell(pos: &PaneRelativePos, cur_size: SizeInfo, display_offset: usize) -> Point {
|
||||||
///
|
let pos = pos.0;
|
||||||
///index = 16 + 36 × r + 6 × g + b (0 ≤ r, g, b ≤ 5)
|
let col = pos.x() / cur_size.cell_width(); //TODO: underflow...
|
||||||
///
|
let col = min(GridCol(col as usize), cur_size.last_column());
|
||||||
///This function does the reverse, calculating the r, g, and b components from a given index.
|
|
||||||
fn rgb_for_index(i: &u8) -> (u8, u8, u8) {
|
let line = pos.y() / cur_size.cell_height();
|
||||||
debug_assert!(i >= &16 && i <= &231);
|
let line = min(line as i32, cur_size.bottommost_line().0);
|
||||||
let i = i - 16;
|
|
||||||
let r = (i - (i % 36)) / 36;
|
//when clicking, need to ADD to get to the top left cell
|
||||||
let g = ((i % 36) - (i % 6)) / 6;
|
//e.g. total_lines - viewport_height, THEN subtract display offset
|
||||||
let b = (i % 36) % 6;
|
//0 -> total_lines - viewport_height - display_offset + mouse_line
|
||||||
(r, g, b)
|
|
||||||
|
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
|
///Draws the grid as Alacritty sees it. Useful for checking if there is an inconsistency between
|
||||||
|
@ -575,14 +731,73 @@ fn draw_debug_grid(bounds: RectF, layout: &mut LayoutState, cx: &mut PaintContex
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
mod test {
|
||||||
mod tests {
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_rgb_for_index() {
|
fn test_mouse_to_selection() {
|
||||||
//Test every possible value in the color cube
|
let term_width = 100.;
|
||||||
for i in 16..=231 {
|
let term_height = 200.;
|
||||||
let (r, g, b) = crate::terminal_element::rgb_for_index(&(i as u8));
|
let cell_width = 10.;
|
||||||
assert_eq!(i, 16 + 36 * r + 6 * g + b);
|
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),
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -108,6 +108,7 @@ pub struct Toolbar {
|
||||||
pub container: ContainerStyle,
|
pub container: ContainerStyle,
|
||||||
pub height: f32,
|
pub height: f32,
|
||||||
pub item_spacing: f32,
|
pub item_spacing: f32,
|
||||||
|
pub nav_button: Interactive<IconButton>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, Default)]
|
#[derive(Clone, Deserialize, Default)]
|
||||||
|
@ -509,28 +510,23 @@ pub struct Interactive<T> {
|
||||||
pub default: T,
|
pub default: T,
|
||||||
pub hover: Option<T>,
|
pub hover: Option<T>,
|
||||||
pub active: Option<T>,
|
pub active: Option<T>,
|
||||||
pub active_hover: Option<T>,
|
pub disabled: Option<T>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Interactive<T> {
|
impl<T> Interactive<T> {
|
||||||
pub fn style_for(&self, state: MouseState, active: bool) -> &T {
|
pub fn style_for(&self, state: MouseState, active: bool) -> &T {
|
||||||
if active {
|
if active {
|
||||||
if state.hovered {
|
self.active.as_ref().unwrap_or(&self.default)
|
||||||
self.active_hover
|
} else if state.hovered {
|
||||||
.as_ref()
|
self.hover.as_ref().unwrap_or(&self.default)
|
||||||
.or(self.active.as_ref())
|
|
||||||
.unwrap_or(&self.default)
|
|
||||||
} else {
|
|
||||||
self.active.as_ref().unwrap_or(&self.default)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if state.hovered {
|
&self.default
|
||||||
self.hover.as_ref().unwrap_or(&self.default)
|
|
||||||
} else {
|
|
||||||
&self.default
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn disabled_style(&self) -> &T {
|
||||||
|
self.disabled.as_ref().unwrap_or(&self.default)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
|
impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
|
||||||
|
@ -544,7 +540,7 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
|
||||||
default: Value,
|
default: Value,
|
||||||
hover: Option<Value>,
|
hover: Option<Value>,
|
||||||
active: Option<Value>,
|
active: Option<Value>,
|
||||||
active_hover: Option<Value>,
|
disabled: Option<Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
let json = Helper::deserialize(deserializer)?;
|
let json = Helper::deserialize(deserializer)?;
|
||||||
|
@ -570,14 +566,14 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
|
||||||
|
|
||||||
let hover = deserialize_state(json.hover)?;
|
let hover = deserialize_state(json.hover)?;
|
||||||
let active = deserialize_state(json.active)?;
|
let active = deserialize_state(json.active)?;
|
||||||
let active_hover = deserialize_state(json.active_hover)?;
|
let disabled = deserialize_state(json.disabled)?;
|
||||||
let default = serde_json::from_value(json.default).map_err(serde::de::Error::custom)?;
|
let default = serde_json::from_value(json.default).map_err(serde::de::Error::custom)?;
|
||||||
|
|
||||||
Ok(Interactive {
|
Ok(Interactive {
|
||||||
default,
|
default,
|
||||||
hover,
|
hover,
|
||||||
active,
|
active,
|
||||||
active_hover,
|
disabled,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -147,14 +147,6 @@ impl<'a> VimTestContext<'a> {
|
||||||
let mode = self.mode();
|
let mode = self.mode();
|
||||||
VimBindingTestContext::new(keystrokes, mode, mode, self)
|
VimBindingTestContext::new(keystrokes, mode, mode, self)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn assert_clipboard_content(&mut self, expected_content: Option<&str>) {
|
|
||||||
self.cx.update(|cx| {
|
|
||||||
let actual_content = cx.read_from_clipboard().map(|item| item.text().to_owned());
|
|
||||||
let expected_content = expected_content.map(|content| content.to_owned());
|
|
||||||
assert_eq!(actual_content, expected_content);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Deref for VimTestContext<'a> {
|
impl<'a> Deref for VimTestContext<'a> {
|
||||||
|
|
|
@ -136,13 +136,13 @@ pub struct ItemNavHistory {
|
||||||
item: Rc<dyn WeakItemHandle>,
|
item: Rc<dyn WeakItemHandle>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
struct NavHistory {
|
||||||
pub struct NavHistory {
|
|
||||||
mode: NavigationMode,
|
mode: NavigationMode,
|
||||||
backward_stack: VecDeque<NavigationEntry>,
|
backward_stack: VecDeque<NavigationEntry>,
|
||||||
forward_stack: VecDeque<NavigationEntry>,
|
forward_stack: VecDeque<NavigationEntry>,
|
||||||
closed_stack: VecDeque<NavigationEntry>,
|
closed_stack: VecDeque<NavigationEntry>,
|
||||||
paths_by_item: HashMap<usize, ProjectPath>,
|
paths_by_item: HashMap<usize, ProjectPath>,
|
||||||
|
pane: WeakViewHandle<Pane>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone)]
|
#[derive(Copy, Clone)]
|
||||||
|
@ -168,17 +168,28 @@ pub struct NavigationEntry {
|
||||||
|
|
||||||
impl Pane {
|
impl Pane {
|
||||||
pub fn new(cx: &mut ViewContext<Self>) -> Self {
|
pub fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||||
|
let handle = cx.weak_handle();
|
||||||
Self {
|
Self {
|
||||||
items: Vec::new(),
|
items: Vec::new(),
|
||||||
active_item_index: 0,
|
active_item_index: 0,
|
||||||
autoscroll: false,
|
autoscroll: false,
|
||||||
nav_history: Default::default(),
|
nav_history: Rc::new(RefCell::new(NavHistory {
|
||||||
toolbar: cx.add_view(|_| Toolbar::new()),
|
mode: NavigationMode::Normal,
|
||||||
|
backward_stack: Default::default(),
|
||||||
|
forward_stack: Default::default(),
|
||||||
|
closed_stack: Default::default(),
|
||||||
|
paths_by_item: Default::default(),
|
||||||
|
pane: handle.clone(),
|
||||||
|
})),
|
||||||
|
toolbar: cx.add_view(|_| Toolbar::new(handle)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn nav_history(&self) -> &Rc<RefCell<NavHistory>> {
|
pub fn nav_history_for_item<T: Item>(&self, item: &ViewHandle<T>) -> ItemNavHistory {
|
||||||
&self.nav_history
|
ItemNavHistory {
|
||||||
|
history: self.nav_history.clone(),
|
||||||
|
item: Rc::new(item.downgrade()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn activate(&self, cx: &mut ViewContext<Self>) {
|
pub fn activate(&self, cx: &mut ViewContext<Self>) {
|
||||||
|
@ -223,6 +234,26 @@ impl Pane {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn disable_history(&mut self) {
|
||||||
|
self.nav_history.borrow_mut().disable();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enable_history(&mut self) {
|
||||||
|
self.nav_history.borrow_mut().enable();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn can_navigate_backward(&self) -> bool {
|
||||||
|
!self.nav_history.borrow().backward_stack.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn can_navigate_forward(&self) -> bool {
|
||||||
|
!self.nav_history.borrow().forward_stack.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn history_updated(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
self.toolbar.update(cx, |_, cx| cx.notify());
|
||||||
|
}
|
||||||
|
|
||||||
fn navigate_history(
|
fn navigate_history(
|
||||||
workspace: &mut Workspace,
|
workspace: &mut Workspace,
|
||||||
pane: ViewHandle<Pane>,
|
pane: ViewHandle<Pane>,
|
||||||
|
@ -234,7 +265,7 @@ impl Pane {
|
||||||
let to_load = pane.update(cx, |pane, cx| {
|
let to_load = pane.update(cx, |pane, cx| {
|
||||||
loop {
|
loop {
|
||||||
// Retrieve the weak item handle from the history.
|
// Retrieve the weak item handle from the history.
|
||||||
let entry = pane.nav_history.borrow_mut().pop(mode)?;
|
let entry = pane.nav_history.borrow_mut().pop(mode, cx)?;
|
||||||
|
|
||||||
// If the item is still present in this pane, then activate it.
|
// If the item is still present in this pane, then activate it.
|
||||||
if let Some(index) = entry
|
if let Some(index) = entry
|
||||||
|
@ -367,7 +398,6 @@ impl Pane {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
item.set_nav_history(pane.read(cx).nav_history.clone(), cx);
|
|
||||||
item.added_to_pane(workspace, pane.clone(), cx);
|
item.added_to_pane(workspace, pane.clone(), cx);
|
||||||
pane.update(cx, |pane, cx| {
|
pane.update(cx, |pane, cx| {
|
||||||
// If there is already an active item, then insert the new item
|
// If there is already an active item, then insert the new item
|
||||||
|
@ -625,11 +655,16 @@ impl Pane {
|
||||||
.borrow_mut()
|
.borrow_mut()
|
||||||
.set_mode(NavigationMode::Normal);
|
.set_mode(NavigationMode::Normal);
|
||||||
|
|
||||||
let mut nav_history = pane.nav_history().borrow_mut();
|
|
||||||
if let Some(path) = item.project_path(cx) {
|
if let Some(path) = item.project_path(cx) {
|
||||||
nav_history.paths_by_item.insert(item.id(), path);
|
pane.nav_history
|
||||||
|
.borrow_mut()
|
||||||
|
.paths_by_item
|
||||||
|
.insert(item.id(), path);
|
||||||
} else {
|
} else {
|
||||||
nav_history.paths_by_item.remove(&item.id());
|
pane.nav_history
|
||||||
|
.borrow_mut()
|
||||||
|
.paths_by_item
|
||||||
|
.remove(&item.id());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -953,57 +988,56 @@ impl View for Pane {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ItemNavHistory {
|
impl ItemNavHistory {
|
||||||
pub fn new<T: Item>(history: Rc<RefCell<NavHistory>>, item: &ViewHandle<T>) -> Self {
|
pub fn push<D: 'static + Any>(&self, data: Option<D>, cx: &mut MutableAppContext) {
|
||||||
Self {
|
self.history.borrow_mut().push(data, self.item.clone(), cx);
|
||||||
history,
|
|
||||||
item: Rc::new(item.downgrade()),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn history(&self) -> Rc<RefCell<NavHistory>> {
|
pub fn pop_backward(&self, cx: &mut MutableAppContext) -> Option<NavigationEntry> {
|
||||||
self.history.clone()
|
self.history.borrow_mut().pop(NavigationMode::GoingBack, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn push<D: 'static + Any>(&self, data: Option<D>) {
|
pub fn pop_forward(&self, cx: &mut MutableAppContext) -> Option<NavigationEntry> {
|
||||||
self.history.borrow_mut().push(data, self.item.clone());
|
self.history
|
||||||
|
.borrow_mut()
|
||||||
|
.pop(NavigationMode::GoingForward, cx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NavHistory {
|
impl NavHistory {
|
||||||
pub fn disable(&mut self) {
|
|
||||||
self.mode = NavigationMode::Disabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn enable(&mut self) {
|
|
||||||
self.mode = NavigationMode::Normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pop_backward(&mut self) -> Option<NavigationEntry> {
|
|
||||||
self.backward_stack.pop_back()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pop_forward(&mut self) -> Option<NavigationEntry> {
|
|
||||||
self.forward_stack.pop_back()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pop_closed(&mut self) -> Option<NavigationEntry> {
|
|
||||||
self.closed_stack.pop_back()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn pop(&mut self, mode: NavigationMode) -> Option<NavigationEntry> {
|
|
||||||
match mode {
|
|
||||||
NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => None,
|
|
||||||
NavigationMode::GoingBack => self.pop_backward(),
|
|
||||||
NavigationMode::GoingForward => self.pop_forward(),
|
|
||||||
NavigationMode::ReopeningClosedItem => self.pop_closed(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_mode(&mut self, mode: NavigationMode) {
|
fn set_mode(&mut self, mode: NavigationMode) {
|
||||||
self.mode = mode;
|
self.mode = mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn push<D: 'static + Any>(&mut self, data: Option<D>, item: Rc<dyn WeakItemHandle>) {
|
fn disable(&mut self) {
|
||||||
|
self.mode = NavigationMode::Disabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enable(&mut self) {
|
||||||
|
self.mode = NavigationMode::Normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pop(&mut self, mode: NavigationMode, cx: &mut MutableAppContext) -> Option<NavigationEntry> {
|
||||||
|
let entry = match mode {
|
||||||
|
NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
|
||||||
|
return None
|
||||||
|
}
|
||||||
|
NavigationMode::GoingBack => &mut self.backward_stack,
|
||||||
|
NavigationMode::GoingForward => &mut self.forward_stack,
|
||||||
|
NavigationMode::ReopeningClosedItem => &mut self.closed_stack,
|
||||||
|
}
|
||||||
|
.pop_back();
|
||||||
|
if entry.is_some() {
|
||||||
|
self.did_update(cx);
|
||||||
|
}
|
||||||
|
entry
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push<D: 'static + Any>(
|
||||||
|
&mut self,
|
||||||
|
data: Option<D>,
|
||||||
|
item: Rc<dyn WeakItemHandle>,
|
||||||
|
cx: &mut MutableAppContext,
|
||||||
|
) {
|
||||||
match self.mode {
|
match self.mode {
|
||||||
NavigationMode::Disabled => {}
|
NavigationMode::Disabled => {}
|
||||||
NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
|
NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
|
||||||
|
@ -1044,5 +1078,12 @@ impl NavHistory {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
self.did_update(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn did_update(&self, cx: &mut MutableAppContext) {
|
||||||
|
if let Some(pane) = self.pane.upgrade(cx) {
|
||||||
|
cx.defer(move |cx| pane.update(cx, |pane, cx| pane.history_updated(cx)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -188,12 +188,13 @@ impl Sidebar {
|
||||||
})
|
})
|
||||||
.with_cursor_style(CursorStyle::ResizeLeftRight)
|
.with_cursor_style(CursorStyle::ResizeLeftRight)
|
||||||
.on_mouse_down(|_, _| {}) // This prevents the mouse down event from being propagated elsewhere
|
.on_mouse_down(|_, _| {}) // This prevents the mouse down event from being propagated elsewhere
|
||||||
.on_drag(move |delta, cx| {
|
.on_drag(move |old_position, new_position, cx| {
|
||||||
|
let delta = new_position.x() - old_position.x();
|
||||||
let prev_width = *actual_width.borrow();
|
let prev_width = *actual_width.borrow();
|
||||||
*custom_width.borrow_mut() = 0f32
|
*custom_width.borrow_mut() = 0f32
|
||||||
.max(match side {
|
.max(match side {
|
||||||
Side::Left => prev_width + delta.x(),
|
Side::Left => prev_width + delta,
|
||||||
Side::Right => prev_width - delta.x(),
|
Side::Right => prev_width - delta,
|
||||||
})
|
})
|
||||||
.round();
|
.round();
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::ItemHandle;
|
use crate::{ItemHandle, Pane};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
elements::*, AnyViewHandle, AppContext, ElementBox, Entity, MutableAppContext, RenderContext,
|
elements::*, platform::CursorStyle, Action, AnyViewHandle, AppContext, ElementBox, Entity,
|
||||||
View, ViewContext, ViewHandle,
|
MutableAppContext, RenderContext, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||||
};
|
};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
|
|
||||||
|
@ -42,6 +42,7 @@ pub enum ToolbarItemLocation {
|
||||||
|
|
||||||
pub struct Toolbar {
|
pub struct Toolbar {
|
||||||
active_pane_item: Option<Box<dyn ItemHandle>>,
|
active_pane_item: Option<Box<dyn ItemHandle>>,
|
||||||
|
pane: WeakViewHandle<Pane>,
|
||||||
items: Vec<(Box<dyn ToolbarItemViewHandle>, ToolbarItemLocation)>,
|
items: Vec<(Box<dyn ToolbarItemViewHandle>, ToolbarItemLocation)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,6 +61,7 @@ impl View for Toolbar {
|
||||||
let mut primary_left_items = Vec::new();
|
let mut primary_left_items = Vec::new();
|
||||||
let mut primary_right_items = Vec::new();
|
let mut primary_right_items = Vec::new();
|
||||||
let mut secondary_item = None;
|
let mut secondary_item = None;
|
||||||
|
let spacing = theme.item_spacing;
|
||||||
|
|
||||||
for (item, position) in &self.items {
|
for (item, position) in &self.items {
|
||||||
match *position {
|
match *position {
|
||||||
|
@ -68,7 +70,7 @@ impl View for Toolbar {
|
||||||
let left_item = ChildView::new(item.as_ref())
|
let left_item = ChildView::new(item.as_ref())
|
||||||
.aligned()
|
.aligned()
|
||||||
.contained()
|
.contained()
|
||||||
.with_margin_right(theme.item_spacing);
|
.with_margin_right(spacing);
|
||||||
if let Some((flex, expanded)) = flex {
|
if let Some((flex, expanded)) = flex {
|
||||||
primary_left_items.push(left_item.flex(flex, expanded).boxed());
|
primary_left_items.push(left_item.flex(flex, expanded).boxed());
|
||||||
} else {
|
} else {
|
||||||
|
@ -79,7 +81,7 @@ impl View for Toolbar {
|
||||||
let right_item = ChildView::new(item.as_ref())
|
let right_item = ChildView::new(item.as_ref())
|
||||||
.aligned()
|
.aligned()
|
||||||
.contained()
|
.contained()
|
||||||
.with_margin_left(theme.item_spacing)
|
.with_margin_left(spacing)
|
||||||
.flex_float();
|
.flex_float();
|
||||||
if let Some((flex, expanded)) = flex {
|
if let Some((flex, expanded)) = flex {
|
||||||
primary_right_items.push(right_item.flex(flex, expanded).boxed());
|
primary_right_items.push(right_item.flex(flex, expanded).boxed());
|
||||||
|
@ -98,26 +100,115 @@ impl View for Toolbar {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let pane = self.pane.clone();
|
||||||
|
let mut enable_go_backward = false;
|
||||||
|
let mut enable_go_forward = false;
|
||||||
|
if let Some(pane) = pane.upgrade(cx) {
|
||||||
|
let pane = pane.read(cx);
|
||||||
|
enable_go_backward = pane.can_navigate_backward();
|
||||||
|
enable_go_forward = pane.can_navigate_forward();
|
||||||
|
}
|
||||||
|
|
||||||
|
let container_style = theme.container;
|
||||||
|
let height = theme.height;
|
||||||
|
let button_style = theme.nav_button;
|
||||||
|
let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
|
||||||
|
|
||||||
Flex::column()
|
Flex::column()
|
||||||
.with_child(
|
.with_child(
|
||||||
Flex::row()
|
Flex::row()
|
||||||
|
.with_child(nav_button(
|
||||||
|
"icons/arrow-left.svg",
|
||||||
|
button_style,
|
||||||
|
tooltip_style.clone(),
|
||||||
|
enable_go_backward,
|
||||||
|
spacing,
|
||||||
|
super::GoBack {
|
||||||
|
pane: Some(pane.clone()),
|
||||||
|
},
|
||||||
|
super::GoBack { pane: None },
|
||||||
|
"Go Back",
|
||||||
|
cx,
|
||||||
|
))
|
||||||
|
.with_child(nav_button(
|
||||||
|
"icons/arrow-right.svg",
|
||||||
|
button_style,
|
||||||
|
tooltip_style.clone(),
|
||||||
|
enable_go_forward,
|
||||||
|
spacing,
|
||||||
|
super::GoForward {
|
||||||
|
pane: Some(pane.clone()),
|
||||||
|
},
|
||||||
|
super::GoForward { pane: None },
|
||||||
|
"Go Forward",
|
||||||
|
cx,
|
||||||
|
))
|
||||||
.with_children(primary_left_items)
|
.with_children(primary_left_items)
|
||||||
.with_children(primary_right_items)
|
.with_children(primary_right_items)
|
||||||
.constrained()
|
.constrained()
|
||||||
.with_height(theme.height)
|
.with_height(height)
|
||||||
.boxed(),
|
.boxed(),
|
||||||
)
|
)
|
||||||
.with_children(secondary_item)
|
.with_children(secondary_item)
|
||||||
.contained()
|
.contained()
|
||||||
.with_style(theme.container)
|
.with_style(container_style)
|
||||||
.boxed()
|
.boxed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn nav_button<A: Action + Clone>(
|
||||||
|
svg_path: &'static str,
|
||||||
|
style: theme::Interactive<theme::IconButton>,
|
||||||
|
tooltip_style: TooltipStyle,
|
||||||
|
enabled: bool,
|
||||||
|
spacing: f32,
|
||||||
|
action: A,
|
||||||
|
tooltip_action: A,
|
||||||
|
action_name: &str,
|
||||||
|
cx: &mut RenderContext<Toolbar>,
|
||||||
|
) -> ElementBox {
|
||||||
|
MouseEventHandler::new::<A, _, _>(0, cx, |state, _| {
|
||||||
|
let style = if enabled {
|
||||||
|
style.style_for(state, false)
|
||||||
|
} else {
|
||||||
|
style.disabled_style()
|
||||||
|
};
|
||||||
|
Svg::new(svg_path)
|
||||||
|
.with_color(style.color)
|
||||||
|
.constrained()
|
||||||
|
.with_width(style.icon_width)
|
||||||
|
.aligned()
|
||||||
|
.contained()
|
||||||
|
.with_style(style.container)
|
||||||
|
.constrained()
|
||||||
|
.with_width(style.button_width)
|
||||||
|
.with_height(style.button_width)
|
||||||
|
.aligned()
|
||||||
|
.boxed()
|
||||||
|
})
|
||||||
|
.with_cursor_style(if enabled {
|
||||||
|
CursorStyle::PointingHand
|
||||||
|
} else {
|
||||||
|
CursorStyle::default()
|
||||||
|
})
|
||||||
|
.on_click(move |_, _, cx| cx.dispatch_action(action.clone()))
|
||||||
|
.with_tooltip::<A, _>(
|
||||||
|
0,
|
||||||
|
action_name.to_string(),
|
||||||
|
Some(Box::new(tooltip_action)),
|
||||||
|
tooltip_style,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.contained()
|
||||||
|
.with_margin_right(spacing)
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
|
||||||
impl Toolbar {
|
impl Toolbar {
|
||||||
pub fn new() -> Self {
|
pub fn new(pane: WeakViewHandle<Pane>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
active_pane_item: None,
|
active_pane_item: None,
|
||||||
|
pane,
|
||||||
items: Default::default(),
|
items: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -414,7 +414,6 @@ pub trait ItemHandle: 'static + fmt::Debug {
|
||||||
fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]>;
|
fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]>;
|
||||||
fn is_singleton(&self, cx: &AppContext) -> bool;
|
fn is_singleton(&self, cx: &AppContext) -> bool;
|
||||||
fn boxed_clone(&self) -> Box<dyn ItemHandle>;
|
fn boxed_clone(&self) -> Box<dyn ItemHandle>;
|
||||||
fn set_nav_history(&self, nav_history: Rc<RefCell<NavHistory>>, cx: &mut MutableAppContext);
|
|
||||||
fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemHandle>>;
|
fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemHandle>>;
|
||||||
fn added_to_pane(
|
fn added_to_pane(
|
||||||
&self,
|
&self,
|
||||||
|
@ -484,12 +483,6 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
|
||||||
Box::new(self.clone())
|
Box::new(self.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_nav_history(&self, nav_history: Rc<RefCell<NavHistory>>, cx: &mut MutableAppContext) {
|
|
||||||
self.update(cx, |item, cx| {
|
|
||||||
item.set_nav_history(ItemNavHistory::new(nav_history, &cx.handle()), cx);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemHandle>> {
|
fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemHandle>> {
|
||||||
self.update(cx, |item, cx| {
|
self.update(cx, |item, cx| {
|
||||||
cx.add_option_view(|cx| item.clone_on_split(cx))
|
cx.add_option_view(|cx| item.clone_on_split(cx))
|
||||||
|
@ -503,6 +496,9 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
|
||||||
pane: ViewHandle<Pane>,
|
pane: ViewHandle<Pane>,
|
||||||
cx: &mut ViewContext<Workspace>,
|
cx: &mut ViewContext<Workspace>,
|
||||||
) {
|
) {
|
||||||
|
let history = pane.read(cx).nav_history_for_item(self);
|
||||||
|
self.update(cx, |this, cx| this.set_nav_history(history, cx));
|
||||||
|
|
||||||
if let Some(followed_item) = self.to_followable_item_handle(cx) {
|
if let Some(followed_item) = self.to_followable_item_handle(cx) {
|
||||||
if let Some(message) = followed_item.to_state_proto(cx) {
|
if let Some(message) = followed_item.to_state_proto(cx) {
|
||||||
workspace.update_followers(
|
workspace.update_followers(
|
||||||
|
@ -2360,7 +2356,12 @@ impl Workspace {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
|
fn on_window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
|
||||||
if !active && cx.global::<Settings>().autosave == Autosave::OnWindowChange {
|
if !active
|
||||||
|
&& matches!(
|
||||||
|
cx.global::<Settings>().autosave,
|
||||||
|
Autosave::OnWindowChange | Autosave::OnFocusChange
|
||||||
|
)
|
||||||
|
{
|
||||||
for pane in &self.panes {
|
for pane in &self.panes {
|
||||||
pane.update(cx, |pane, cx| {
|
pane.update(cx, |pane, cx| {
|
||||||
for item in pane.items() {
|
for item in pane.items() {
|
||||||
|
@ -3073,6 +3074,17 @@ mod tests {
|
||||||
deterministic.run_until_parked();
|
deterministic.run_until_parked();
|
||||||
item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
|
item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
|
||||||
|
|
||||||
|
// Deactivating the window still saves the file.
|
||||||
|
cx.simulate_window_activation(Some(window_id));
|
||||||
|
item.update(cx, |item, cx| {
|
||||||
|
cx.focus_self();
|
||||||
|
item.is_dirty = true;
|
||||||
|
});
|
||||||
|
cx.simulate_window_activation(None);
|
||||||
|
|
||||||
|
deterministic.run_until_parked();
|
||||||
|
item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
|
||||||
|
|
||||||
// Autosave after delay.
|
// Autosave after delay.
|
||||||
item.update(cx, |item, cx| {
|
item.update(cx, |item, cx| {
|
||||||
cx.update_global(|settings: &mut Settings, _| {
|
cx.update_global(|settings: &mut Settings, _| {
|
||||||
|
@ -3084,11 +3096,11 @@ mod tests {
|
||||||
|
|
||||||
// Delay hasn't fully expired, so the file is still dirty and unsaved.
|
// Delay hasn't fully expired, so the file is still dirty and unsaved.
|
||||||
deterministic.advance_clock(Duration::from_millis(250));
|
deterministic.advance_clock(Duration::from_millis(250));
|
||||||
item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
|
item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
|
||||||
|
|
||||||
// After delay expires, the file is saved.
|
// After delay expires, the file is saved.
|
||||||
deterministic.advance_clock(Duration::from_millis(250));
|
deterministic.advance_clock(Duration::from_millis(250));
|
||||||
item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
|
item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
|
||||||
|
|
||||||
// Autosave on focus change, ensuring closing the tab counts as such.
|
// Autosave on focus change, ensuring closing the tab counts as such.
|
||||||
item.update(cx, |item, cx| {
|
item.update(cx, |item, cx| {
|
||||||
|
@ -3106,7 +3118,7 @@ mod tests {
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(!cx.has_pending_prompt(window_id));
|
assert!(!cx.has_pending_prompt(window_id));
|
||||||
item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
|
item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
|
||||||
|
|
||||||
// Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
|
// Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
|
||||||
workspace.update(cx, |workspace, cx| {
|
workspace.update(cx, |workspace, cx| {
|
||||||
|
@ -3118,7 +3130,7 @@ mod tests {
|
||||||
cx.blur();
|
cx.blur();
|
||||||
});
|
});
|
||||||
deterministic.run_until_parked();
|
deterministic.run_until_parked();
|
||||||
item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
|
item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
|
||||||
|
|
||||||
// Ensure autosave is prevented for deleted files also when closing the buffer.
|
// Ensure autosave is prevented for deleted files also when closing the buffer.
|
||||||
let _close_items = workspace.update(cx, |workspace, cx| {
|
let _close_items = workspace.update(cx, |workspace, cx| {
|
||||||
|
@ -3127,28 +3139,107 @@ mod tests {
|
||||||
});
|
});
|
||||||
deterministic.run_until_parked();
|
deterministic.run_until_parked();
|
||||||
assert!(cx.has_pending_prompt(window_id));
|
assert!(cx.has_pending_prompt(window_id));
|
||||||
item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
|
item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_pane_navigation(
|
||||||
|
deterministic: Arc<Deterministic>,
|
||||||
|
cx: &mut gpui::TestAppContext,
|
||||||
|
) {
|
||||||
|
deterministic.forbid_parking();
|
||||||
|
Settings::test_async(cx);
|
||||||
|
let fs = FakeFs::new(cx.background());
|
||||||
|
|
||||||
|
let project = Project::test(fs, [], cx).await;
|
||||||
|
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
|
||||||
|
|
||||||
|
let item = cx.add_view(window_id, |_| {
|
||||||
|
let mut item = TestItem::new();
|
||||||
|
item.project_entry_ids = vec![ProjectEntryId::from_proto(1)];
|
||||||
|
item
|
||||||
|
});
|
||||||
|
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
|
||||||
|
let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone());
|
||||||
|
let toolbar_notify_count = Rc::new(RefCell::new(0));
|
||||||
|
|
||||||
|
workspace.update(cx, |workspace, cx| {
|
||||||
|
workspace.add_item(Box::new(item.clone()), cx);
|
||||||
|
let toolbar_notification_count = toolbar_notify_count.clone();
|
||||||
|
cx.observe(&toolbar, move |_, _, _| {
|
||||||
|
*toolbar_notification_count.borrow_mut() += 1
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
});
|
||||||
|
|
||||||
|
pane.read_with(cx, |pane, _| {
|
||||||
|
assert!(!pane.can_navigate_backward());
|
||||||
|
assert!(!pane.can_navigate_forward());
|
||||||
|
});
|
||||||
|
|
||||||
|
item.update(cx, |item, cx| {
|
||||||
|
item.set_state("one".to_string(), cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Toolbar must be notified to re-render the navigation buttons
|
||||||
|
assert_eq!(*toolbar_notify_count.borrow(), 1);
|
||||||
|
|
||||||
|
pane.read_with(cx, |pane, _| {
|
||||||
|
assert!(pane.can_navigate_backward());
|
||||||
|
assert!(!pane.can_navigate_forward());
|
||||||
|
});
|
||||||
|
|
||||||
|
workspace
|
||||||
|
.update(cx, |workspace, cx| {
|
||||||
|
Pane::go_back(workspace, Some(pane.clone()), cx)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(*toolbar_notify_count.borrow(), 3);
|
||||||
|
pane.read_with(cx, |pane, _| {
|
||||||
|
assert!(!pane.can_navigate_backward());
|
||||||
|
assert!(pane.can_navigate_forward());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct TestItem {
|
struct TestItem {
|
||||||
|
state: String,
|
||||||
save_count: usize,
|
save_count: usize,
|
||||||
save_as_count: usize,
|
save_as_count: usize,
|
||||||
reload_count: usize,
|
reload_count: usize,
|
||||||
is_dirty: bool,
|
is_dirty: bool,
|
||||||
|
is_singleton: bool,
|
||||||
has_conflict: bool,
|
has_conflict: bool,
|
||||||
project_entry_ids: Vec<ProjectEntryId>,
|
project_entry_ids: Vec<ProjectEntryId>,
|
||||||
project_path: Option<ProjectPath>,
|
project_path: Option<ProjectPath>,
|
||||||
is_singleton: bool,
|
nav_history: Option<ItemNavHistory>,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum TestItemEvent {
|
enum TestItemEvent {
|
||||||
Edit,
|
Edit,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Clone for TestItem {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
state: self.state.clone(),
|
||||||
|
save_count: self.save_count,
|
||||||
|
save_as_count: self.save_as_count,
|
||||||
|
reload_count: self.reload_count,
|
||||||
|
is_dirty: self.is_dirty,
|
||||||
|
is_singleton: self.is_singleton,
|
||||||
|
has_conflict: self.has_conflict,
|
||||||
|
project_entry_ids: self.project_entry_ids.clone(),
|
||||||
|
project_path: self.project_path.clone(),
|
||||||
|
nav_history: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl TestItem {
|
impl TestItem {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
state: String::new(),
|
||||||
save_count: 0,
|
save_count: 0,
|
||||||
save_as_count: 0,
|
save_as_count: 0,
|
||||||
reload_count: 0,
|
reload_count: 0,
|
||||||
|
@ -3157,6 +3248,18 @@ mod tests {
|
||||||
project_entry_ids: Vec::new(),
|
project_entry_ids: Vec::new(),
|
||||||
project_path: None,
|
project_path: None,
|
||||||
is_singleton: true,
|
is_singleton: true,
|
||||||
|
nav_history: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_state(&mut self, state: String, cx: &mut ViewContext<Self>) {
|
||||||
|
self.push_to_nav_history(cx);
|
||||||
|
self.state = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_to_nav_history(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
if let Some(history) = &mut self.nav_history {
|
||||||
|
history.push(Some(Box::new(self.state.clone())), cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3192,7 +3295,23 @@ mod tests {
|
||||||
self.is_singleton
|
self.is_singleton
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_nav_history(&mut self, _: ItemNavHistory, _: &mut ViewContext<Self>) {}
|
fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext<Self>) {
|
||||||
|
self.nav_history = Some(history);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn navigate(&mut self, state: Box<dyn Any>, _: &mut ViewContext<Self>) -> bool {
|
||||||
|
let state = *state.downcast::<String>().unwrap_or_default();
|
||||||
|
if state != self.state {
|
||||||
|
self.state = state;
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
self.push_to_nav_history(cx);
|
||||||
|
}
|
||||||
|
|
||||||
fn clone_on_split(&self, _: &mut ViewContext<Self>) -> Option<Self>
|
fn clone_on_split(&self, _: &mut ViewContext<Self>) -> Option<Self>
|
||||||
where
|
where
|
||||||
|
|
|
@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
|
||||||
description = "The fast, collaborative code editor."
|
description = "The fast, collaborative code editor."
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
name = "zed"
|
name = "zed"
|
||||||
version = "0.44.1"
|
version = "0.45.0"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "zed"
|
name = "zed"
|
||||||
|
|
|
@ -554,7 +554,7 @@ mod tests {
|
||||||
});
|
});
|
||||||
|
|
||||||
let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
|
let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
|
||||||
app_state.fs.as_fake().insert_dir("/root").await;
|
app_state.fs.create_dir(Path::new("/root")).await.unwrap();
|
||||||
cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name")));
|
cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name")));
|
||||||
save_task.await.unwrap();
|
save_task.await.unwrap();
|
||||||
editor.read_with(cx, |editor, cx| {
|
editor.read_with(cx, |editor, cx| {
|
||||||
|
@ -680,14 +680,25 @@ mod tests {
|
||||||
async fn test_open_paths(cx: &mut TestAppContext) {
|
async fn test_open_paths(cx: &mut TestAppContext) {
|
||||||
let app_state = init(cx);
|
let app_state = init(cx);
|
||||||
|
|
||||||
let fs = app_state.fs.as_fake();
|
app_state
|
||||||
fs.insert_dir("/dir1").await;
|
.fs
|
||||||
fs.insert_dir("/dir2").await;
|
.as_fake()
|
||||||
fs.insert_dir("/dir3").await;
|
.insert_tree(
|
||||||
fs.insert_file("/dir1/a.txt", "".into()).await;
|
"/",
|
||||||
fs.insert_file("/dir2/b.txt", "".into()).await;
|
json!({
|
||||||
fs.insert_file("/dir3/c.txt", "".into()).await;
|
"dir1": {
|
||||||
fs.insert_file("/d.txt", "".into()).await;
|
"a.txt": ""
|
||||||
|
},
|
||||||
|
"dir2": {
|
||||||
|
"b.txt": ""
|
||||||
|
},
|
||||||
|
"dir3": {
|
||||||
|
"c.txt": ""
|
||||||
|
},
|
||||||
|
"d.txt": ""
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
let project = Project::test(app_state.fs.clone(), ["/dir1".as_ref()], cx).await;
|
let project = Project::test(app_state.fs.clone(), ["/dir1".as_ref()], cx).await;
|
||||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
|
let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
|
||||||
|
@ -891,7 +902,7 @@ mod tests {
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_open_and_save_new_file(cx: &mut TestAppContext) {
|
async fn test_open_and_save_new_file(cx: &mut TestAppContext) {
|
||||||
let app_state = init(cx);
|
let app_state = init(cx);
|
||||||
app_state.fs.as_fake().insert_dir("/root").await;
|
app_state.fs.create_dir(Path::new("/root")).await.unwrap();
|
||||||
|
|
||||||
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
|
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
|
||||||
project.update(cx, |project, _| project.languages().add(rust_lang()));
|
project.update(cx, |project, _| project.languages().add(rust_lang()));
|
||||||
|
@ -980,7 +991,7 @@ mod tests {
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) {
|
async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) {
|
||||||
let app_state = init(cx);
|
let app_state = init(cx);
|
||||||
app_state.fs.as_fake().insert_dir("/root").await;
|
app_state.fs.create_dir(Path::new("/root")).await.unwrap();
|
||||||
|
|
||||||
let project = Project::test(app_state.fs.clone(), [], cx).await;
|
let project = Project::test(app_state.fs.clone(), [], cx).await;
|
||||||
project.update(cx, |project, _| project.languages().add(rust_lang()));
|
project.update(cx, |project, _| project.languages().add(rust_lang()));
|
||||||
|
|
1
pbcpoy
Normal file
1
pbcpoy
Normal file
|
@ -0,0 +1 @@
|
||||||
|
A
|
1
styles/package-lock.json
generated
1
styles/package-lock.json
generated
|
@ -5,7 +5,6 @@
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "styles",
|
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
@ -139,6 +139,19 @@ export default function workspace(theme: Theme) {
|
||||||
background: backgroundColor(theme, 500),
|
background: backgroundColor(theme, 500),
|
||||||
border: border(theme, "secondary", { bottom: true }),
|
border: border(theme, "secondary", { bottom: true }),
|
||||||
itemSpacing: 8,
|
itemSpacing: 8,
|
||||||
|
navButton: {
|
||||||
|
color: iconColor(theme, "secondary"),
|
||||||
|
iconWidth: 8,
|
||||||
|
buttonWidth: 18,
|
||||||
|
cornerRadius: 6,
|
||||||
|
hover: {
|
||||||
|
color: iconColor(theme, "active"),
|
||||||
|
background: backgroundColor(theme, 300),
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
color: withOpacity(iconColor(theme, "muted"), 0.6),
|
||||||
|
},
|
||||||
|
},
|
||||||
padding: { left: 16, right: 8, top: 4, bottom: 4 },
|
padding: { left: 16, right: 8, top: 4, bottom: 4 },
|
||||||
},
|
},
|
||||||
breadcrumbs: {
|
breadcrumbs: {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue