diff --git a/Cargo.lock b/Cargo.lock index 7dcecd009390c92a0602ae3fc70c421be27c6f1d..0fd0c8a212eb84638b1a4659f1f80b1a732bfb1a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4878,6 +4878,8 @@ name = "terminal" version = "0.1.0" dependencies = [ "alacritty_terminal", + "client", + "dirs 4.0.0", "editor", "futures", "gpui", @@ -6159,7 +6161,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.44.1" +version = "0.45.0" dependencies = [ "activity_indicator", "anyhow", diff --git a/assets/icons/arrow-left.svg b/assets/icons/arrow-left.svg new file mode 100644 index 0000000000000000000000000000000000000000..904fdaa1a73221efda6946614db7c707dc74c2ed --- /dev/null +++ b/assets/icons/arrow-left.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/arrow-right.svg b/assets/icons/arrow-right.svg new file mode 100644 index 0000000000000000000000000000000000000000..b7e1bec6d8a0830b8ebfcf677cd1a5b3480ce5b0 --- /dev/null +++ b/assets/icons/arrow-right.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index bea53ece45b2a672879453bb522fd64814e635f3..6cd3660bf5120d4b16f1f6988588a537b7b92a31 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -417,7 +417,8 @@ "up": "terminal::Up", "down": "terminal::Down", "tab": "terminal::Tab", - "cmd-v": "terminal::Paste" + "cmd-v": "terminal::Paste", + "cmd-c": "terminal::Copy" } } ] \ No newline at end of file diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 538b0fa4b0101be73c64d366faf09238e5d8da16..0e9ec4076ad43754a53e11f833935c9b807f025c 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -549,7 +549,7 @@ impl Client { client.respond_with_error( receipt, proto::Error { - message: error.to_string(), + message: format!("{:?}", error), }, )?; Err(error) diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index a4c8386b13fb58a88fd2a4a0f1d5664ed9226852..7767b361c1bbbd327ae765ab62a8308ca3cb6b61 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -35,7 +35,7 @@ use project::{ use rand::prelude::*; use rpc::PeerId; use serde_json::json; -use settings::Settings; +use settings::{FormatOnSave, Settings}; use sqlx::types::time::OffsetDateTime; use std::{ cell::RefCell, @@ -1912,7 +1912,6 @@ async fn test_reloading_buffer_manually(cx_a: &mut TestAppContext, cx_b: &mut Te #[gpui::test(iterations = 10)] 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 client_a = server.create_client(cx_a, "user_a").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()); 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 .fs - .insert_tree("/a", json!({ "a.rs": "let one = two" })) + .insert_tree(&directory, json!({ "a.rs": "let one = \"two\"" })) .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 buffer_b = cx_b @@ -1967,7 +1970,28 @@ async fn test_formatting_buffer(cx_a: &mut TestAppContext, cx_b: &mut TestAppCon .unwrap(); assert_eq!( 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()) ); } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 9dd40413fd254e7ef852643acfdb5cbe464ee415..808926ff50f3e71b157f6b8380980fc1bd109044 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -4065,13 +4065,16 @@ impl Editor { } } - nav_history.push(Some(NavigationData { - cursor_anchor: position, - cursor_position: point, - scroll_position: self.scroll_position, - scroll_top_anchor: self.scroll_top_anchor.clone(), - scroll_top_row, - })); + nav_history.push( + Some(NavigationData { + cursor_anchor: position, + cursor_position: point, + scroll_position: self.scroll_position, + scroll_top_anchor: self.scroll_top_anchor.clone(), + scroll_top_row, + }), + cx, + ); } } @@ -4669,7 +4672,7 @@ impl Editor { definitions: Vec, cx: &mut ViewContext, ) { - let nav_history = workspace.active_pane().read(cx).nav_history().clone(); + let pane = workspace.active_pane().clone(); for definition in definitions { let range = definition .target @@ -4681,13 +4684,13 @@ impl Editor { // When selecting a definition in a different buffer, disable the nav history // to avoid creating a history entry at the previous cursor location. 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| { 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.push_to_nav_history(editor.selections.newest_anchor().head(), None, cx); }); - let nav_history = workspace.active_pane().read(cx).nav_history().clone(); - nav_history.borrow_mut().disable(); + let pane = workspace.active_pane().clone(); + pane.update(cx, |pane, _| pane.disable_history()); // 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, @@ -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, 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] fn test_edit_events(cx: &mut MutableAppContext) { @@ -6589,12 +6592,20 @@ mod tests { fn test_navigation_history(cx: &mut gpui::MutableAppContext) { cx.set_global(Settings::test(cx)); 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); cx.add_window(Default::default(), |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 { + editor.nav_history.as_mut().unwrap().pop_backward(cx) + } // Move the cursor a small distance. // Nothing is added to the navigation history. @@ -6604,21 +6615,21 @@ mod tests { editor.change_selections(None, cx, |s| { 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. // The history can jump back to the previous position. editor.change_selections(None, cx, |s| { 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); assert_eq!(nav_entry.item.id(), cx.view_id()); assert_eq!( editor.selections.display_ranges(cx), &[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. // Nothing is added to the navigation history. @@ -6628,7 +6639,7 @@ mod tests { editor.selections.display_ranges(cx), &[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. // The history can jump back to the previous position. @@ -6638,14 +6649,14 @@ mod tests { editor.selections.display_ranges(cx), &[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); assert_eq!(nav_entry.item.id(), cx.view_id()); assert_eq!( editor.selections.display_ranges(cx), &[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 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_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); assert_eq!(editor.scroll_position, original_scroll_position); assert_eq!(editor.scroll_top_anchor, original_scroll_top_anchor); @@ -8221,7 +8232,7 @@ mod tests { fox ju|mps over the lazy dog"}); 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 // before the empty selections but replaces the selection that is non-empty. diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 1169df3fd1d72f725a72beaa81154c58e9bd4884..56f664566ecd03bba53cbd7960e3ac5be8d33287 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -23,8 +23,9 @@ use gpui::{ json::{self, ToJson}, platform::CursorStyle, text_layout::{self, Line, RunStyle, TextLayoutCache}, - AppContext, Axis, Border, CursorRegion, Element, ElementBox, Event, EventContext, - LayoutContext, MutableAppContext, PaintContext, Quad, Scene, SizeConstraint, ViewContext, + AppContext, Axis, Border, CursorRegion, Element, ElementBox, Event, EventContext, KeyDownEvent, + LayoutContext, ModifiersChangedEvent, MouseButton, MouseEvent, MouseMovedEvent, + MutableAppContext, PaintContext, Quad, Scene, ScrollWheelEvent, SizeConstraint, ViewContext, WeakViewHandle, }; use json::json; @@ -1463,14 +1464,15 @@ impl Element for EditorElement { } match event { - Event::LeftMouseDown { + Event::MouseDown(MouseEvent { + button: MouseButton::Left, position, cmd, alt, shift, click_count, .. - } => self.mouse_down( + }) => self.mouse_down( *position, *cmd, *alt, @@ -1480,18 +1482,26 @@ impl Element for EditorElement { paint, cx, ), - Event::LeftMouseUp { position, .. } => self.mouse_up(*position, cx), - Event::LeftMouseDragged { position, .. } => { - self.mouse_dragged(*position, layout, paint, cx) - } - Event::ScrollWheel { + Event::MouseUp(MouseEvent { + button: MouseButton::Left, + position, + .. + }) => 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, delta, precise, - } => self.scroll(*position, *delta, *precise, layout, paint, cx), - Event::KeyDown { input, .. } => self.key_down(input.as_deref(), cx), - Event::ModifiersChanged { cmd, .. } => self.modifiers_changed(*cmd, cx), - Event::MouseMoved { position, cmd, .. } => { + }) => self.scroll(*position, *delta, *precise, layout, paint, cx), + Event::KeyDown(KeyDownEvent { input, .. }) => self.key_down(input.as_deref(), cx), + Event::ModifiersChanged(ModifiersChangedEvent { cmd, .. }) => { + self.modifiers_changed(*cmd, cx) + } + Event::MouseMoved(MouseMovedEvent { position, cmd, .. }) => { self.mouse_moved(*position, *cmd, layout, paint, cx) } @@ -1685,22 +1695,22 @@ impl Cursor { } #[derive(Debug)] -struct HighlightedRange { - start_y: f32, - line_height: f32, - lines: Vec, - color: Color, - corner_radius: f32, +pub struct HighlightedRange { + pub start_y: f32, + pub line_height: f32, + pub lines: Vec, + pub color: Color, + pub corner_radius: f32, } #[derive(Debug)] -struct HighlightedRangeLine { - start_x: f32, - end_x: f32, +pub struct HighlightedRangeLine { + pub start_x: f32, + pub end_x: f32, } 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 { self.paint_lines(self.start_y, &self.lines[0..1], bounds, scene); self.paint_lines( diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index f7aa80beaa6aa9219d42e091d258306781aef62c..0e3aca1447043aaba3354fb925babcbaa324b35f 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -352,13 +352,8 @@ impl Item for Editor { project: ModelHandle, cx: &mut ViewContext, ) -> Task> { - let settings = cx.global::(); let buffer = self.buffer().clone(); - let mut 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 buffers = buffer.read(cx).all_buffers(); let mut timeout = cx.background().timer(FORMAT_TIMEOUT).fuse(); let format = project.update(cx, |project, cx| project.format(buffers, true, cx)); cx.spawn(|this, mut cx| async move { diff --git a/crates/editor/src/test.rs b/crates/editor/src/test.rs index d1316a85a0bf38afe5c39cb596b6647b89119252..0affe06f64b555890ea3b6289ec497ace6aeb18d 100644 --- a/crates/editor/src/test.rs +++ b/crates/editor/src/test.rs @@ -404,14 +404,6 @@ impl<'a> EditorTestContext<'a> { 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> { diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index fd447e2469fc24c538bc7a05ab77fe64460a0a36..901c131d032bd6683951c866b7000bb2f4b4fb4a 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -4,7 +4,7 @@ use crate::{ elements::ElementBox, executor::{self, Task}, keymap::{self, Binding, Keystroke}, - platform::{self, Platform, PromptLevel, WindowOptions}, + platform::{self, KeyDownEvent, Platform, PromptLevel, WindowOptions}, presenter::Presenter, util::post_inc, AssetCache, AssetSource, ClipboardItem, FontCache, MouseRegionId, PathPromptOptions, @@ -151,6 +151,7 @@ pub struct AsyncAppContext(Rc>); pub struct TestAppContext { cx: Rc>, foreground_platform: Rc, + condition_duration: Option, } impl App { @@ -337,6 +338,7 @@ impl TestAppContext { let cx = TestAppContext { cx: Rc::new(RefCell::new(cx)), foreground_platform, + condition_duration: None, }; cx.cx.borrow_mut().weak_self = Some(Rc::downgrade(&cx.cx)); cx @@ -377,11 +379,11 @@ impl TestAppContext { if !cx.dispatch_keystroke(window_id, dispatch_path, &keystroke) { presenter.borrow_mut().dispatch_event( - Event::KeyDown { + Event::KeyDown(KeyDownEvent { keystroke, input, is_held, - }, + }), cx, ); } @@ -612,6 +614,27 @@ impl TestAppContext { 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 { @@ -1820,7 +1843,7 @@ impl MutableAppContext { window.on_event(Box::new(move |event| { app.update(|cx| { if let Some(presenter) = presenter.upgrade() { - if let Event::KeyDown { keystroke, .. } = &event { + if let Event::KeyDown(KeyDownEvent { keystroke, .. }) = &event { if cx.dispatch_keystroke( window_id, presenter.borrow().dispatch_path(cx.as_ref()), @@ -4424,6 +4447,7 @@ impl ViewHandle { use postage::prelude::{Sink as _, Stream as _}; let (tx, mut rx) = postage::mpsc::channel(1024); + let timeout_duration = cx.condition_duration(); let mut cx = cx.cx.borrow_mut(); let subscriptions = self.update(&mut *cx, |_, cx| { @@ -4445,14 +4469,9 @@ impl ViewHandle { let cx = cx.weak_self.as_ref().unwrap().upgrade().unwrap(); let handle = self.downgrade(); - let duration = if std::env::var("CI").is_ok() { - Duration::from_secs(2) - } else { - Duration::from_millis(500) - }; async move { - crate::util::timeout(duration, async move { + crate::util::timeout(timeout_duration, async move { loop { { let cx = cx.borrow(); @@ -5381,7 +5400,7 @@ impl RefCounts { #[cfg(test)] mod tests { use super::*; - use crate::{actions, elements::*, impl_actions}; + use crate::{actions, elements::*, impl_actions, MouseButton, MouseEvent}; use serde::Deserialize; use smol::future::poll_once; use std::{ @@ -5734,14 +5753,15 @@ mod tests { let presenter = cx.presenters_and_platform_windows[&window_id].0.clone(); // Ensure window's root element is in a valid lifecycle state. presenter.borrow_mut().dispatch_event( - Event::LeftMouseDown { + Event::MouseDown(MouseEvent { position: Default::default(), + button: MouseButton::Left, ctrl: false, alt: false, shift: false, cmd: false, click_count: 1, - }, + }), cx, ); assert_eq!(mouse_down_count.load(SeqCst), 1); diff --git a/crates/gpui/src/elements/event_handler.rs b/crates/gpui/src/elements/event_handler.rs index 7144b21dd0f19ed42dd9403e40173fdec4cf7506..1fec838788e28217d94671877cae26439e03b467 100644 --- a/crates/gpui/src/elements/event_handler.rs +++ b/crates/gpui/src/elements/event_handler.rs @@ -1,6 +1,7 @@ use crate::{ 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 serde_json::json; @@ -90,7 +91,7 @@ impl Element for EventHandler { click: Some(Rc::new(|_, _, _| {})), right_mouse_down: Some(Rc::new(|_, _| {})), right_click: Some(Rc::new(|_, _, _| {})), - drag: Some(Rc::new(|_, _| {})), + drag: Some(Rc::new(|_, _, _| {})), mouse_down_out: Some(Rc::new(|_, _| {})), right_mouse_down_out: Some(Rc::new(|_, _| {})), }); @@ -116,7 +117,11 @@ impl Element for EventHandler { true } else { match event { - Event::LeftMouseDown { position, .. } => { + Event::MouseDown(MouseEvent { + button: MouseButton::Left, + position, + .. + }) => { if let Some(callback) = self.mouse_down.as_mut() { if visible_bounds.contains_point(*position) { return callback(cx); @@ -124,7 +129,11 @@ impl Element for EventHandler { } false } - Event::RightMouseDown { position, .. } => { + Event::MouseDown(MouseEvent { + button: MouseButton::Right, + position, + .. + }) => { if let Some(callback) = self.right_mouse_down.as_mut() { if visible_bounds.contains_point(*position) { return callback(cx); @@ -132,11 +141,11 @@ impl Element for EventHandler { } false } - Event::NavigateMouseDown { + Event::MouseDown(MouseEvent { + button: MouseButton::Navigate(direction), position, - direction, .. - } => { + }) => { if let Some(callback) = self.navigate_mouse_down.as_mut() { if visible_bounds.contains_point(*position) { return callback(*direction, cx); diff --git a/crates/gpui/src/elements/flex.rs b/crates/gpui/src/elements/flex.rs index 82a6299d10d07537cca26e179902e2b5b9af4f84..cb43c1db68e87112b7f991171c00e72119c757bd 100644 --- a/crates/gpui/src/elements/flex.rs +++ b/crates/gpui/src/elements/flex.rs @@ -3,7 +3,8 @@ use std::{any::Any, f32::INFINITY}; use crate::{ json::{self, ToJson, Value}, Axis, DebugContext, Element, ElementBox, ElementStateHandle, Event, EventContext, - LayoutContext, PaintContext, RenderContext, SizeConstraint, Vector2FExt, View, + LayoutContext, MouseMovedEvent, PaintContext, RenderContext, ScrollWheelEvent, SizeConstraint, + Vector2FExt, View, }; use pathfinder_geometry::{ rect::RectF, @@ -287,11 +288,11 @@ impl Element for Flex { handled = child.dispatch_event(event, cx) || handled; } if !handled { - if let &Event::ScrollWheel { + if let &Event::ScrollWheel(ScrollWheelEvent { position, delta, precise, - } = event + }) = event { if *remaining_space < 0. && bounds.contains_point(position) { if let Some(scroll_state) = self.scroll_state.as_ref() { @@ -321,7 +322,7 @@ impl Element for Flex { } 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 // propogating it to the element below. if self.scroll_state.is_some() && bounds.contains_point(position) { diff --git a/crates/gpui/src/elements/list.rs b/crates/gpui/src/elements/list.rs index 6479f2ee283dac64c6b815f47b16131f05ec8796..e368b45288ce5490bb16ad121943b3d3ea5c4ab2 100644 --- a/crates/gpui/src/elements/list.rs +++ b/crates/gpui/src/elements/list.rs @@ -5,7 +5,7 @@ use crate::{ }, json::json, 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 sum_tree::{Bias, SumTree}; @@ -311,11 +311,11 @@ impl Element for List { state.items = new_items; match event { - Event::ScrollWheel { + Event::ScrollWheel(ScrollWheelEvent { position, delta, precise, - } => { + }) => { if bounds.contains_point(*position) { if state.scroll(scroll_top, bounds.height(), *delta, *precise, cx) { handled = true; diff --git a/crates/gpui/src/elements/mouse_event_handler.rs b/crates/gpui/src/elements/mouse_event_handler.rs index 8f70daf9e6d695a5c91bc9e1ecbbe508e5b878ae..832aafaa9e8b890cd613027c982cd2ef841c266e 100644 --- a/crates/gpui/src/elements/mouse_event_handler.rs +++ b/crates/gpui/src/elements/mouse_event_handler.rs @@ -24,7 +24,7 @@ pub struct MouseEventHandler { right_click: Option>, mouse_down_out: Option>, right_mouse_down_out: Option>, - drag: Option>, + drag: Option>, hover: Option>, padding: Padding, } @@ -106,7 +106,10 @@ impl MouseEventHandler { 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 } diff --git a/crates/gpui/src/elements/uniform_list.rs b/crates/gpui/src/elements/uniform_list.rs index de217a017c7699bddad0d991fadca9cf72ba02a9..9b2d966a7d6cc44b39fee390747fe7d28f4265b8 100644 --- a/crates/gpui/src/elements/uniform_list.rs +++ b/crates/gpui/src/elements/uniform_list.rs @@ -5,7 +5,7 @@ use crate::{ vector::{vec2f, Vector2F}, }, json::{self, json}, - ElementBox, RenderContext, View, + ElementBox, RenderContext, ScrollWheelEvent, View, }; use json::ToJson; use std::{cell::RefCell, cmp, ops::Range, rc::Rc}; @@ -310,11 +310,11 @@ impl Element for UniformList { } match event { - Event::ScrollWheel { + Event::ScrollWheel(ScrollWheelEvent { position, delta, precise, - } => { + }) => { if bounds.contains_point(*position) { if self.scroll(*position, *delta, *precise, layout.scroll_max, cx) { handled = true; diff --git a/crates/gpui/src/gpui.rs b/crates/gpui/src/gpui.rs index 5a1fc2fe143a32a15c13e1bea92389ef9b0a7898..723e25c55d62a55951a013255992167bbe6676bf 100644 --- a/crates/gpui/src/gpui.rs +++ b/crates/gpui/src/gpui.rs @@ -28,8 +28,7 @@ pub mod json; pub mod keymap; pub mod platform; pub use gpui_macros::test; -pub use platform::FontSystem; -pub use platform::{Event, NavigationDirection, PathPromptOptions, Platform, PromptLevel}; +pub use platform::*; pub use presenter::{ Axis, DebugContext, EventContext, LayoutContext, PaintContext, SizeConstraint, Vector2FExt, }; diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index b4875df3f5708010172fc0447865fca7116faef5..cf508a5634a4341ee65573f6613fb93087d2bb6d 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -20,7 +20,7 @@ use crate::{ }; use anyhow::{anyhow, Result}; use async_task::Runnable; -pub use event::{Event, NavigationDirection}; +pub use event::*; use postage::oneshot; use serde::Deserialize; use std::{ diff --git a/crates/gpui/src/platform/event.rs b/crates/gpui/src/platform/event.rs index f43d5bea49796b7dad579af8a0e4c137b9ddc6fe..90b5d21fc2f7e1f8ec9092c598ca4a4c58cde658 100644 --- a/crates/gpui/src/platform/event.rs +++ b/crates/gpui/src/platform/event.rs @@ -1,85 +1,77 @@ use crate::{geometry::vector::Vector2F, keymap::Keystroke}; +#[derive(Clone, Debug)] +pub struct KeyDownEvent { + pub keystroke: Keystroke, + pub input: Option, + pub is_held: bool, +} + +#[derive(Clone, Debug)] +pub struct KeyUpEvent { + pub keystroke: Keystroke, + pub input: Option, +} + +#[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)] pub enum NavigationDirection { Back, 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, + pub ctrl: bool, + pub cmd: bool, + pub alt: bool, + pub shift: bool, +} + #[derive(Clone, Debug)] pub enum Event { - KeyDown { - keystroke: Keystroke, - input: Option, - is_held: bool, - }, - KeyUp { - keystroke: Keystroke, - input: Option, - }, - 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, - }, + KeyDown(KeyDownEvent), + KeyUp(KeyUpEvent), + ModifiersChanged(ModifiersChangedEvent), + MouseDown(MouseEvent), + MouseUp(MouseEvent), + MouseMoved(MouseMovedEvent), + ScrollWheel(ScrollWheelEvent), } impl Event { @@ -88,15 +80,9 @@ impl Event { Event::KeyDown { .. } => None, Event::KeyUp { .. } => None, Event::ModifiersChanged { .. } => None, - Event::ScrollWheel { position, .. } - | Event::LeftMouseDown { position, .. } - | Event::LeftMouseUp { position, .. } - | Event::LeftMouseDragged { position, .. } - | Event::RightMouseDown { position, .. } - | Event::RightMouseUp { position, .. } - | Event::NavigateMouseDown { position, .. } - | Event::NavigateMouseUp { position, .. } - | Event::MouseMoved { position, .. } => Some(*position), + Event::MouseDown(event) | Event::MouseUp(event) => Some(event.position), + Event::MouseMoved(event) => Some(event.position), + Event::ScrollWheel(event) => Some(event.position), } } } diff --git a/crates/gpui/src/platform/mac/event.rs b/crates/gpui/src/platform/mac/event.rs index ed880513e823a291e81b7d5cbf8a33418ed99c89..5e23859675cbc173254428f376f832207a2b3c00 100644 --- a/crates/gpui/src/platform/mac/event.rs +++ b/crates/gpui/src/platform/mac/event.rs @@ -2,10 +2,12 @@ use crate::{ geometry::vector::vec2f, keymap::Keystroke, platform::{Event, NavigationDirection}, + KeyDownEvent, KeyUpEvent, ModifiersChangedEvent, MouseButton, MouseEvent, MouseMovedEvent, + ScrollWheelEvent, }; use cocoa::{ appkit::{NSEvent, NSEventModifierFlags, NSEventType}, - base::{id, nil, YES}, + base::{id, YES}, foundation::NSString as _, }; use std::{borrow::Cow, ffi::CStr, os::raw::c_char}; @@ -59,12 +61,12 @@ impl Event { let shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask); let cmd = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask); - Some(Self::ModifiersChanged { + Some(Self::ModifiersChanged(ModifiersChangedEvent { ctrl, alt, shift, cmd, - }) + })) } NSEventType::NSKeyDown => { let modifiers = native_event.modifierFlags(); @@ -76,7 +78,7 @@ impl Event { let (unmodified_chars, input) = get_key_text(native_event, cmd, ctrl, function)?; - Some(Self::KeyDown { + Some(Self::KeyDown(KeyDownEvent { keystroke: Keystroke { ctrl, alt, @@ -86,7 +88,7 @@ impl Event { }, input, is_held: native_event.isARepeat() == YES, - }) + })) } NSEventType::NSKeyUp => { let modifiers = native_event.modifierFlags(); @@ -98,7 +100,7 @@ impl Event { let (unmodified_chars, input) = get_key_text(native_event, cmd, ctrl, function)?; - Some(Self::KeyUp { + Some(Self::KeyUp(KeyUpEvent { keystroke: Keystroke { ctrl, alt, @@ -107,125 +109,120 @@ impl Event { key: unmodified_chars.into(), }, input, - }) - } - NSEventType::NSLeftMouseDown => { - let modifiers = native_event.modifierFlags(); - window_height.map(|window_height| Self::LeftMouseDown { - 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::NSLeftMouseUp => window_height.map(|window_height| Self::LeftMouseUp { - position: vec2f( - native_event.locationInWindow().x as f32, - window_height - native_event.locationInWindow().y as f32, - ), - click_count: native_event.clickCount() as usize, - }), - NSEventType::NSRightMouseDown => { + 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(); - 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, + + window_height.map(|window_height| { + Self::MouseDown(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::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, + NSEventType::NSLeftMouseUp + | NSEventType::NSRightMouseUp + | NSEventType::NSOtherMouseUp => { + 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(); - window_height.map(|window_height| Self::NavigateMouseDown { + window_height.map(|window_height| { + 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( native_event.locationInWindow().x as f32, window_height - native_event.locationInWindow().y as f32, ), - direction, - 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, + delta: vec2f( + native_event.scrollingDeltaX() as f32, + native_event.scrollingDeltaY() as f32, + ), + precise: native_event.hasPreciseScrollingDeltas() == YES, }) - } - NSEventType::NSOtherMouseUp => { - let direction = match native_event.buttonNumber() { - 3 => NavigationDirection::Back, - 4 => NavigationDirection::Forward, + }), + NSEventType::NSLeftMouseDragged + | NSEventType::NSRightMouseDragged + | NSEventType::NSOtherMouseDragged => { + 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 _ => return None, }; - window_height.map(|window_height| Self::NavigateMouseUp { - position: vec2f( - native_event.locationInWindow().x as f32, - window_height - native_event.locationInWindow().y as f32, - ), - direction, + window_height.map(|window_height| { + let modifiers = native_event.modifierFlags(); + Self::MouseMoved(MouseMovedEvent { + pressed_button: Some(pressed_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), + }) }) } - 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| { let modifiers = native_event.modifierFlags(); - Self::MouseMoved { + Self::MouseMoved(MouseMovedEvent { position: vec2f( native_event.locationInWindow().x 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), alt: modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask), shift: modifiers.contains(NSEventModifierFlags::NSShiftKeyMask), cmd: modifiers.contains(NSEventModifierFlags::NSCommandKeyMask), - } + }) }), _ => None, } diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index c845693ba996af356a04e5de77c93cbe4639944d..5e6b3b9c190fa6ff16a2cfee0c245a35fea0b444 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -6,7 +6,7 @@ use crate::{ }, keymap::Keystroke, platform::{self, Event, WindowBounds, WindowContext}, - Scene, + KeyDownEvent, ModifiersChangedEvent, MouseButton, MouseEvent, MouseMovedEvent, Scene, }; use block::ConcreteBlock; 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())) }; if let Some(event) = event { match &event { - Event::KeyDown { + Event::KeyDown(KeyDownEvent { keystroke, input, is_held, - } => { + }) => { let keydown = (keystroke.clone(), input.clone()); // Ignore events from held-down keys after some of the initially-pressed keys // were released. @@ -603,33 +603,41 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) { if let Some(event) = event { match &event { - Event::LeftMouseDragged { position, .. } => { + Event::MouseMoved( + event @ MouseMovedEvent { + pressed_button: Some(_), + .. + }, + ) => { window_state_borrow.synthetic_drag_counter += 1; window_state_borrow .executor .spawn(synthetic_drag( weak_window_state, window_state_borrow.synthetic_drag_counter, - *position, + *event, )) .detach(); } - Event::LeftMouseUp { .. } => { + Event::MouseUp(MouseEvent { + button: MouseButton::Left, + .. + }) => { window_state_borrow.synthetic_drag_counter += 1; } - Event::ModifiersChanged { + Event::ModifiersChanged(ModifiersChangedEvent { ctrl, alt, shift, cmd, - } => { + }) => { // Only raise modifiers changed event when they have actually changed - if let Some(Event::ModifiersChanged { + if let Some(Event::ModifiersChanged(ModifiersChangedEvent { ctrl: prev_ctrl, alt: prev_alt, shift: prev_shift, cmd: prev_cmd, - }) = &window_state_borrow.previous_modifiers_changed_event + })) = &window_state_borrow.previous_modifiers_changed_event { if prev_ctrl == ctrl && prev_alt == alt @@ -667,11 +675,11 @@ extern "C" fn cancel_operation(this: &Object, _sel: Sel, _sender: id) { shift: false, key: chars.clone(), }; - let event = Event::KeyDown { + let event = Event::KeyDown(KeyDownEvent { keystroke: keystroke.clone(), input: Some(chars.clone()), is_held: false, - }; + }); window_state_borrow.last_fresh_keydown = Some((keystroke, Some(chars))); 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( window_state: Weak>, drag_id: usize, - position: Vector2F, + event: MouseMovedEvent, ) { loop { 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 let Some(mut callback) = window_state_borrow.event_callback.take() { drop(window_state_borrow); - callback(Event::LeftMouseDragged { - // TODO: Make sure empty modifiers is correct for this - position, - shift: false, - ctrl: false, - alt: false, - cmd: false, - }); + callback(Event::MouseMoved(event)); window_state.borrow_mut().event_callback = Some(callback); } } else { diff --git a/crates/gpui/src/presenter.rs b/crates/gpui/src/presenter.rs index 6285b1be9931b30ada4d4673681ce335398bf252..86a8c4cf3049e924cfcea798ae2ff703ea6132d8 100644 --- a/crates/gpui/src/presenter.rs +++ b/crates/gpui/src/presenter.rs @@ -9,9 +9,9 @@ use crate::{ scene::CursorRegion, text_layout::TextLayoutCache, Action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AssetCache, ElementBox, Entity, - FontSystem, ModelHandle, MouseRegion, MouseRegionId, ReadModel, ReadView, RenderContext, - RenderParams, Scene, UpgradeModelHandle, UpgradeViewHandle, View, ViewHandle, WeakModelHandle, - WeakViewHandle, + FontSystem, ModelHandle, MouseButton, MouseEvent, MouseMovedEvent, MouseRegion, MouseRegionId, + ReadModel, ReadView, RenderContext, RenderParams, Scene, UpgradeModelHandle, UpgradeViewHandle, + View, ViewHandle, WeakModelHandle, WeakViewHandle, }; use pathfinder_geometry::vector::{vec2f, Vector2F}; use serde_json::json; @@ -235,7 +235,11 @@ impl Presenter { let mut dragged_region = None; match event { - Event::LeftMouseDown { position, .. } => { + Event::MouseDown(MouseEvent { + position, + button: MouseButton::Left, + .. + }) => { let mut hit = false; for (region, _) in self.mouse_regions.iter().rev() { if region.bounds.contains_point(position) { @@ -251,11 +255,12 @@ impl Presenter { } } } - Event::LeftMouseUp { + Event::MouseUp(MouseEvent { position, click_count, + button: MouseButton::Left, .. - } => { + }) => { self.prev_drag_position.take(); if let Some(region) = self.clicked_region.take() { 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; for (region, _) in self.mouse_regions.iter().rev() { if region.bounds.contains_point(position) { @@ -279,11 +288,12 @@ impl Presenter { } } } - Event::RightMouseUp { + Event::MouseUp(MouseEvent { position, click_count, + button: MouseButton::Right, .. - } => { + }) => { if let Some(region) = self.right_clicked_region.take() { invalidated_views.push(region.view_id); if region.bounds.contains_point(position) { @@ -291,34 +301,37 @@ impl Presenter { } } } - Event::MouseMoved { .. } => { - self.last_mouse_moved_event = Some(event.clone()); - } - Event::LeftMouseDragged { + Event::MouseMoved(MouseMovedEvent { + pressed_button, position, shift, ctrl, alt, cmd, - } => { - if let Some((clicked_region, prev_drag_position)) = self - .clicked_region - .as_ref() - .zip(self.prev_drag_position.as_mut()) - { - dragged_region = - Some((clicked_region.clone(), position - *prev_drag_position)); - *prev_drag_position = position; + .. + }) => { + if let Some(MouseButton::Left) = pressed_button { + if let Some((clicked_region, prev_drag_position)) = self + .clicked_region + .as_ref() + .zip(self.prev_drag_position.as_mut()) + { + 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 { - position, - left_mouse_down: true, - shift, - ctrl, - alt, - cmd, - }); + self.last_mouse_moved_event = Some(event.clone()); } _ => {} } @@ -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; if let Some(drag_callback) = dragged_region.drag { 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 hovered_regions = Vec::new(); - if let Event::MouseMoved { + if let Event::MouseMoved(MouseMovedEvent { position, - left_mouse_down, + pressed_button, .. - } = event + }) = event { - if !left_mouse_down { + if let None = pressed_button { let mut style_to_assign = CursorStyle::Arrow; for region in self.cursor_regions.iter().rev() { if region.bounds.contains_point(*position) { @@ -648,6 +661,16 @@ impl<'a> PaintContext<'a> { } } + #[inline] + pub fn paint_layer(&mut self, clip_bounds: Option, 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 { *self.view_stack.last().unwrap() } diff --git a/crates/gpui/src/scene.rs b/crates/gpui/src/scene.rs index 1f0e2c0ecc0e3921f7a37625a5b32457e923fef9..769eabe7e58372e13628d316381df24e0b0a2044 100644 --- a/crates/gpui/src/scene.rs +++ b/crates/gpui/src/scene.rs @@ -8,7 +8,7 @@ use crate::{ geometry::{rect::RectF, vector::Vector2F}, json::ToJson, platform::CursorStyle, - EventContext, ImageData, + EventContext, ImageData, MouseEvent, MouseMovedEvent, ScrollWheelEvent, }; pub struct Scene { @@ -44,17 +44,28 @@ pub struct CursorRegion { pub style: CursorStyle, } +pub enum MouseRegionEvent { + Moved(MouseMovedEvent), + Hover(MouseEvent), + Down(MouseEvent), + Up(MouseEvent), + Click(MouseEvent), + DownOut(MouseEvent), + ScrollWheel(ScrollWheelEvent), +} + #[derive(Clone, Default)] pub struct MouseRegion { pub view_id: usize, pub discriminant: Option<(TypeId, usize)>, pub bounds: RectF, + pub hover: Option>, pub mouse_down: Option>, pub click: Option>, pub right_mouse_down: Option>, pub right_click: Option>, - pub drag: Option>, + pub drag: Option>, pub mouse_down_out: Option>, pub right_mouse_down_out: Option>, } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index cebf5f504ea8c634a38155e5c5654362fb345b12..d5ed1c1620da0b514ba22f4c04e050f5043c67fb 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -273,7 +273,7 @@ pub struct Chunk<'a> { pub is_unnecessary: bool, } -pub(crate) struct Diff { +pub struct Diff { base_version: clock::Global, new_text: Arc, changes: Vec<(ChangeTag, usize)>, @@ -958,7 +958,7 @@ impl Buffer { } } - pub(crate) fn diff(&self, mut new_text: String, cx: &AppContext) -> Task { + pub fn diff(&self, mut new_text: String, cx: &AppContext) -> Task { let old_text = self.as_rope().clone(); let base_version = self.version(); cx.background().spawn(async move { @@ -979,11 +979,7 @@ impl Buffer { }) } - pub(crate) fn apply_diff( - &mut self, - diff: Diff, - cx: &mut ModelContext, - ) -> Option<&Transaction> { + pub fn apply_diff(&mut self, diff: Diff, cx: &mut ModelContext) -> Option<&Transaction> { if self.version == diff.base_version { self.finalize_last_transaction(); self.start_transaction(); diff --git a/crates/project/src/fs.rs b/crates/project/src/fs.rs index 17d7264f1d80853a02707ccdff6308feeff178e9..5c528016118fdde2126f512170e5c46e6be9e171 100644 --- a/crates/project/src/fs.rs +++ b/crates/project/src/fs.rs @@ -334,28 +334,6 @@ impl FakeFs { }) } - pub async fn insert_dir(&self, path: impl AsRef) { - 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, content: String) { let mut state = self.state.lock().await; let path = path.as_ref(); @@ -392,7 +370,7 @@ impl FakeFs { match tree { Object(map) => { - self.insert_dir(path).await; + self.create_dir(path).await.unwrap(); for (name, contents) in map { let mut path = PathBuf::from(path); path.push(name); @@ -400,7 +378,7 @@ impl FakeFs { } } Null => { - self.insert_dir(&path).await; + self.create_dir(&path).await.unwrap(); } String(contents) => { self.insert_file(&path, contents).await; diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index e4425e341476dfb4a316bfdc9df5b010e97e0964..0ac3064e56b33840130d0e43a98d12d8908f4476 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -12,7 +12,7 @@ use anyhow::{anyhow, Context, Result}; use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore}; use clock::ReplicaId; 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 gpui::{ AnyModelHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, @@ -51,10 +51,12 @@ use std::{ ffi::OsString, hash::Hash, mem, + num::NonZeroU32, ops::Range, os::unix::{ffi::OsStrExt, prelude::OsStringExt}, path::{Component, Path, PathBuf}, rc::Rc, + str, sync::{ atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst}, Arc, @@ -3025,78 +3027,50 @@ impl Project { } for (buffer, buffer_abs_path, language_server) in local_buffers { - let text_document = lsp::TextDocumentIdentifier::new( - lsp::Url::from_file_path(&buffer_abs_path).unwrap(), - ); - let capabilities = &language_server.capabilities(); - let tab_size = cx.update(|cx| { - let language_name = buffer.read(cx).language().map(|language| language.name()); - cx.global::().tab_size(language_name.as_deref()) + let (format_on_save, tab_size) = buffer.read_with(&cx, |buffer, cx| { + let settings = cx.global::(); + let language_name = buffer.language().map(|language| language.name()); + ( + settings.format_on_save(language_name.as_deref()), + settings.tab_size(language_name.as_deref()), + ) }); - let lsp_edits = if capabilities - .document_formatting_provider - .as_ref() - .map_or(false, |provider| *provider != lsp::OneOf::Left(false)) - { - language_server - .request::(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::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(), - }, + + let transaction = match format_on_save { + settings::FormatOnSave::Off => continue, + settings::FormatOnSave::LanguageServer => Self::format_via_lsp( + &this, + &buffer, + &buffer_abs_path, + &language_server, + tab_size, + &mut cx, + ) + .await + .context("failed to format via language server")?, + settings::FormatOnSave::External { command, arguments } => { + Self::format_via_external_command( + &buffer, + &buffer_abs_path, + &command, + &arguments, + &mut cx, ) - .await? - } else { - continue; + .await + .context(format!( + "failed to format via external command {:?}", + command + ))? + } }; - if let Some(lsp_edits) = lsp_edits { - let edits = this - .update(&mut cx, |this, cx| { - this.edits_from_lsp(&buffer, lsp_edits, None, cx) - }) - .await?; - buffer.update(&mut 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(); - if !push_to_history { - buffer.forget_transaction(transaction.id); - } - project_transaction.0.insert(cx.handle(), transaction); - } - }); + if let Some(transaction) = transaction { + if !push_to_history { + buffer.update(&mut cx, |buffer, _| { + buffer.forget_transaction(transaction.id) + }); + } + project_transaction.0.insert(buffer, transaction); } } @@ -3104,6 +3078,141 @@ impl Project { }) } + async fn format_via_lsp( + this: &ModelHandle, + buffer: &ModelHandle, + abs_path: &Path, + language_server: &Arc, + tab_size: NonZeroU32, + cx: &mut AsyncAppContext, + ) -> Result> { + 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::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::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_abs_path: &Path, + command: &str, + arguments: &[String], + cx: &mut AsyncAppContext, + ) -> Result> { + 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( &self, buffer: &ModelHandle, diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 0a59d28cc9e845adb1ba4d3fd434245b101a5fbf..98df5e2f1f6be1d664fd4e63278ecac48b084454 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -38,7 +38,7 @@ pub struct LanguageSettings { pub hard_tabs: Option, pub soft_wrap: Option, pub preferred_line_length: Option, - pub format_on_save: Option, + pub format_on_save: Option, pub enable_language_server: Option, } @@ -50,6 +50,17 @@ pub enum SoftWrap { PreferredLineLength, } +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum FormatOnSave { + Off, + LanguageServer, + External { + command: String, + arguments: Vec, + }, +} + #[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum Autosave { @@ -72,7 +83,7 @@ pub struct SettingsFileContent { #[serde(default)] pub vim_mode: Option, #[serde(default)] - pub format_on_save: Option, + pub format_on_save: Option, #[serde(default)] pub autosave: Option, #[serde(default)] @@ -136,9 +147,9 @@ impl Settings { .unwrap_or(80) } - pub fn format_on_save(&self, language: Option<&str>) -> bool { - self.language_setting(language, |settings| settings.format_on_save) - .unwrap_or(true) + pub fn format_on_save(&self, language: Option<&str>) -> FormatOnSave { + self.language_setting(language, |settings| settings.format_on_save.clone()) + .unwrap_or(FormatOnSave::LanguageServer) } pub fn enable_language_server(&self, language: Option<&str>) -> bool { @@ -215,7 +226,7 @@ impl Settings { merge(&mut self.autosave, data.autosave); merge_option( &mut self.language_settings.format_on_save, - data.format_on_save, + data.format_on_save.clone(), ); merge_option( &mut self.language_settings.enable_language_server, @@ -339,7 +350,7 @@ fn merge(target: &mut T, value: Option) { } } -fn merge_option(target: &mut Option, value: Option) { +fn merge_option(target: &mut Option, value: Option) { if value.is_some() { *target = value; } diff --git a/crates/terminal/Cargo.toml b/crates/terminal/Cargo.toml index 0bbc0569227198a45538016b63a24f35d079abd0..b44b93e745ccabe06da58d540ded3eec17f3a145 100644 --- a/crates/terminal/Cargo.toml +++ b/crates/terminal/Cargo.toml @@ -21,7 +21,11 @@ mio-extras = "2.0.6" futures = "0.3" ordered-float = "2.1.1" itertools = "0.10" +dirs = "4.0.0" [dev-dependencies] gpui = { path = "../gpui", features = ["test-support"] } +client = { path = "../client", features = ["test-support"]} +project = { path = "../project", features = ["test-support"]} + diff --git a/crates/terminal/src/color_translation.rs b/crates/terminal/src/color_translation.rs new file mode 100644 index 0000000000000000000000000000000000000000..78c2a569dbbeac5806e6535a2c87b7c0b6b57c03 --- /dev/null +++ b/crates/terminal/src/color_translation.rs @@ -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); + } + } +} diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 00b1e44d5f59f2c5f1394e48903d3b4b7d3f0747..437c5995df6f7155417f054c584c0e8a3a60c0fd 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -3,32 +3,33 @@ mod modal; pub mod terminal_element; use alacritty_terminal::{ - config::{Config, Program, PtyConfig}, + config::{Config, PtyConfig}, event::{Event as AlacTermEvent, EventListener, Notify}, event_loop::{EventLoop, Msg, Notifier}, grid::Scroll, sync::FairMutex, - term::{color::Rgb as AlacRgb, SizeInfo}, + term::SizeInfo, tty::{self, setup_env}, Term, }; - +use color_translation::{get_color_at_index, to_alac_rgb}; +use dirs::home_dir; use futures::{ channel::mpsc::{unbounded, UnboundedSender}, StreamExt, }; use gpui::{ - actions, color::Color, elements::*, impl_internal_actions, platform::CursorStyle, - ClipboardItem, Entity, MutableAppContext, View, ViewContext, + actions, elements::*, impl_internal_actions, platform::CursorStyle, ClipboardItem, Entity, + MutableAppContext, View, ViewContext, }; use modal::deploy_modal; -use project::{Project, ProjectPath}; +use project::{LocalWorktree, Project, ProjectPath}; use settings::Settings; use smallvec::SmallVec; use std::{collections::HashMap, path::PathBuf, sync::Arc}; use workspace::{Item, Workspace}; -use crate::terminal_element::{get_color_at_index, TerminalEl}; +use crate::terminal_element::TerminalEl; //ASCII Control characters on a keyboard 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 DOWN_SEQ: &str = "\x1b[B"; 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 #[derive(Clone, Default, Debug, PartialEq, Eq)] @@ -63,6 +72,7 @@ actions!( Down, Tab, Clear, + Copy, Paste, Deploy, Quit, @@ -79,12 +89,13 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(Terminal::escape); cx.add_action(Terminal::quit); 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::right); cx.add_action(Terminal::up); cx.add_action(Terminal::down); cx.add_action(Terminal::tab); + cx.add_action(Terminal::copy); cx.add_action(Terminal::paste); cx.add_action(Terminal::scroll_terminal); 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 cur_size: SizeInfo, modal: bool, + associated_directory: Option, } ///Upward flowing events, for changing the title and such @@ -143,12 +155,11 @@ impl Terminal { .detach(); let pty_config = PtyConfig { - shell: Some(Program::Just("zsh".to_string())), - working_directory, + shell: None, //Use the users default shell + working_directory: working_directory.clone(), hold: false, }; - //Does this mangle the zed Env? I'm guessing it does... do child processes have a seperate ENV? let mut env: HashMap = HashMap::new(); //TODO: Properly set the current locale, env.insert("LC_ALL".to_string(), "en_US.UTF-8".to_string()); @@ -162,8 +173,15 @@ impl Terminal { setup_env(&config); //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(200., 100.0, 5., 5., 0., 0., false); + let size_info = SizeInfo::new( + DEBUG_TERMINAL_WIDTH, + DEBUG_TERMINAL_HEIGHT, + DEBUG_CELL_WIDTH, + DEBUG_LINE_HEIGHT, + 0., + 0., + false, + ); //Set up the terminal... let term = Term::new(&config, size_info, ZedListener(events_tx.clone())); @@ -192,6 +210,7 @@ impl Terminal { has_bell: false, cur_size: size_info, modal, + associated_directory: working_directory, } } @@ -238,25 +257,8 @@ impl Terminal { ), AlacTermEvent::ColorRequest(index, format) => { let color = self.term.lock().colors()[index].unwrap_or_else(|| { - let term_style = &cx.global::().theme.terminal.colors; - match index { - 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 }, - } + let term_style = &cx.global::().theme.terminal; + to_alac_rgb(get_color_at_index(&index, term_style)) }); 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 fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { let project = workspace.project().read(cx); + let abs_path = project .active_entry() .and_then(|entry_id| project.worktree_for_entry(entry_id, cx)) .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( Box::new(cx.add_view(|cx| Terminal::new(cx, abs_path, false))), @@ -310,6 +313,16 @@ impl Terminal { cx.emit(Event::CloseTerminal); } + ///Attempt to paste the clipboard into the terminal + fn copy(&mut self, _: &Copy, cx: &mut ViewContext) { + 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 fn paste(&mut self, _: &Paste, cx: &mut ViewContext) { if let Some(item) = cx.read_from_clipboard() { @@ -444,6 +457,13 @@ impl Item for Terminal { .boxed() } + fn clone_on_split(&self, cx: &mut ViewContext) -> Option { + //From what I can tell, there's no way to tell the current working + //Directory of the terminal from outside the terminal. There might be + //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 { None } @@ -504,27 +524,134 @@ impl Item for Terminal { } } -//Convenience method for less lines -fn to_alac_rgb(color: Color) -> AlacRgb { - AlacRgb { - r: color.r, - g: color.g, - b: color.g, - } +fn get_working_directory(wt: &LocalWorktree) -> Option { + Some(wt.abs_path().to_path_buf()) + .filter(|path| path.is_dir()) + .or_else(|| home_dir()) } #[cfg(test)] mod tests { + 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 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, //and produce noticable output? #[gpui::test] async fn test_terminal(cx: &mut TestAppContext) { 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.write_to_pty(&Input(("expr 3 + 4".to_string()).to_string()), cx); @@ -538,6 +665,19 @@ mod tests { content.contains("7") }) .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) -> String { diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index b4e43bfc4f574c3aa99bec6bf01ebf3235ff0e14..329fbaabe094c539f0a02a6a5ccfe5057314a24e 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -1,13 +1,15 @@ use alacritty_terminal::{ - ansi::Color as AnsiColor, grid::{Dimensions, GridIterator, Indexed}, - index::Point, + index::{Column as GridCol, Line as GridLine, Point, Side}, + selection::{Selection, SelectionRange, SelectionType}, + sync::FairMutex, term::{ cell::{Cell, Flags}, SizeInfo, }, + Term, }; -use editor::{Cursor, CursorShape}; +use editor::{Cursor, CursorShape, HighlightedRange, HighlightedRangeLine}; use gpui::{ color::Color, elements::*, @@ -18,16 +20,21 @@ use gpui::{ }, json::json, text_layout::{Line, RunStyle}, - Event, FontCache, MouseRegion, PaintContext, Quad, SizeConstraint, TextLayoutCache, - WeakViewHandle, + Event, FontCache, KeyDownEvent, MouseRegion, PaintContext, Quad, ScrollWheelEvent, + SizeConstraint, TextLayoutCache, WeakViewHandle, }; use itertools::Itertools; use ordered_float::OrderedFloat; use settings::Settings; -use std::rc::Rc; 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 ///Scroll multiplier that is set to 3 by default. This will be removed when I @@ -44,14 +51,27 @@ pub struct TerminalEl { view: WeakViewHandle, } -///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 LineHeight(f32); +struct LayoutLine { + cells: Vec, + highlighted_range: Option>, +} + +///New type pattern to ensure that we use adjusted mouse positions throughout the code base, rather than +struct PaneRelativePos(Vector2F); + +///Functionally the constructor for the PaneRelativePos type, mutates the mouse_position +fn relative_pos(mouse_position: Vector2F, origin: Vector2F) -> PaneRelativePos { + PaneRelativePos(mouse_position.sub(origin)) //Avoid the extra allocation by mutating +} + #[derive(Clone, Debug, Default)] struct LayoutCell { point: Point, - text: Line, + text: Line, //NOTE TO SELF THIS IS BAD PERFORMANCE RN! background_color: Color, } @@ -67,13 +87,14 @@ impl LayoutCell { ///The information generated during layout that is nescessary for painting pub struct LayoutState { - cells: Vec<(Point, Line)>, - background_rects: Vec<(RectF, Color)>, //Vec index == Line index for the LineSpan + layout_lines: Vec, line_height: LineHeight, em_width: CellWidth, cursor: Option, background_color: Color, cur_size: SizeInfo, + terminal: Arc>>, + selection_color: Color, } 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 let cur_size = make_new_size(constraint, &cell_width, &line_height); //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)); //Now that we're done with the mutable portion, grab the immutable settings and view again - let terminal_theme = &(cx.global::()).theme.terminal; let view = view_handle.read(cx); let term = view.term.lock(); - + let (selection_color, terminal_theme) = { + let theme = &(cx.global::()).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 cursor_point = grid.cursor.point; let cursor_text = grid[cursor_point.line][cursor_point.column].c.to_string(); let content = term.renderable_content(); - let layout_cells = layout_cells( + let layout_lines = layout_lines( content.display_iter, &text_style, terminal_theme, cx.text_layout_cache, view.modal, + content.selection, ); - let cells = layout_cells - .iter() - .map(|c| (c.point, c.text.clone())) - .collect::, 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::>(); - let block_text = cx.text_layout_cache.layout_str( &cursor_text, text_style.font_size, @@ -185,6 +190,7 @@ impl Element for TerminalEl { Some(block_text.clone()), ) }); + drop(term); let background_color = if view.modal { terminal_theme.colors.modal_background @@ -195,13 +201,14 @@ impl Element for TerminalEl { ( constraint.max, LayoutState { - cells, + layout_lines, line_height, em_width: cell_width, cursor, cur_size, - background_rects, background_color, + terminal: terminal_mutex, + selection_color, }, ) } @@ -215,17 +222,21 @@ impl Element for TerminalEl { ) -> Self::PaintState { //Setup element stuff 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.); + //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| { //Start with a background color cx.scene.push_quad(Quad { @@ -236,25 +247,83 @@ impl Element for TerminalEl { }); //Draw cell backgrounds - for background_rect in &layout.background_rects { - let new_origin = origin + background_rect.0.origin(); - cx.scene.push_quad(Quad { - bounds: RectF::new(new_origin, background_rect.0.size()), - background: Some(background_rect.1), - border: Default::default(), - corner_radius: 0., + for layout_line in &layout.layout_lines { + for layout_cell in &layout_line.cells { + let position = vec2f( + origin.x() + layout_cell.point.column as f32 * layout.em_width.0, + origin.y() + layout_cell.point.line as f32 * layout.line_height.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::>(); + + 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 paint_layer(cx, clip_bounds, |cx| { - for (point, cell) in &layout.cells { - let cell_origin = vec2f( - origin.x() + point.column as f32 * layout.em_width.0, - origin.y() + point.line as f32 * layout.line_height.0, - ); - cell.paint(cell_origin, visible_bounds, layout.line_height.0, cx); + for layout_line in &layout.layout_lines { + for layout_cell in &layout_line.cells { + let point = layout_cell.point; + + //Don't actually know the start_x for a line, until here: + let cell_origin = vec2f( + origin.x() + point.column as f32 * layout.em_width.0, + 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, ) -> bool { match event { - Event::ScrollWheel { + Event::ScrollWheel(ScrollWheelEvent { delta, position, .. - } => visible_bounds + }) => visible_bounds .contains_point(*position) .then(|| { let vertical_scroll = @@ -294,9 +363,9 @@ impl Element for TerminalEl { cx.dispatch_action(ScrollTerminal(vertical_scroll.round() as i32)); }) .is_some(), - Event::KeyDown { + Event::KeyDown(KeyDownEvent { input: Some(input), .. - } => cx + }) => cx .is_parent_view_focused() .then(|| { 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. fn make_text_style(font_cache: &FontCache, settings: &Settings) -> TextStyle { TextStyle { @@ -351,39 +432,57 @@ fn make_new_size( ) } -fn layout_cells( +fn layout_lines( grid: GridIterator, text_style: &TextStyle, terminal_theme: &TerminalStyle, text_layout_cache: &TextLayoutCache, modal: bool, -) -> Vec { - let mut line_count: i32 = 0; + selection_range: Option, +) -> Vec { let lines = grid.group_by(|i| i.point.line); lines .into_iter() - .map(|(_, line)| { - line_count += 1; - line.map(|indexed_cell| { - let cell_text = &indexed_cell.c.to_string(); - - let cell_style = cell_style(&indexed_cell, terminal_theme, text_style, modal); - - let layout_cell = text_layout_cache.layout_str( - cell_text, - text_style.font_size, - &[(cell_text.len(), cell_style)], - ); - LayoutCell::new( - Point::new(line_count - 1, indexed_cell.point.column.0 as i32), - layout_cell, - convert_color(&indexed_cell.bg, &terminal_theme.colors, modal), - ) - }) - .collect::>() + .enumerate() + .map(|(line_index, (_, line))| { + let mut highlighted_range = None; + let cells = line + .enumerate() + .map(|(x_index, indexed_cell)| { + if selection_range + .map(|range| range.contains(indexed_cell.point)) + .unwrap_or(false) + { + let mut range = highlighted_range.take().unwrap_or(x_index..x_index); + range.end = range.end.max(x_index); + highlighted_range = Some(range); + } + + let cell_text = &indexed_cell.c.to_string(); + + let cell_style = cell_style(&indexed_cell, terminal_theme, text_style, modal); + + //This is where we might be able to get better performance + let layout_cell = text_layout_cache.layout_str( + cell_text, + text_style.font_size, + &[(cell_text.len(), cell_style)], + ); + + LayoutCell::new( + Point::new(line_index as i32, indexed_cell.point.column.0 as i32), + layout_cell, + convert_color(&indexed_cell.bg, &terminal_theme.colors, modal), + ) + }) + .collect::>(); + + LayoutLine { + cells, + highlighted_range, + } }) - .flatten() - .collect::>() + .collect::>() } // 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. -pub fn get_color_at_index(index: &u8, colors: &TerminalColors) -> Color { - match index { - //0-15 are the same as the named colors above - 0 => colors.black, - 1 => colors.red, - 2 => colors.green, - 3 => colors.yellow, - 4 => colors.blue, - 5 => colors.magenta, - 6 => colors.cyan, - 7 => colors.white, - 8 => colors.bright_black, - 9 => colors.bright_red, - 10 => colors.bright_green, - 11 => colors.bright_yellow, - 12 => colors.bright_blue, - 13 => colors.bright_magenta, - 14 => colors.bright_cyan, - 15 => colors.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); //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 - 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 - } +fn attach_mouse_handlers( + origin: Vector2F, + cur_size: SizeInfo, + view_id: usize, + terminal_mutex: &Arc>>, + visible_bounds: RectF, + cx: &mut PaintContext, +) { + let click_mutex = terminal_mutex.clone(); + let drag_mutex = terminal_mutex.clone(); + let mouse_down_mutex = terminal_mutex.clone(); + + cx.scene.push_mouse_region(MouseRegion { + view_id, + mouse_down: Some(Rc::new(move |pos, _| { + let mut term = mouse_down_mutex.lock(); + let (point, side) = mouse_to_cell_data( + pos, + origin, + cur_size, + term.renderable_content().display_offset, + ); + term.selection = Some(Selection::new(SelectionType::Simple, point, side)) + })), + click: Some(Rc::new(move |pos, click_count, cx| { + let mut term = click_mutex.lock(); + + let (point, side) = mouse_to_cell_data( + pos, + origin, + cur_size, + term.renderable_content().display_offset, + ); + + let selection_type = match click_count { + 0 => return, //This is a release + 1 => Some(SelectionType::Simple), + 2 => Some(SelectionType::Semantic), + 3 => Some(SelectionType::Lines), + _ => None, + }; + + let selection = + selection_type.map(|selection_type| Selection::new(selection_type, point, side)); + + term.selection = selection; + cx.focus_parent_view(); + cx.notify(); + })), + 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 -///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) +///Copied (with modifications) from alacritty/src/event.rs > Mouse::point() +///Position is a pane-relative position. That means the top left corner of the mouse +///Region should be (0,0) +fn grid_cell(pos: &PaneRelativePos, cur_size: SizeInfo, display_offset: usize) -> Point { + let pos = pos.0; + let col = pos.x() / cur_size.cell_width(); //TODO: underflow... + let col = min(GridCol(col as usize), cur_size.last_column()); + + let line = pos.y() / cur_size.cell_height(); + let line = min(line as i32, cur_size.bottommost_line().0); + + //when clicking, need to ADD to get to the top left cell + //e.g. total_lines - viewport_height, THEN subtract display offset + //0 -> total_lines - viewport_height - display_offset + mouse_line + + Point::new(GridLine(line - display_offset as i32), col) } ///Draws the grid as Alacritty sees it. Useful for checking if there is an inconsistency between @@ -575,14 +731,73 @@ fn draw_debug_grid(bounds: RectF, layout: &mut LayoutState, cx: &mut PaintContex } } -#[cfg(test)] -mod tests { +mod test { + #[test] - fn test_rgb_for_index() { - //Test every possible value in the color cube - for i in 16..=231 { - let (r, g, b) = crate::terminal_element::rgb_for_index(&(i as u8)); - assert_eq!(i, 16 + 36 * r + 6 * g + b); - } + fn test_mouse_to_selection() { + let term_width = 100.; + let term_height = 200.; + let cell_width = 10.; + let line_height = 20.; + let mouse_pos_x = 100.; //Window relative + let mouse_pos_y = 100.; //Window relative + let origin_x = 10.; + let origin_y = 20.; + + let cur_size = alacritty_terminal::term::SizeInfo::new( + term_width, + term_height, + cell_width, + line_height, + 0., + 0., + false, + ); + + let mouse_pos = gpui::geometry::vector::vec2f(mouse_pos_x, mouse_pos_y); + let origin = gpui::geometry::vector::vec2f(origin_x, origin_y); //Position of terminal window, 1 'cell' in + let (point, _) = + crate::terminal_element::mouse_to_cell_data(mouse_pos, origin, cur_size, 0); + assert_eq!( + point, + alacritty_terminal::index::Point::new( + alacritty_terminal::index::Line(((mouse_pos_y - origin_y) / line_height) as i32), + alacritty_terminal::index::Column(((mouse_pos_x - origin_x) / cell_width) as usize), + ) + ); + } + + #[test] + fn test_mouse_to_selection_off_edge() { + let term_width = 100.; + let term_height = 200.; + let cell_width = 10.; + let line_height = 20.; + let mouse_pos_x = 100.; //Window relative + let mouse_pos_y = 100.; //Window relative + let origin_x = 10.; + let origin_y = 20.; + + let cur_size = alacritty_terminal::term::SizeInfo::new( + term_width, + term_height, + cell_width, + line_height, + 0., + 0., + false, + ); + + let mouse_pos = gpui::geometry::vector::vec2f(mouse_pos_x, mouse_pos_y); + let origin = gpui::geometry::vector::vec2f(origin_x, origin_y); //Position of terminal window, 1 'cell' in + let (point, _) = + crate::terminal_element::mouse_to_cell_data(mouse_pos, origin, cur_size, 0); + assert_eq!( + point, + alacritty_terminal::index::Point::new( + alacritty_terminal::index::Line(((mouse_pos_y - origin_y) / line_height) as i32), + alacritty_terminal::index::Column(((mouse_pos_x - origin_x) / cell_width) as usize), + ) + ); } } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index a3af61a41cb8cacf9e372b43367ca5d42d2824b7..51f5fb7fcc8e0a9aaf1f8bb1f942fc3034d88dee 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -108,6 +108,7 @@ pub struct Toolbar { pub container: ContainerStyle, pub height: f32, pub item_spacing: f32, + pub nav_button: Interactive, } #[derive(Clone, Deserialize, Default)] @@ -509,28 +510,23 @@ pub struct Interactive { pub default: T, pub hover: Option, pub active: Option, - pub active_hover: Option, + pub disabled: Option, } impl Interactive { pub fn style_for(&self, state: MouseState, active: bool) -> &T { if active { - if state.hovered { - self.active_hover - .as_ref() - .or(self.active.as_ref()) - .unwrap_or(&self.default) - } else { - self.active.as_ref().unwrap_or(&self.default) - } + self.active.as_ref().unwrap_or(&self.default) + } else if state.hovered { + self.hover.as_ref().unwrap_or(&self.default) } else { - if state.hovered { - self.hover.as_ref().unwrap_or(&self.default) - } else { - &self.default - } + &self.default } } + + pub fn disabled_style(&self) -> &T { + self.disabled.as_ref().unwrap_or(&self.default) + } } impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive { @@ -544,7 +540,7 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive { default: Value, hover: Option, active: Option, - active_hover: Option, + disabled: Option, } let json = Helper::deserialize(deserializer)?; @@ -570,14 +566,14 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive { let hover = deserialize_state(json.hover)?; 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)?; Ok(Interactive { default, hover, active, - active_hover, + disabled, }) } } diff --git a/crates/vim/src/vim_test_context.rs b/crates/vim/src/vim_test_context.rs index 57d0174703bbf5da9781c1f6f70f5bb23a9527d7..08ec4bd5e961315fa8c661ffbcfac89ab3a59207 100644 --- a/crates/vim/src/vim_test_context.rs +++ b/crates/vim/src/vim_test_context.rs @@ -147,14 +147,6 @@ impl<'a> VimTestContext<'a> { let mode = self.mode(); 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> { diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index bbc086395b12a4fd09d7c8ce07e6c52c6491cb53..391ddfc1f7a6d0c9e6a727622d925b3d40ead78b 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -136,13 +136,13 @@ pub struct ItemNavHistory { item: Rc, } -#[derive(Default)] -pub struct NavHistory { +struct NavHistory { mode: NavigationMode, backward_stack: VecDeque, forward_stack: VecDeque, closed_stack: VecDeque, paths_by_item: HashMap, + pane: WeakViewHandle, } #[derive(Copy, Clone)] @@ -168,17 +168,28 @@ pub struct NavigationEntry { impl Pane { pub fn new(cx: &mut ViewContext) -> Self { + let handle = cx.weak_handle(); Self { items: Vec::new(), active_item_index: 0, autoscroll: false, - nav_history: Default::default(), - toolbar: cx.add_view(|_| Toolbar::new()), + nav_history: Rc::new(RefCell::new(NavHistory { + 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> { - &self.nav_history + pub fn nav_history_for_item(&self, item: &ViewHandle) -> ItemNavHistory { + ItemNavHistory { + history: self.nav_history.clone(), + item: Rc::new(item.downgrade()), + } } pub fn activate(&self, cx: &mut ViewContext) { @@ -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.toolbar.update(cx, |_, cx| cx.notify()); + } + fn navigate_history( workspace: &mut Workspace, pane: ViewHandle, @@ -234,7 +265,7 @@ impl Pane { let to_load = pane.update(cx, |pane, cx| { loop { // 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 let Some(index) = entry @@ -367,7 +398,6 @@ impl Pane { return; } - item.set_nav_history(pane.read(cx).nav_history.clone(), cx); item.added_to_pane(workspace, pane.clone(), cx); pane.update(cx, |pane, cx| { // If there is already an active item, then insert the new item @@ -625,11 +655,16 @@ impl Pane { .borrow_mut() .set_mode(NavigationMode::Normal); - let mut nav_history = pane.nav_history().borrow_mut(); 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 { - 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 { - pub fn new(history: Rc>, item: &ViewHandle) -> Self { - Self { - history, - item: Rc::new(item.downgrade()), - } + pub fn push(&self, data: Option, cx: &mut MutableAppContext) { + self.history.borrow_mut().push(data, self.item.clone(), cx); } - pub fn history(&self) -> Rc> { - self.history.clone() + pub fn pop_backward(&self, cx: &mut MutableAppContext) -> Option { + self.history.borrow_mut().pop(NavigationMode::GoingBack, cx) } - pub fn push(&self, data: Option) { - self.history.borrow_mut().push(data, self.item.clone()); + pub fn pop_forward(&self, cx: &mut MutableAppContext) -> Option { + self.history + .borrow_mut() + .pop(NavigationMode::GoingForward, cx) } } 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 { - self.backward_stack.pop_back() + fn set_mode(&mut self, mode: NavigationMode) { + self.mode = mode; } - pub fn pop_forward(&mut self) -> Option { - self.forward_stack.pop_back() + fn disable(&mut self) { + self.mode = NavigationMode::Disabled; } - pub fn pop_closed(&mut self) -> Option { - self.closed_stack.pop_back() + fn enable(&mut self) { + self.mode = NavigationMode::Normal; } - fn pop(&mut self, mode: NavigationMode) -> Option { - 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 pop(&mut self, mode: NavigationMode, cx: &mut MutableAppContext) -> Option { + 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 set_mode(&mut self, mode: NavigationMode) { - self.mode = mode; - } - - pub fn push(&mut self, data: Option, item: Rc) { + fn push( + &mut self, + data: Option, + item: Rc, + cx: &mut MutableAppContext, + ) { match self.mode { NavigationMode::Disabled => {} 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))); + } } } diff --git a/crates/workspace/src/sidebar.rs b/crates/workspace/src/sidebar.rs index 341026aecf3c064343e4d12fb9a0603a03c56ce7..a500f994924d09aaae9304c2d3c9b0326f8803f7 100644 --- a/crates/workspace/src/sidebar.rs +++ b/crates/workspace/src/sidebar.rs @@ -188,12 +188,13 @@ impl Sidebar { }) .with_cursor_style(CursorStyle::ResizeLeftRight) .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(); *custom_width.borrow_mut() = 0f32 .max(match side { - Side::Left => prev_width + delta.x(), - Side::Right => prev_width - delta.x(), + Side::Left => prev_width + delta, + Side::Right => prev_width - delta, }) .round(); diff --git a/crates/workspace/src/toolbar.rs b/crates/workspace/src/toolbar.rs index e9b20bf3a04e5876a7a6e202f06b3b78c8727a68..636df9a03955ff2ebc7c98c63f3b8d9963e3ea6c 100644 --- a/crates/workspace/src/toolbar.rs +++ b/crates/workspace/src/toolbar.rs @@ -1,7 +1,7 @@ -use crate::ItemHandle; +use crate::{ItemHandle, Pane}; use gpui::{ - elements::*, AnyViewHandle, AppContext, ElementBox, Entity, MutableAppContext, RenderContext, - View, ViewContext, ViewHandle, + elements::*, platform::CursorStyle, Action, AnyViewHandle, AppContext, ElementBox, Entity, + MutableAppContext, RenderContext, View, ViewContext, ViewHandle, WeakViewHandle, }; use settings::Settings; @@ -42,6 +42,7 @@ pub enum ToolbarItemLocation { pub struct Toolbar { active_pane_item: Option>, + pane: WeakViewHandle, items: Vec<(Box, ToolbarItemLocation)>, } @@ -60,6 +61,7 @@ impl View for Toolbar { let mut primary_left_items = Vec::new(); let mut primary_right_items = Vec::new(); let mut secondary_item = None; + let spacing = theme.item_spacing; for (item, position) in &self.items { match *position { @@ -68,7 +70,7 @@ impl View for Toolbar { let left_item = ChildView::new(item.as_ref()) .aligned() .contained() - .with_margin_right(theme.item_spacing); + .with_margin_right(spacing); if let Some((flex, expanded)) = flex { primary_left_items.push(left_item.flex(flex, expanded).boxed()); } else { @@ -79,7 +81,7 @@ impl View for Toolbar { let right_item = ChildView::new(item.as_ref()) .aligned() .contained() - .with_margin_left(theme.item_spacing) + .with_margin_left(spacing) .flex_float(); if let Some((flex, expanded)) = flex { 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::().theme.tooltip.clone(); + Flex::column() .with_child( 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_right_items) .constrained() - .with_height(theme.height) + .with_height(height) .boxed(), ) .with_children(secondary_item) .contained() - .with_style(theme.container) + .with_style(container_style) .boxed() } } +fn nav_button( + svg_path: &'static str, + style: theme::Interactive, + tooltip_style: TooltipStyle, + enabled: bool, + spacing: f32, + action: A, + tooltip_action: A, + action_name: &str, + cx: &mut RenderContext, +) -> ElementBox { + MouseEventHandler::new::(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::( + 0, + action_name.to_string(), + Some(Box::new(tooltip_action)), + tooltip_style, + cx, + ) + .contained() + .with_margin_right(spacing) + .boxed() +} + impl Toolbar { - pub fn new() -> Self { + pub fn new(pane: WeakViewHandle) -> Self { Self { active_pane_item: None, + pane, items: Default::default(), } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 53318d943fe1b5854823e88b4bcbd1edd474692a..fdfa640718fdd066f801a81142c54e7a99c9c9b3 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -414,7 +414,6 @@ pub trait ItemHandle: 'static + fmt::Debug { fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]>; fn is_singleton(&self, cx: &AppContext) -> bool; fn boxed_clone(&self) -> Box; - fn set_nav_history(&self, nav_history: Rc>, cx: &mut MutableAppContext); fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option>; fn added_to_pane( &self, @@ -484,12 +483,6 @@ impl ItemHandle for ViewHandle { Box::new(self.clone()) } - fn set_nav_history(&self, nav_history: Rc>, 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> { self.update(cx, |item, cx| { cx.add_option_view(|cx| item.clone_on_split(cx)) @@ -503,6 +496,9 @@ impl ItemHandle for ViewHandle { pane: ViewHandle, cx: &mut ViewContext, ) { + 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(message) = followed_item.to_state_proto(cx) { workspace.update_followers( @@ -2360,7 +2356,12 @@ impl Workspace { } fn on_window_activation_changed(&mut self, active: bool, cx: &mut ViewContext) { - if !active && cx.global::().autosave == Autosave::OnWindowChange { + if !active + && matches!( + cx.global::().autosave, + Autosave::OnWindowChange | Autosave::OnFocusChange + ) + { for pane in &self.panes { pane.update(cx, |pane, cx| { for item in pane.items() { @@ -3073,6 +3074,17 @@ mod tests { deterministic.run_until_parked(); 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. item.update(cx, |item, cx| { 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. 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. 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. item.update(cx, |item, cx| { @@ -3106,7 +3118,7 @@ mod tests { .await .unwrap(); 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. workspace.update(cx, |workspace, cx| { @@ -3118,7 +3130,7 @@ mod tests { cx.blur(); }); 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. let _close_items = workspace.update(cx, |workspace, cx| { @@ -3127,28 +3139,107 @@ mod tests { }); deterministic.run_until_parked(); 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, + 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 { + state: String, save_count: usize, save_as_count: usize, reload_count: usize, is_dirty: bool, + is_singleton: bool, has_conflict: bool, project_entry_ids: Vec, project_path: Option, - is_singleton: bool, + nav_history: Option, } enum TestItemEvent { 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 { fn new() -> Self { Self { + state: String::new(), save_count: 0, save_as_count: 0, reload_count: 0, @@ -3157,6 +3248,18 @@ mod tests { project_entry_ids: Vec::new(), project_path: None, is_singleton: true, + nav_history: None, + } + } + + fn set_state(&mut self, state: String, cx: &mut ViewContext) { + self.push_to_nav_history(cx); + self.state = state; + } + + fn push_to_nav_history(&mut self, cx: &mut ViewContext) { + 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 } - fn set_nav_history(&mut self, _: ItemNavHistory, _: &mut ViewContext) {} + fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext) { + self.nav_history = Some(history); + } + + fn navigate(&mut self, state: Box, _: &mut ViewContext) -> bool { + let state = *state.downcast::().unwrap_or_default(); + if state != self.state { + self.state = state; + true + } else { + false + } + } + + fn deactivated(&mut self, cx: &mut ViewContext) { + self.push_to_nav_history(cx); + } fn clone_on_split(&self, _: &mut ViewContext) -> Option where diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 6d488e83e8ce64729791d96b2a6b9c28119a69e9..88acfdb14dd2f3796c5715abfe56a4c8da8fd829 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] description = "The fast, collaborative code editor." edition = "2021" name = "zed" -version = "0.44.1" +version = "0.45.0" [lib] name = "zed" diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index f88aee3d7c6c06265e94e8c541e0c6ec44261e02..56ccfaf9fed86fbfddd5544d75565c808bdce24a 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -554,7 +554,7 @@ mod tests { }); 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"))); save_task.await.unwrap(); editor.read_with(cx, |editor, cx| { @@ -680,14 +680,25 @@ mod tests { async fn test_open_paths(cx: &mut TestAppContext) { let app_state = init(cx); - let fs = app_state.fs.as_fake(); - fs.insert_dir("/dir1").await; - fs.insert_dir("/dir2").await; - fs.insert_dir("/dir3").await; - fs.insert_file("/dir1/a.txt", "".into()).await; - fs.insert_file("/dir2/b.txt", "".into()).await; - fs.insert_file("/dir3/c.txt", "".into()).await; - fs.insert_file("/d.txt", "".into()).await; + app_state + .fs + .as_fake() + .insert_tree( + "/", + json!({ + "dir1": { + "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 (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx)); @@ -891,7 +902,7 @@ mod tests { #[gpui::test] async fn test_open_and_save_new_file(cx: &mut TestAppContext) { 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; project.update(cx, |project, _| project.languages().add(rust_lang())); @@ -980,7 +991,7 @@ mod tests { #[gpui::test] async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) { 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; project.update(cx, |project, _| project.languages().add(rust_lang())); diff --git a/pbcpoy b/pbcpoy new file mode 100644 index 0000000000000000000000000000000000000000..f70f10e4db19068f79bc43844b49f3eece45c4e8 --- /dev/null +++ b/pbcpoy @@ -0,0 +1 @@ +A diff --git a/styles/package-lock.json b/styles/package-lock.json index 49304dc2fa98dfec942bf6687be56cefc13cdf80..2eb6d3a1bfaeaa206f0cc8a0a03efa0387d144c2 100644 --- a/styles/package-lock.json +++ b/styles/package-lock.json @@ -5,7 +5,6 @@ "requires": true, "packages": { "": { - "name": "styles", "version": "1.0.0", "license": "ISC", "dependencies": { diff --git a/styles/src/styleTree/workspace.ts b/styles/src/styleTree/workspace.ts index 2deadc02e7730041c2801d5fb87cf95c7bfb3ec8..fbd7b05a224f8272126631ad5107e1a36215c623 100644 --- a/styles/src/styleTree/workspace.ts +++ b/styles/src/styleTree/workspace.ts @@ -139,6 +139,19 @@ export default function workspace(theme: Theme) { background: backgroundColor(theme, 500), border: border(theme, "secondary", { bottom: true }), 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 }, }, breadcrumbs: {