diff --git a/Cargo.lock b/Cargo.lock index cbbcbc7914..cd79c19630 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10549,6 +10549,43 @@ dependencies = [ "uuid 1.4.1", ] +[[package]] +name = "workspace2" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-recursion 1.0.5", + "bincode", + "call2", + "client2", + "collections", + "db2", + "env_logger 0.9.3", + "fs2", + "futures 0.3.28", + "gpui2", + "indoc", + "install_cli2", + "itertools 0.10.5", + "language2", + "lazy_static", + "log", + "node_runtime", + "parking_lot 0.11.2", + "postage", + "project2", + "schemars", + "serde", + "serde_derive", + "serde_json", + "settings2", + "smallvec", + "terminal2", + "theme2", + "util", + "uuid 1.4.1", +] + [[package]] name = "ws2_32-sys" version = "0.2.1" @@ -10857,6 +10894,7 @@ dependencies = [ "urlencoding", "util", "uuid 1.4.1", + "workspace2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index f8ce95ea6b..0bb710fc1d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -94,7 +94,7 @@ members = [ "crates/semantic_index", "crates/vim", "crates/vcs_menu", - "crates/workspace", + "crates/workspace2", "crates/welcome", "crates/xtask", "crates/zed", diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index 75f202e710..260ec0b6b3 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -607,6 +607,20 @@ impl AppContext { self.globals_by_type.insert(global_type, lease.global); } + pub fn observe_release( + &mut self, + handle: &Handle, + mut on_release: impl FnMut(&mut E, &mut AppContext) + Send + Sync + 'static, + ) -> Subscription { + self.release_listeners.insert( + handle.entity_id, + Box::new(move |entity, cx| { + let entity = entity.downcast_mut().expect("invalid entity type"); + on_release(entity, cx) + }), + ) + } + pub(crate) fn push_text_style(&mut self, text_style: TextStyleRefinement) { self.text_style_stack.push(text_style); } diff --git a/crates/gpui2/src/app/model_context.rs b/crates/gpui2/src/app/model_context.rs index 6f88bf1aa6..f3d0bf2397 100644 --- a/crates/gpui2/src/app/model_context.rs +++ b/crates/gpui2/src/app/model_context.rs @@ -115,15 +115,11 @@ impl<'a, T: 'static> ModelContext<'a, T> { T: Any + Send + Sync, { let this = self.weak_handle(); - self.app.release_listeners.insert( - handle.entity_id, - Box::new(move |entity, cx| { - let entity = entity.downcast_mut().expect("invalid entity type"); - if let Some(this) = this.upgrade() { - this.update(cx, |this, cx| on_release(this, entity, cx)); - } - }), - ) + self.app.observe_release(handle, move |entity, cx| { + if let Some(this) = this.upgrade() { + this.update(cx, |this, cx| on_release(this, entity, cx)); + } + }) } pub fn observe_global( diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 8b3aa7a117..55cf04c51d 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1,7 +1,7 @@ use crate::{ px, size, Action, AnyBox, AnyDrag, AnyView, AppContext, AsyncWindowContext, AvailableSpace, - Bounds, BoxShadow, Context, Corners, DevicePixels, DispatchContext, DisplayId, ExternalPaths, - Edges, Effect, Element, EntityId, EventEmitter, FileDropEvent, FocusEvent, FontId, + Bounds, BoxShadow, Context, Corners, DevicePixels, DispatchContext, DisplayId, Edges, Effect, + Element, EntityId, EventEmitter, ExternalPaths, FileDropEvent, FocusEvent, FontId, GlobalElementId, GlyphId, Handle, Hsla, ImageData, InputEvent, IsZero, KeyListener, KeyMatch, KeyMatcher, Keystroke, LayoutId, MainThread, MainThreadOnly, Modifiers, MonochromeSprite, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Path, Pixels, PlatformAtlas, @@ -1517,22 +1517,14 @@ impl<'a, 'w, V: 'static> ViewContext<'a, 'w, V> { &mut self, handle: &Handle, mut on_release: impl FnMut(&mut V, &mut T, &mut ViewContext<'_, '_, V>) + Send + Sync + 'static, - ) -> Subscription - where - V: Any + Send + Sync, - { + ) -> Subscription { let this = self.handle(); let window_handle = self.window.handle; - self.app.release_listeners.insert( - handle.entity_id, - Box::new(move |entity, cx| { - let entity = entity.downcast_mut().expect("invalid entity type"); - // todo!("are we okay with silently swallowing the error?") - let _ = cx.update_window(window_handle.id, |cx| { - this.update(cx, |this, cx| on_release(this, entity, cx)) - }); - }), - ) + self.app.observe_release(handle, move |entity, cx| { + let _ = cx.update_window(window_handle.id, |cx| { + this.update(cx, |this, cx| on_release(this, entity, cx)) + }); + }) } pub fn notify(&mut self) { diff --git a/crates/workspace2/Cargo.toml b/crates/workspace2/Cargo.toml new file mode 100644 index 0000000000..7c55d8bedb --- /dev/null +++ b/crates/workspace2/Cargo.toml @@ -0,0 +1,65 @@ +[package] +name = "workspace2" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/workspace2.rs" +doctest = false + +[features] +test-support = [ + "call2/test-support", + "client2/test-support", + "project2/test-support", + "settings2/test-support", + "gpui2/test-support", + "fs2/test-support" +] + +[dependencies] +db2 = { path = "../db2" } +call2 = { path = "../call2" } +client2 = { path = "../client2" } +collections = { path = "../collections" } +# context_menu = { path = "../context_menu" } +fs2 = { path = "../fs2" } +gpui2 = { path = "../gpui2" } +install_cli2 = { path = "../install_cli2" } +language2 = { path = "../language2" } +#menu = { path = "../menu" } +node_runtime = { path = "../node_runtime" } +project2 = { path = "../project2" } +settings2 = { path = "../settings2" } +terminal2 = { path = "../terminal2" } +theme2 = { path = "../theme2" } +util = { path = "../util" } + +async-recursion = "1.0.0" +itertools = "0.10" +bincode = "1.2.1" +anyhow.workspace = true +futures.workspace = true +lazy_static.workspace = true +log.workspace = true +parking_lot.workspace = true +postage.workspace = true +schemars.workspace = true +serde.workspace = true +serde_derive.workspace = true +serde_json.workspace = true +smallvec.workspace = true +uuid.workspace = true + +[dev-dependencies] +call2 = { path = "../call2", features = ["test-support"] } +client2 = { path = "../client2", features = ["test-support"] } +gpui2 = { path = "../gpui2", features = ["test-support"] } +project2 = { path = "../project2", features = ["test-support"] } +settings2 = { path = "../settings2", features = ["test-support"] } +fs2 = { path = "../fs2", features = ["test-support"] } +db2 = { path = "../db2", features = ["test-support"] } + +indoc.workspace = true +env_logger.workspace = true diff --git a/crates/workspace2/src/dock.rs b/crates/workspace2/src/dock.rs new file mode 100644 index 0000000000..5445f89050 --- /dev/null +++ b/crates/workspace2/src/dock.rs @@ -0,0 +1,744 @@ +use crate::{StatusItemView, Workspace, WorkspaceBounds}; +use gpui2::{ + elements::*, platform::CursorStyle, platform::MouseButton, Action, AnyViewHandle, AppContext, + Axis, Entity, Subscription, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext, +}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::rc::Rc; +use theme2::ThemeSettings; + +pub trait Panel: View { + fn position(&self, cx: &WindowContext) -> DockPosition; + fn position_is_valid(&self, position: DockPosition) -> bool; + fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext); + fn size(&self, cx: &WindowContext) -> f32; + fn set_size(&mut self, size: Option, cx: &mut ViewContext); + fn icon_path(&self, cx: &WindowContext) -> Option<&'static str>; + fn icon_tooltip(&self) -> (String, Option>); + fn icon_label(&self, _: &WindowContext) -> Option { + None + } + fn should_change_position_on_event(_: &Self::Event) -> bool; + fn should_zoom_in_on_event(_: &Self::Event) -> bool { + false + } + fn should_zoom_out_on_event(_: &Self::Event) -> bool { + false + } + fn is_zoomed(&self, _cx: &WindowContext) -> bool { + false + } + fn set_zoomed(&mut self, _zoomed: bool, _cx: &mut ViewContext) {} + fn set_active(&mut self, _active: bool, _cx: &mut ViewContext) {} + fn should_activate_on_event(_: &Self::Event) -> bool { + false + } + fn should_close_on_event(_: &Self::Event) -> bool { + false + } + fn has_focus(&self, cx: &WindowContext) -> bool; + fn is_focus_event(_: &Self::Event) -> bool; +} + +pub trait PanelHandle { + fn id(&self) -> usize; + fn position(&self, cx: &WindowContext) -> DockPosition; + fn position_is_valid(&self, position: DockPosition, cx: &WindowContext) -> bool; + fn set_position(&self, position: DockPosition, cx: &mut WindowContext); + fn is_zoomed(&self, cx: &WindowContext) -> bool; + fn set_zoomed(&self, zoomed: bool, cx: &mut WindowContext); + fn set_active(&self, active: bool, cx: &mut WindowContext); + fn size(&self, cx: &WindowContext) -> f32; + fn set_size(&self, size: Option, cx: &mut WindowContext); + fn icon_path(&self, cx: &WindowContext) -> Option<&'static str>; + fn icon_tooltip(&self, cx: &WindowContext) -> (String, Option>); + fn icon_label(&self, cx: &WindowContext) -> Option; + fn has_focus(&self, cx: &WindowContext) -> bool; + fn as_any(&self) -> &AnyViewHandle; +} + +impl PanelHandle for ViewHandle +where + T: Panel, +{ + fn id(&self) -> usize { + self.id() + } + + fn position(&self, cx: &WindowContext) -> DockPosition { + self.read(cx).position(cx) + } + + fn position_is_valid(&self, position: DockPosition, cx: &WindowContext) -> bool { + self.read(cx).position_is_valid(position) + } + + fn set_position(&self, position: DockPosition, cx: &mut WindowContext) { + self.update(cx, |this, cx| this.set_position(position, cx)) + } + + fn size(&self, cx: &WindowContext) -> f32 { + self.read(cx).size(cx) + } + + fn set_size(&self, size: Option, cx: &mut WindowContext) { + self.update(cx, |this, cx| this.set_size(size, cx)) + } + + fn is_zoomed(&self, cx: &WindowContext) -> bool { + self.read(cx).is_zoomed(cx) + } + + fn set_zoomed(&self, zoomed: bool, cx: &mut WindowContext) { + self.update(cx, |this, cx| this.set_zoomed(zoomed, cx)) + } + + fn set_active(&self, active: bool, cx: &mut WindowContext) { + self.update(cx, |this, cx| this.set_active(active, cx)) + } + + fn icon_path(&self, cx: &WindowContext) -> Option<&'static str> { + self.read(cx).icon_path(cx) + } + + fn icon_tooltip(&self, cx: &WindowContext) -> (String, Option>) { + self.read(cx).icon_tooltip() + } + + fn icon_label(&self, cx: &WindowContext) -> Option { + self.read(cx).icon_label(cx) + } + + fn has_focus(&self, cx: &WindowContext) -> bool { + self.read(cx).has_focus(cx) + } + + fn as_any(&self) -> &AnyViewHandle { + self + } +} + +impl From<&dyn PanelHandle> for AnyViewHandle { + fn from(val: &dyn PanelHandle) -> Self { + val.as_any().clone() + } +} + +pub struct Dock { + position: DockPosition, + panel_entries: Vec, + is_open: bool, + active_panel_index: usize, +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum DockPosition { + Left, + Bottom, + Right, +} + +impl DockPosition { + fn to_label(&self) -> &'static str { + match self { + Self::Left => "left", + Self::Bottom => "bottom", + Self::Right => "right", + } + } + + fn to_resize_handle_side(self) -> HandleSide { + match self { + Self::Left => HandleSide::Right, + Self::Bottom => HandleSide::Top, + Self::Right => HandleSide::Left, + } + } + + pub fn axis(&self) -> Axis { + match self { + Self::Left | Self::Right => Axis::Horizontal, + Self::Bottom => Axis::Vertical, + } + } +} + +struct PanelEntry { + panel: Rc, + context_menu: ViewHandle, + _subscriptions: [Subscription; 2], +} + +pub struct PanelButtons { + dock: ViewHandle, + workspace: WeakViewHandle, +} + +impl Dock { + pub fn new(position: DockPosition) -> Self { + Self { + position, + panel_entries: Default::default(), + active_panel_index: 0, + is_open: false, + } + } + + pub fn position(&self) -> DockPosition { + self.position + } + + pub fn is_open(&self) -> bool { + self.is_open + } + + pub fn has_focus(&self, cx: &WindowContext) -> bool { + self.visible_panel() + .map_or(false, |panel| panel.has_focus(cx)) + } + + pub fn panel(&self) -> Option> { + self.panel_entries + .iter() + .find_map(|entry| entry.panel.as_any().clone().downcast()) + } + + pub fn panel_index_for_type(&self) -> Option { + self.panel_entries + .iter() + .position(|entry| entry.panel.as_any().is::()) + } + + pub fn panel_index_for_ui_name(&self, ui_name: &str, cx: &AppContext) -> Option { + self.panel_entries.iter().position(|entry| { + let panel = entry.panel.as_any(); + cx.view_ui_name(panel.window(), panel.id()) == Some(ui_name) + }) + } + + pub fn active_panel_index(&self) -> usize { + self.active_panel_index + } + + pub(crate) fn set_open(&mut self, open: bool, cx: &mut ViewContext) { + if open != self.is_open { + self.is_open = open; + if let Some(active_panel) = self.panel_entries.get(self.active_panel_index) { + active_panel.panel.set_active(open, cx); + } + + cx.notify(); + } + } + + pub fn set_panel_zoomed( + &mut self, + panel: &AnyViewHandle, + zoomed: bool, + cx: &mut ViewContext, + ) { + for entry in &mut self.panel_entries { + if entry.panel.as_any() == panel { + if zoomed != entry.panel.is_zoomed(cx) { + entry.panel.set_zoomed(zoomed, cx); + } + } else if entry.panel.is_zoomed(cx) { + entry.panel.set_zoomed(false, cx); + } + } + + cx.notify(); + } + + pub fn zoom_out(&mut self, cx: &mut ViewContext) { + for entry in &mut self.panel_entries { + if entry.panel.is_zoomed(cx) { + entry.panel.set_zoomed(false, cx); + } + } + } + + pub(crate) fn add_panel(&mut self, panel: ViewHandle, cx: &mut ViewContext) { + let subscriptions = [ + cx.observe(&panel, |_, _, cx| cx.notify()), + cx.subscribe(&panel, |this, panel, event, cx| { + if T::should_activate_on_event(event) { + if let Some(ix) = this + .panel_entries + .iter() + .position(|entry| entry.panel.id() == panel.id()) + { + this.set_open(true, cx); + this.activate_panel(ix, cx); + cx.focus(&panel); + } + } else if T::should_close_on_event(event) + && this.visible_panel().map_or(false, |p| p.id() == panel.id()) + { + this.set_open(false, cx); + } + }), + ]; + + let dock_view_id = cx.view_id(); + self.panel_entries.push(PanelEntry { + panel: Rc::new(panel), + context_menu: cx.add_view(|cx| { + let mut menu = ContextMenu::new(dock_view_id, cx); + menu.set_position_mode(OverlayPositionMode::Local); + menu + }), + _subscriptions: subscriptions, + }); + cx.notify() + } + + pub fn remove_panel(&mut self, panel: &ViewHandle, cx: &mut ViewContext) { + if let Some(panel_ix) = self + .panel_entries + .iter() + .position(|entry| entry.panel.id() == panel.id()) + { + if panel_ix == self.active_panel_index { + self.active_panel_index = 0; + self.set_open(false, cx); + } else if panel_ix < self.active_panel_index { + self.active_panel_index -= 1; + } + self.panel_entries.remove(panel_ix); + cx.notify(); + } + } + + pub fn panels_len(&self) -> usize { + self.panel_entries.len() + } + + pub fn activate_panel(&mut self, panel_ix: usize, cx: &mut ViewContext) { + if panel_ix != self.active_panel_index { + if let Some(active_panel) = self.panel_entries.get(self.active_panel_index) { + active_panel.panel.set_active(false, cx); + } + + self.active_panel_index = panel_ix; + if let Some(active_panel) = self.panel_entries.get(self.active_panel_index) { + active_panel.panel.set_active(true, cx); + } + + cx.notify(); + } + } + + pub fn visible_panel(&self) -> Option<&Rc> { + let entry = self.visible_entry()?; + Some(&entry.panel) + } + + pub fn active_panel(&self) -> Option<&Rc> { + Some(&self.panel_entries.get(self.active_panel_index)?.panel) + } + + fn visible_entry(&self) -> Option<&PanelEntry> { + if self.is_open { + self.panel_entries.get(self.active_panel_index) + } else { + None + } + } + + pub fn zoomed_panel(&self, cx: &WindowContext) -> Option> { + let entry = self.visible_entry()?; + if entry.panel.is_zoomed(cx) { + Some(entry.panel.clone()) + } else { + None + } + } + + pub fn panel_size(&self, panel: &dyn PanelHandle, cx: &WindowContext) -> Option { + self.panel_entries + .iter() + .find(|entry| entry.panel.id() == panel.id()) + .map(|entry| entry.panel.size(cx)) + } + + pub fn active_panel_size(&self, cx: &WindowContext) -> Option { + if self.is_open { + self.panel_entries + .get(self.active_panel_index) + .map(|entry| entry.panel.size(cx)) + } else { + None + } + } + + pub fn resize_active_panel(&mut self, size: Option, cx: &mut ViewContext) { + if let Some(entry) = self.panel_entries.get_mut(self.active_panel_index) { + entry.panel.set_size(size, cx); + cx.notify(); + } + } + + pub fn render_placeholder(&self, cx: &WindowContext) -> AnyElement { + if let Some(active_entry) = self.visible_entry() { + Empty::new() + .into_any() + .contained() + .with_style(self.style(cx)) + .resizable::( + self.position.to_resize_handle_side(), + active_entry.panel.size(cx), + |_, _, _| {}, + ) + .into_any() + } else { + Empty::new().into_any() + } + } + + fn style(&self, cx: &WindowContext) -> ContainerStyle { + let theme = &settings::get::(cx).theme; + let style = match self.position { + DockPosition::Left => theme.workspace.dock.left, + DockPosition::Bottom => theme.workspace.dock.bottom, + DockPosition::Right => theme.workspace.dock.right, + }; + style + } +} + +impl Entity for Dock { + type Event = (); +} + +impl View for Dock { + fn ui_name() -> &'static str { + "Dock" + } + + fn render(&mut self, cx: &mut ViewContext) -> AnyElement { + if let Some(active_entry) = self.visible_entry() { + let style = self.style(cx); + ChildView::new(active_entry.panel.as_any(), cx) + .contained() + .with_style(style) + .resizable::( + self.position.to_resize_handle_side(), + active_entry.panel.size(cx), + |dock: &mut Self, size, cx| dock.resize_active_panel(size, cx), + ) + .into_any() + } else { + Empty::new().into_any() + } + } + + fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { + if cx.is_self_focused() { + if let Some(active_entry) = self.visible_entry() { + cx.focus(active_entry.panel.as_any()); + } else { + cx.focus_parent(); + } + } + } +} + +impl PanelButtons { + pub fn new( + dock: ViewHandle, + workspace: WeakViewHandle, + cx: &mut ViewContext, + ) -> Self { + cx.observe(&dock, |_, _, cx| cx.notify()).detach(); + Self { dock, workspace } + } +} + +impl Entity for PanelButtons { + type Event = (); +} + +impl View for PanelButtons { + fn ui_name() -> &'static str { + "PanelButtons" + } + + fn render(&mut self, cx: &mut ViewContext) -> AnyElement { + let theme = &settings::get::(cx).theme; + let tooltip_style = theme.tooltip.clone(); + let theme = &theme.workspace.status_bar.panel_buttons; + let button_style = theme.button.clone(); + let dock = self.dock.read(cx); + let active_ix = dock.active_panel_index; + let is_open = dock.is_open; + let dock_position = dock.position; + let group_style = match dock_position { + DockPosition::Left => theme.group_left, + DockPosition::Bottom => theme.group_bottom, + DockPosition::Right => theme.group_right, + }; + let menu_corner = match dock_position { + DockPosition::Left => AnchorCorner::BottomLeft, + DockPosition::Bottom | DockPosition::Right => AnchorCorner::BottomRight, + }; + + let panels = dock + .panel_entries + .iter() + .map(|item| (item.panel.clone(), item.context_menu.clone())) + .collect::>(); + Flex::row() + .with_children(panels.into_iter().enumerate().filter_map( + |(panel_ix, (view, context_menu))| { + let icon_path = view.icon_path(cx)?; + let is_active = is_open && panel_ix == active_ix; + let (tooltip, tooltip_action) = if is_active { + ( + format!("Close {} dock", dock_position.to_label()), + Some(match dock_position { + DockPosition::Left => crate::ToggleLeftDock.boxed_clone(), + DockPosition::Bottom => crate::ToggleBottomDock.boxed_clone(), + DockPosition::Right => crate::ToggleRightDock.boxed_clone(), + }), + ) + } else { + view.icon_tooltip(cx) + }; + Some( + Stack::new() + .with_child( + MouseEventHandler::new::(panel_ix, cx, |state, cx| { + let style = button_style.in_state(is_active); + + let style = style.style_for(state); + Flex::row() + .with_child( + Svg::new(icon_path) + .with_color(style.icon_color) + .constrained() + .with_width(style.icon_size) + .aligned(), + ) + .with_children(if let Some(label) = view.icon_label(cx) { + Some( + Label::new(label, style.label.text.clone()) + .contained() + .with_style(style.label.container) + .aligned(), + ) + } else { + None + }) + .constrained() + .with_height(style.icon_size) + .contained() + .with_style(style.container) + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, { + let tooltip_action = + tooltip_action.as_ref().map(|action| action.boxed_clone()); + move |_, this, cx| { + if let Some(tooltip_action) = &tooltip_action { + let window = cx.window(); + let view_id = this.workspace.id(); + let tooltip_action = tooltip_action.boxed_clone(); + cx.spawn(|_, mut cx| async move { + window.dispatch_action( + view_id, + &*tooltip_action, + &mut cx, + ); + }) + .detach(); + } + } + }) + .on_click(MouseButton::Right, { + let view = view.clone(); + let menu = context_menu.clone(); + move |_, _, cx| { + const POSITIONS: [DockPosition; 3] = [ + DockPosition::Left, + DockPosition::Right, + DockPosition::Bottom, + ]; + + menu.update(cx, |menu, cx| { + let items = POSITIONS + .into_iter() + .filter(|position| { + *position != dock_position + && view.position_is_valid(*position, cx) + }) + .map(|position| { + let view = view.clone(); + ContextMenuItem::handler( + format!("Dock {}", position.to_label()), + move |cx| view.set_position(position, cx), + ) + }) + .collect(); + menu.show(Default::default(), menu_corner, items, cx); + }) + } + }) + .with_tooltip::( + panel_ix, + tooltip, + tooltip_action, + tooltip_style.clone(), + cx, + ), + ) + .with_child(ChildView::new(&context_menu, cx)), + ) + }, + )) + .contained() + .with_style(group_style) + .into_any() + } +} + +impl StatusItemView for PanelButtons { + fn set_active_pane_item( + &mut self, + _: Option<&dyn crate::ItemHandle>, + _: &mut ViewContext, + ) { + } +} + +#[cfg(any(test, feature = "test-support"))] +pub mod test { + use super::*; + use gpui2::{ViewContext, WindowContext}; + + #[derive(Debug)] + pub enum TestPanelEvent { + PositionChanged, + Activated, + Closed, + ZoomIn, + ZoomOut, + Focus, + } + + pub struct TestPanel { + pub position: DockPosition, + pub zoomed: bool, + pub active: bool, + pub has_focus: bool, + pub size: f32, + } + + impl TestPanel { + pub fn new(position: DockPosition) -> Self { + Self { + position, + zoomed: false, + active: false, + has_focus: false, + size: 300., + } + } + } + + impl Entity for TestPanel { + type Event = TestPanelEvent; + } + + impl View for TestPanel { + fn ui_name() -> &'static str { + "TestPanel" + } + + fn render(&mut self, _: &mut ViewContext<'_, '_, Self>) -> AnyElement { + Empty::new().into_any() + } + + fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { + self.has_focus = true; + cx.emit(TestPanelEvent::Focus); + } + + fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext) { + self.has_focus = false; + } + } + + impl Panel for TestPanel { + fn position(&self, _: &gpui::WindowContext) -> super::DockPosition { + self.position + } + + fn position_is_valid(&self, _: super::DockPosition) -> bool { + true + } + + fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext) { + self.position = position; + cx.emit(TestPanelEvent::PositionChanged); + } + + fn is_zoomed(&self, _: &WindowContext) -> bool { + self.zoomed + } + + fn set_zoomed(&mut self, zoomed: bool, _cx: &mut ViewContext) { + self.zoomed = zoomed; + } + + fn set_active(&mut self, active: bool, _cx: &mut ViewContext) { + self.active = active; + } + + fn size(&self, _: &WindowContext) -> f32 { + self.size + } + + fn set_size(&mut self, size: Option, _: &mut ViewContext) { + self.size = size.unwrap_or(300.); + } + + fn icon_path(&self, _: &WindowContext) -> Option<&'static str> { + Some("icons/test_panel.svg") + } + + fn icon_tooltip(&self) -> (String, Option>) { + ("Test Panel".into(), None) + } + + fn should_change_position_on_event(event: &Self::Event) -> bool { + matches!(event, TestPanelEvent::PositionChanged) + } + + fn should_zoom_in_on_event(event: &Self::Event) -> bool { + matches!(event, TestPanelEvent::ZoomIn) + } + + fn should_zoom_out_on_event(event: &Self::Event) -> bool { + matches!(event, TestPanelEvent::ZoomOut) + } + + fn should_activate_on_event(event: &Self::Event) -> bool { + matches!(event, TestPanelEvent::Activated) + } + + fn should_close_on_event(event: &Self::Event) -> bool { + matches!(event, TestPanelEvent::Closed) + } + + fn has_focus(&self, _cx: &WindowContext) -> bool { + self.has_focus + } + + fn is_focus_event(event: &Self::Event) -> bool { + matches!(event, TestPanelEvent::Focus) + } + } +} diff --git a/crates/workspace2/src/item.rs b/crates/workspace2/src/item.rs new file mode 100644 index 0000000000..39a1f0d51a --- /dev/null +++ b/crates/workspace2/src/item.rs @@ -0,0 +1,1081 @@ +use crate::{ + pane, persistence::model::ItemId, searchable::SearchableItemHandle, FollowableItemBuilders, + ItemNavHistory, Pane, ToolbarItemLocation, ViewId, Workspace, WorkspaceId, +}; +use crate::{AutosaveSetting, DelayedDebouncedEditAction, WorkspaceSettings}; +use anyhow::Result; +use client2::{ + proto::{self, PeerId}, + Client, +}; +use gpui2::geometry::vector::Vector2F; +use gpui2::AnyWindowHandle; +use gpui2::{ + fonts::HighlightStyle, AnyElement, AnyViewHandle, AppContext, ModelHandle, Task, View, + ViewContext, ViewHandle, WeakViewHandle, WindowContext, +}; +use project2::{Project, ProjectEntryId, ProjectPath}; +use schemars::JsonSchema; +use serde_derive::{Deserialize, Serialize}; +use settings2::Setting; +use smallvec::SmallVec; +use std::{ + any::{Any, TypeId}, + borrow::Cow, + cell::RefCell, + fmt, + ops::Range, + path::PathBuf, + rc::Rc, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, + time::Duration, +}; +use theme2::Theme; + +#[derive(Deserialize)] +pub struct ItemSettings { + pub git_status: bool, + pub close_position: ClosePosition, +} + +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "lowercase")] +pub enum ClosePosition { + Left, + #[default] + Right, +} + +impl ClosePosition { + pub fn right(&self) -> bool { + match self { + ClosePosition::Left => false, + ClosePosition::Right => true, + } + } +} + +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] +pub struct ItemSettingsContent { + git_status: Option, + close_position: Option, +} + +impl Setting for ItemSettings { + const KEY: Option<&'static str> = Some("tabs"); + + type FileContent = ItemSettingsContent; + + fn load( + default_value: &Self::FileContent, + user_values: &[&Self::FileContent], + _: &gpui2::AppContext, + ) -> anyhow::Result { + Self::load_via_json_merge(default_value, user_values) + } +} + +#[derive(Eq, PartialEq, Hash, Debug)] +pub enum ItemEvent { + CloseItem, + UpdateTab, + UpdateBreadcrumbs, + Edit, +} + +// TODO: Combine this with existing HighlightedText struct? +pub struct BreadcrumbText { + pub text: String, + pub highlights: Option, HighlightStyle)>>, +} + +pub trait Item: View { + fn deactivated(&mut self, _: &mut ViewContext) {} + fn workspace_deactivated(&mut self, _: &mut ViewContext) {} + fn navigate(&mut self, _: Box, _: &mut ViewContext) -> bool { + false + } + fn tab_tooltip_text(&self, _: &AppContext) -> Option> { + None + } + fn tab_description<'a>(&'a self, _: usize, _: &'a AppContext) -> Option> { + None + } + fn tab_content( + &self, + detail: Option, + style: &theme2::Tab, + cx: &AppContext, + ) -> AnyElement; + fn for_each_project_item(&self, _: &AppContext, _: &mut dyn FnMut(usize, &dyn project2::Item)) { + } // (model id, Item) + fn is_singleton(&self, _cx: &AppContext) -> bool { + false + } + fn set_nav_history(&mut self, _: ItemNavHistory, _: &mut ViewContext) {} + fn clone_on_split(&self, _workspace_id: WorkspaceId, _: &mut ViewContext) -> Option + where + Self: Sized, + { + None + } + fn is_dirty(&self, _: &AppContext) -> bool { + false + } + fn has_conflict(&self, _: &AppContext) -> bool { + false + } + fn can_save(&self, _cx: &AppContext) -> bool { + false + } + fn save( + &mut self, + _project: ModelHandle, + _cx: &mut ViewContext, + ) -> Task> { + unimplemented!("save() must be implemented if can_save() returns true") + } + fn save_as( + &mut self, + _project: ModelHandle, + _abs_path: PathBuf, + _cx: &mut ViewContext, + ) -> Task> { + unimplemented!("save_as() must be implemented if can_save() returns true") + } + fn reload( + &mut self, + _project: ModelHandle, + _cx: &mut ViewContext, + ) -> Task> { + unimplemented!("reload() must be implemented if can_save() returns true") + } + fn to_item_events(_event: &Self::Event) -> SmallVec<[ItemEvent; 2]> { + SmallVec::new() + } + fn should_close_item_on_event(_: &Self::Event) -> bool { + false + } + fn should_update_tab_on_event(_: &Self::Event) -> bool { + false + } + + fn act_as_type<'a>( + &'a self, + type_id: TypeId, + self_handle: &'a ViewHandle, + _: &'a AppContext, + ) -> Option<&AnyViewHandle> { + if TypeId::of::() == type_id { + Some(self_handle) + } else { + None + } + } + + fn as_searchable(&self, _: &ViewHandle) -> Option> { + None + } + + fn breadcrumb_location(&self) -> ToolbarItemLocation { + ToolbarItemLocation::Hidden + } + + fn breadcrumbs(&self, _theme: &Theme, _cx: &AppContext) -> Option> { + None + } + + fn added_to_workspace(&mut self, _workspace: &mut Workspace, _cx: &mut ViewContext) {} + + fn serialized_item_kind() -> Option<&'static str> { + None + } + + fn deserialize( + _project: ModelHandle, + _workspace: WeakViewHandle, + _workspace_id: WorkspaceId, + _item_id: ItemId, + _cx: &mut ViewContext, + ) -> Task>> { + unimplemented!( + "deserialize() must be implemented if serialized_item_kind() returns Some(_)" + ) + } + fn show_toolbar(&self) -> bool { + true + } + fn pixel_position_of_cursor(&self, _: &AppContext) -> Option { + None + } +} + +pub trait ItemHandle: 'static + fmt::Debug { + fn subscribe_to_item_events( + &self, + cx: &mut WindowContext, + handler: Box, + ) -> gpui2::Subscription; + fn tab_tooltip_text<'a>(&self, cx: &'a AppContext) -> Option>; + fn tab_description<'a>(&'a self, detail: usize, cx: &'a AppContext) -> Option>; + fn tab_content( + &self, + detail: Option, + style: &theme2::Tab, + cx: &AppContext, + ) -> AnyElement; + fn dragged_tab_content( + &self, + detail: Option, + style: &theme2::Tab, + cx: &AppContext, + ) -> AnyElement; + fn project_path(&self, cx: &AppContext) -> Option; + fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]>; + fn project_item_model_ids(&self, cx: &AppContext) -> SmallVec<[usize; 3]>; + fn for_each_project_item(&self, _: &AppContext, _: &mut dyn FnMut(usize, &dyn project2::Item)); + fn is_singleton(&self, cx: &AppContext) -> bool; + fn boxed_clone(&self) -> Box; + fn clone_on_split( + &self, + workspace_id: WorkspaceId, + cx: &mut WindowContext, + ) -> Option>; + fn added_to_pane( + &self, + workspace: &mut Workspace, + pane: ViewHandle, + cx: &mut ViewContext, + ); + fn deactivated(&self, cx: &mut WindowContext); + fn workspace_deactivated(&self, cx: &mut WindowContext); + fn navigate(&self, data: Box, cx: &mut WindowContext) -> bool; + fn id(&self) -> usize; + fn window(&self) -> AnyWindowHandle; + fn as_any(&self) -> &AnyViewHandle; + fn is_dirty(&self, cx: &AppContext) -> bool; + fn has_conflict(&self, cx: &AppContext) -> bool; + fn can_save(&self, cx: &AppContext) -> bool; + fn save(&self, project: ModelHandle, cx: &mut WindowContext) -> Task>; + fn save_as( + &self, + project: ModelHandle, + abs_path: PathBuf, + cx: &mut WindowContext, + ) -> Task>; + fn reload(&self, project: ModelHandle, cx: &mut WindowContext) -> Task>; + fn act_as_type<'a>(&'a self, type_id: TypeId, cx: &'a AppContext) -> Option<&'a AnyViewHandle>; + fn to_followable_item_handle(&self, cx: &AppContext) -> Option>; + fn on_release( + &self, + cx: &mut AppContext, + callback: Box, + ) -> gpui2::Subscription; + fn to_searchable_item_handle(&self, cx: &AppContext) -> Option>; + fn breadcrumb_location(&self, cx: &AppContext) -> ToolbarItemLocation; + fn breadcrumbs(&self, theme: &Theme, cx: &AppContext) -> Option>; + fn serialized_item_kind(&self) -> Option<&'static str>; + fn show_toolbar(&self, cx: &AppContext) -> bool; + fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option; +} + +pub trait WeakItemHandle { + fn id(&self) -> usize; + fn window(&self) -> AnyWindowHandle; + fn upgrade(&self, cx: &AppContext) -> Option>; +} + +impl dyn ItemHandle { + pub fn downcast(&self) -> Option> { + self.as_any().clone().downcast() + } + + pub fn act_as(&self, cx: &AppContext) -> Option> { + self.act_as_type(TypeId::of::(), cx) + .and_then(|t| t.clone().downcast()) + } +} + +impl ItemHandle for ViewHandle { + fn subscribe_to_item_events( + &self, + cx: &mut WindowContext, + handler: Box, + ) -> gpui2::Subscription { + cx.subscribe(self, move |_, event, cx| { + for item_event in T::to_item_events(event) { + handler(item_event, cx) + } + }) + } + + fn tab_tooltip_text<'a>(&self, cx: &'a AppContext) -> Option> { + self.read(cx).tab_tooltip_text(cx) + } + + fn tab_description<'a>(&'a self, detail: usize, cx: &'a AppContext) -> Option> { + self.read(cx).tab_description(detail, cx) + } + + fn tab_content( + &self, + detail: Option, + style: &theme2::Tab, + cx: &AppContext, + ) -> AnyElement { + self.read(cx).tab_content(detail, style, cx) + } + + fn dragged_tab_content( + &self, + detail: Option, + style: &theme2::Tab, + cx: &AppContext, + ) -> AnyElement { + self.read(cx).tab_content(detail, style, cx) + } + + fn project_path(&self, cx: &AppContext) -> Option { + let this = self.read(cx); + let mut result = None; + if this.is_singleton(cx) { + this.for_each_project_item(cx, &mut |_, item| { + result = item.project_path(cx); + }); + } + result + } + + fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]> { + let mut result = SmallVec::new(); + self.read(cx).for_each_project_item(cx, &mut |_, item| { + if let Some(id) = item.entry_id(cx) { + result.push(id); + } + }); + result + } + + fn project_item_model_ids(&self, cx: &AppContext) -> SmallVec<[usize; 3]> { + let mut result = SmallVec::new(); + self.read(cx).for_each_project_item(cx, &mut |id, _| { + result.push(id); + }); + result + } + + fn for_each_project_item( + &self, + cx: &AppContext, + f: &mut dyn FnMut(usize, &dyn project2::Item), + ) { + self.read(cx).for_each_project_item(cx, f) + } + + fn is_singleton(&self, cx: &AppContext) -> bool { + self.read(cx).is_singleton(cx) + } + + fn boxed_clone(&self) -> Box { + Box::new(self.clone()) + } + + fn clone_on_split( + &self, + workspace_id: WorkspaceId, + cx: &mut WindowContext, + ) -> Option> { + self.update(cx, |item, cx| { + cx.add_option_view(|cx| item.clone_on_split(workspace_id, cx)) + }) + .map(|handle| Box::new(handle) as Box) + } + + fn added_to_pane( + &self, + workspace: &mut Workspace, + 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); + this.added_to_workspace(workspace, 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( + followed_item.is_project_item(cx), + proto::update_followers::Variant::CreateView(proto::View { + id: followed_item + .remote_id(&workspace.app_state.client, cx) + .map(|id| id.to_proto()), + variant: Some(message), + leader_id: workspace.leader_for_pane(&pane), + }), + cx, + ); + } + } + + if workspace + .panes_by_item + .insert(self.id(), pane.downgrade()) + .is_none() + { + let mut pending_autosave = DelayedDebouncedEditAction::new(); + let pending_update = Rc::new(RefCell::new(None)); + let pending_update_scheduled = Rc::new(AtomicBool::new(false)); + + let mut event_subscription = + Some(cx.subscribe(self, move |workspace, item, event, cx| { + let pane = if let Some(pane) = workspace + .panes_by_item + .get(&item.id()) + .and_then(|pane| pane.upgrade(cx)) + { + pane + } else { + log::error!("unexpected item event after pane was dropped"); + return; + }; + + if let Some(item) = item.to_followable_item_handle(cx) { + let is_project_item = item.is_project_item(cx); + let leader_id = workspace.leader_for_pane(&pane); + + if leader_id.is_some() && item.should_unfollow_on_event(event, cx) { + workspace.unfollow(&pane, cx); + } + + if item.add_event_to_update_proto( + event, + &mut *pending_update.borrow_mut(), + cx, + ) && !pending_update_scheduled.load(Ordering::SeqCst) + { + pending_update_scheduled.store(true, Ordering::SeqCst); + cx.after_window_update({ + let pending_update = pending_update.clone(); + let pending_update_scheduled = pending_update_scheduled.clone(); + move |this, cx| { + pending_update_scheduled.store(false, Ordering::SeqCst); + this.update_followers( + is_project_item, + proto::update_followers::Variant::UpdateView( + proto::UpdateView { + id: item + .remote_id(&this.app_state.client, cx) + .map(|id| id.to_proto()), + variant: pending_update.borrow_mut().take(), + leader_id, + }, + ), + cx, + ); + } + }); + } + } + + for item_event in T::to_item_events(event).into_iter() { + match item_event { + ItemEvent::CloseItem => { + pane.update(cx, |pane, cx| { + pane.close_item_by_id(item.id(), crate::SaveIntent::Close, cx) + }) + .detach_and_log_err(cx); + return; + } + + ItemEvent::UpdateTab => { + pane.update(cx, |_, cx| { + cx.emit(pane::Event::ChangeItemTitle); + cx.notify(); + }); + } + + ItemEvent::Edit => { + let autosave = settings2::get::(cx).autosave; + if let AutosaveSetting::AfterDelay { milliseconds } = autosave { + let delay = Duration::from_millis(milliseconds); + let item = item.clone(); + pending_autosave.fire_new(delay, cx, move |workspace, cx| { + Pane::autosave_item(&item, workspace.project().clone(), cx) + }); + } + } + + _ => {} + } + } + })); + + cx.observe_focus(self, move |workspace, item, focused, cx| { + if !focused + && settings2::get::(cx).autosave + == AutosaveSetting::OnFocusChange + { + Pane::autosave_item(&item, workspace.project.clone(), cx) + .detach_and_log_err(cx); + } + }) + .detach(); + + let item_id = self.id(); + cx.observe_release(self, move |workspace, _, _| { + workspace.panes_by_item.remove(&item_id); + event_subscription.take(); + }) + .detach(); + } + + cx.defer(|workspace, cx| { + workspace.serialize_workspace(cx); + }); + } + + fn deactivated(&self, cx: &mut WindowContext) { + self.update(cx, |this, cx| this.deactivated(cx)); + } + + fn workspace_deactivated(&self, cx: &mut WindowContext) { + self.update(cx, |this, cx| this.workspace_deactivated(cx)); + } + + fn navigate(&self, data: Box, cx: &mut WindowContext) -> bool { + self.update(cx, |this, cx| this.navigate(data, cx)) + } + + fn id(&self) -> usize { + self.id() + } + + fn window(&self) -> AnyWindowHandle { + AnyViewHandle::window(self) + } + + fn as_any(&self) -> &AnyViewHandle { + self + } + + fn is_dirty(&self, cx: &AppContext) -> bool { + self.read(cx).is_dirty(cx) + } + + fn has_conflict(&self, cx: &AppContext) -> bool { + self.read(cx).has_conflict(cx) + } + + fn can_save(&self, cx: &AppContext) -> bool { + self.read(cx).can_save(cx) + } + + fn save(&self, project: ModelHandle, cx: &mut WindowContext) -> Task> { + self.update(cx, |item, cx| item.save(project, cx)) + } + + fn save_as( + &self, + project: ModelHandle, + abs_path: PathBuf, + cx: &mut WindowContext, + ) -> Task> { + self.update(cx, |item, cx| item.save_as(project, abs_path, cx)) + } + + fn reload(&self, project: ModelHandle, cx: &mut WindowContext) -> Task> { + self.update(cx, |item, cx| item.reload(project, cx)) + } + + fn act_as_type<'a>(&'a self, type_id: TypeId, cx: &'a AppContext) -> Option<&'a AnyViewHandle> { + self.read(cx).act_as_type(type_id, self, cx) + } + + fn to_followable_item_handle(&self, cx: &AppContext) -> Option> { + if cx.has_global::() { + let builders = cx.global::(); + let item = self.as_any(); + Some(builders.get(&item.view_type())?.1(item)) + } else { + None + } + } + + fn on_release( + &self, + cx: &mut AppContext, + callback: Box, + ) -> gpui2::Subscription { + cx.observe_release(self, move |_, cx| callback(cx)) + } + + fn to_searchable_item_handle(&self, cx: &AppContext) -> Option> { + self.read(cx).as_searchable(self) + } + + fn breadcrumb_location(&self, cx: &AppContext) -> ToolbarItemLocation { + self.read(cx).breadcrumb_location() + } + + fn breadcrumbs(&self, theme: &Theme, cx: &AppContext) -> Option> { + self.read(cx).breadcrumbs(theme, cx) + } + + fn serialized_item_kind(&self) -> Option<&'static str> { + T::serialized_item_kind() + } + + fn show_toolbar(&self, cx: &AppContext) -> bool { + self.read(cx).show_toolbar() + } + + fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option { + self.read(cx).pixel_position_of_cursor(cx) + } +} + +impl From> for AnyViewHandle { + fn from(val: Box) -> Self { + val.as_any().clone() + } +} + +impl From<&Box> for AnyViewHandle { + fn from(val: &Box) -> Self { + val.as_any().clone() + } +} + +impl Clone for Box { + fn clone(&self) -> Box { + self.boxed_clone() + } +} + +impl WeakItemHandle for WeakViewHandle { + fn id(&self) -> usize { + self.id() + } + + fn window(&self) -> AnyWindowHandle { + self.window() + } + + fn upgrade(&self, cx: &AppContext) -> Option> { + self.upgrade(cx).map(|v| Box::new(v) as Box) + } +} + +pub trait ProjectItem: Item { + type Item: project2::Item + gpui2::Entity; + + fn for_project_item( + project: ModelHandle, + item: ModelHandle, + cx: &mut ViewContext, + ) -> Self; +} + +pub trait FollowableItem: Item { + fn remote_id(&self) -> Option; + fn to_state_proto(&self, cx: &AppContext) -> Option; + fn from_state_proto( + pane: ViewHandle, + project: ViewHandle, + id: ViewId, + state: &mut Option, + cx: &mut AppContext, + ) -> Option>>>; + fn add_event_to_update_proto( + &self, + event: &Self::Event, + update: &mut Option, + cx: &AppContext, + ) -> bool; + fn apply_update_proto( + &mut self, + project: &ModelHandle, + message: proto::update_view::Variant, + cx: &mut ViewContext, + ) -> Task>; + fn is_project_item(&self, cx: &AppContext) -> bool; + + fn set_leader_peer_id(&mut self, leader_peer_id: Option, cx: &mut ViewContext); + fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool; +} + +pub trait FollowableItemHandle: ItemHandle { + fn remote_id(&self, client: &Arc, cx: &AppContext) -> Option; + fn set_leader_peer_id(&self, leader_peer_id: Option, cx: &mut WindowContext); + fn to_state_proto(&self, cx: &AppContext) -> Option; + fn add_event_to_update_proto( + &self, + event: &dyn Any, + update: &mut Option, + cx: &AppContext, + ) -> bool; + fn apply_update_proto( + &self, + project: &ModelHandle, + message: proto::update_view::Variant, + cx: &mut WindowContext, + ) -> Task>; + fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool; + fn is_project_item(&self, cx: &AppContext) -> bool; +} + +impl FollowableItemHandle for ViewHandle { + fn remote_id(&self, client: &Arc, cx: &AppContext) -> Option { + self.read(cx).remote_id().or_else(|| { + client.peer_id().map(|creator| ViewId { + creator, + id: self.id() as u64, + }) + }) + } + + fn set_leader_peer_id(&self, leader_peer_id: Option, cx: &mut WindowContext) { + self.update(cx, |this, cx| this.set_leader_peer_id(leader_peer_id, cx)) + } + + fn to_state_proto(&self, cx: &AppContext) -> Option { + self.read(cx).to_state_proto(cx) + } + + fn add_event_to_update_proto( + &self, + event: &dyn Any, + update: &mut Option, + cx: &AppContext, + ) -> bool { + if let Some(event) = event.downcast_ref() { + self.read(cx).add_event_to_update_proto(event, update, cx) + } else { + false + } + } + + fn apply_update_proto( + &self, + project: &ModelHandle, + message: proto::update_view::Variant, + cx: &mut WindowContext, + ) -> Task> { + self.update(cx, |this, cx| this.apply_update_proto(project, message, cx)) + } + + fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool { + if let Some(event) = event.downcast_ref() { + T::should_unfollow_on_event(event, cx) + } else { + false + } + } + + fn is_project_item(&self, cx: &AppContext) -> bool { + self.read(cx).is_project_item(cx) + } +} + +#[cfg(any(test, feature = "test-support"))] +pub mod test { + use super::{Item, ItemEvent}; + use crate::{ItemId, ItemNavHistory, Pane, Workspace, WorkspaceId}; + use gpui2::{ + elements::Empty, AnyElement, AppContext, Element, Entity, ModelHandle, Task, View, + ViewContext, ViewHandle, WeakViewHandle, + }; + use project2::{Project, ProjectEntryId, ProjectPath, WorktreeId}; + use smallvec::SmallVec; + use std::{any::Any, borrow::Cow, cell::Cell, path::Path}; + + pub struct TestProjectItem { + pub entry_id: Option, + pub project_path: Option, + } + + pub struct TestItem { + pub workspace_id: WorkspaceId, + pub state: String, + pub label: String, + pub save_count: usize, + pub save_as_count: usize, + pub reload_count: usize, + pub is_dirty: bool, + pub is_singleton: bool, + pub has_conflict: bool, + pub project_items: Vec>, + pub nav_history: Option, + pub tab_descriptions: Option>, + pub tab_detail: Cell>, + } + + impl Entity for TestProjectItem { + type Event = (); + } + + impl project2::Item for TestProjectItem { + fn entry_id(&self, _: &AppContext) -> Option { + self.entry_id + } + + fn project_path(&self, _: &AppContext) -> Option { + self.project_path.clone() + } + } + + pub enum TestItemEvent { + Edit, + } + + impl Clone for TestItem { + fn clone(&self) -> Self { + Self { + state: self.state.clone(), + label: self.label.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_items: self.project_items.clone(), + nav_history: None, + tab_descriptions: None, + tab_detail: Default::default(), + workspace_id: self.workspace_id, + } + } + } + + impl TestProjectItem { + pub fn new(id: u64, path: &str, cx: &mut AppContext) -> ModelHandle { + let entry_id = Some(ProjectEntryId::from_proto(id)); + let project_path = Some(ProjectPath { + worktree_id: WorktreeId::from_usize(0), + path: Path::new(path).into(), + }); + cx.add_model(|_| Self { + entry_id, + project_path, + }) + } + + pub fn new_untitled(cx: &mut AppContext) -> ModelHandle { + cx.add_model(|_| Self { + project_path: None, + entry_id: None, + }) + } + } + + impl TestItem { + pub fn new() -> Self { + Self { + state: String::new(), + label: String::new(), + save_count: 0, + save_as_count: 0, + reload_count: 0, + is_dirty: false, + has_conflict: false, + project_items: Vec::new(), + is_singleton: true, + nav_history: None, + tab_descriptions: None, + tab_detail: Default::default(), + workspace_id: 0, + } + } + + pub fn new_deserialized(id: WorkspaceId) -> Self { + let mut this = Self::new(); + this.workspace_id = id; + this + } + + pub fn with_label(mut self, state: &str) -> Self { + self.label = state.to_string(); + self + } + + pub fn with_singleton(mut self, singleton: bool) -> Self { + self.is_singleton = singleton; + self + } + + pub fn with_dirty(mut self, dirty: bool) -> Self { + self.is_dirty = dirty; + self + } + + pub fn with_conflict(mut self, has_conflict: bool) -> Self { + self.has_conflict = has_conflict; + self + } + + pub fn with_project_items(mut self, items: &[ModelHandle]) -> Self { + self.project_items.clear(); + self.project_items.extend(items.iter().cloned()); + self + } + + pub 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); + } + } + } + + impl Entity for TestItem { + type Event = TestItemEvent; + } + + impl View for TestItem { + fn ui_name() -> &'static str { + "TestItem" + } + + fn render(&mut self, _: &mut ViewContext) -> AnyElement { + Empty::new().into_any() + } + } + + impl Item for TestItem { + fn tab_description(&self, detail: usize, _: &AppContext) -> Option> { + self.tab_descriptions.as_ref().and_then(|descriptions| { + let description = *descriptions.get(detail).or_else(|| descriptions.last())?; + Some(description.into()) + }) + } + + fn tab_content( + &self, + detail: Option, + _: &theme2::Tab, + _: &AppContext, + ) -> AnyElement { + self.tab_detail.set(detail); + Empty::new().into_any() + } + + fn for_each_project_item( + &self, + cx: &AppContext, + f: &mut dyn FnMut(usize, &dyn project2::Item), + ) { + self.project_items + .iter() + .for_each(|item| f(item.id(), item.read(cx))) + } + + fn is_singleton(&self, _: &AppContext) -> bool { + self.is_singleton + } + + 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, + _workspace_id: WorkspaceId, + _: &mut ViewContext, + ) -> Option + where + Self: Sized, + { + Some(self.clone()) + } + + fn is_dirty(&self, _: &AppContext) -> bool { + self.is_dirty + } + + fn has_conflict(&self, _: &AppContext) -> bool { + self.has_conflict + } + + fn can_save(&self, cx: &AppContext) -> bool { + !self.project_items.is_empty() + && self + .project_items + .iter() + .all(|item| item.read(cx).entry_id.is_some()) + } + + fn save( + &mut self, + _: ModelHandle, + _: &mut ViewContext, + ) -> Task> { + self.save_count += 1; + self.is_dirty = false; + Task::ready(Ok(())) + } + + fn save_as( + &mut self, + _: ModelHandle, + _: std::path::PathBuf, + _: &mut ViewContext, + ) -> Task> { + self.save_as_count += 1; + self.is_dirty = false; + Task::ready(Ok(())) + } + + fn reload( + &mut self, + _: ModelHandle, + _: &mut ViewContext, + ) -> Task> { + self.reload_count += 1; + self.is_dirty = false; + Task::ready(Ok(())) + } + + fn to_item_events(_: &Self::Event) -> SmallVec<[ItemEvent; 2]> { + [ItemEvent::UpdateTab, ItemEvent::Edit].into() + } + + fn serialized_item_kind() -> Option<&'static str> { + Some("TestItem") + } + + fn deserialize( + _project: ModelHandle, + _workspace: WeakViewHandle, + workspace_id: WorkspaceId, + _item_id: ItemId, + cx: &mut ViewContext, + ) -> Task>> { + let view = cx.add_view(|_cx| Self::new_deserialized(workspace_id)); + Task::Ready(Some(anyhow::Ok(view))) + } + } +} diff --git a/crates/workspace2/src/notifications.rs b/crates/workspace2/src/notifications.rs new file mode 100644 index 0000000000..7846c7470a --- /dev/null +++ b/crates/workspace2/src/notifications.rs @@ -0,0 +1,400 @@ +use crate::{Toast, Workspace}; +use collections::HashMap; +use gpui2::{AnyViewHandle, AppContext, Entity, View, ViewContext, ViewHandle}; +use std::{any::TypeId, ops::DerefMut}; + +pub fn init(cx: &mut AppContext) { + cx.set_global(NotificationTracker::new()); + simple_message_notification::init(cx); +} + +pub trait Notification: View { + fn should_dismiss_notification_on_event(&self, event: &::Event) -> bool; +} + +pub trait NotificationHandle { + fn id(&self) -> usize; + fn as_any(&self) -> &AnyViewHandle; +} + +impl NotificationHandle for ViewHandle { + fn id(&self) -> usize { + self.id() + } + + fn as_any(&self) -> &AnyViewHandle { + self + } +} + +impl From<&dyn NotificationHandle> for AnyViewHandle { + fn from(val: &dyn NotificationHandle) -> Self { + val.as_any().clone() + } +} + +pub(crate) struct NotificationTracker { + notifications_sent: HashMap>, +} + +impl std::ops::Deref for NotificationTracker { + type Target = HashMap>; + + fn deref(&self) -> &Self::Target { + &self.notifications_sent + } +} + +impl DerefMut for NotificationTracker { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.notifications_sent + } +} + +impl NotificationTracker { + fn new() -> Self { + Self { + notifications_sent: Default::default(), + } + } +} + +impl Workspace { + pub fn has_shown_notification_once( + &self, + id: usize, + cx: &ViewContext, + ) -> bool { + cx.global::() + .get(&TypeId::of::()) + .map(|ids| ids.contains(&id)) + .unwrap_or(false) + } + + pub fn show_notification_once( + &mut self, + id: usize, + cx: &mut ViewContext, + build_notification: impl FnOnce(&mut ViewContext) -> ViewHandle, + ) { + if !self.has_shown_notification_once::(id, cx) { + cx.update_global::(|tracker, _| { + let entry = tracker.entry(TypeId::of::()).or_default(); + entry.push(id); + }); + + self.show_notification::(id, cx, build_notification) + } + } + + pub fn show_notification( + &mut self, + id: usize, + cx: &mut ViewContext, + build_notification: impl FnOnce(&mut ViewContext) -> ViewHandle, + ) { + let type_id = TypeId::of::(); + if self + .notifications + .iter() + .all(|(existing_type_id, existing_id, _)| { + (*existing_type_id, *existing_id) != (type_id, id) + }) + { + let notification = build_notification(cx); + cx.subscribe(¬ification, move |this, handle, event, cx| { + if handle.read(cx).should_dismiss_notification_on_event(event) { + this.dismiss_notification_internal(type_id, id, cx); + } + }) + .detach(); + self.notifications + .push((type_id, id, Box::new(notification))); + cx.notify(); + } + } + + pub fn dismiss_notification(&mut self, id: usize, cx: &mut ViewContext) { + let type_id = TypeId::of::(); + + self.dismiss_notification_internal(type_id, id, cx) + } + + pub fn show_toast(&mut self, toast: Toast, cx: &mut ViewContext) { + self.dismiss_notification::(toast.id, cx); + self.show_notification(toast.id, cx, |cx| { + cx.add_view(|_cx| match toast.on_click.as_ref() { + Some((click_msg, on_click)) => { + let on_click = on_click.clone(); + simple_message_notification::MessageNotification::new(toast.msg.clone()) + .with_click_message(click_msg.clone()) + .on_click(move |cx| on_click(cx)) + } + None => simple_message_notification::MessageNotification::new(toast.msg.clone()), + }) + }) + } + + pub fn dismiss_toast(&mut self, id: usize, cx: &mut ViewContext) { + self.dismiss_notification::(id, cx); + } + + fn dismiss_notification_internal( + &mut self, + type_id: TypeId, + id: usize, + cx: &mut ViewContext, + ) { + self.notifications + .retain(|(existing_type_id, existing_id, _)| { + if (*existing_type_id, *existing_id) == (type_id, id) { + cx.notify(); + false + } else { + true + } + }); + } +} + +pub mod simple_message_notification { + use super::Notification; + use crate::Workspace; + use gpui2::{ + actions, + elements::{Flex, MouseEventHandler, Padding, ParentElement, Svg, Text}, + fonts::TextStyle, + impl_actions, + platform::{CursorStyle, MouseButton}, + AnyElement, AppContext, Element, Entity, View, ViewContext, + }; + use menu::Cancel; + use serde::Deserialize; + use std::{borrow::Cow, sync::Arc}; + + actions!(message_notifications, [CancelMessageNotification]); + + #[derive(Clone, Default, Deserialize, PartialEq)] + pub struct OsOpen(pub Cow<'static, str>); + + impl OsOpen { + pub fn new>>(url: I) -> Self { + OsOpen(url.into()) + } + } + + impl_actions!(message_notifications, [OsOpen]); + + pub fn init(cx: &mut AppContext) { + cx.add_action(MessageNotification::dismiss); + cx.add_action( + |_workspace: &mut Workspace, open_action: &OsOpen, cx: &mut ViewContext| { + cx.platform().open_url(open_action.0.as_ref()); + }, + ) + } + + enum NotificationMessage { + Text(Cow<'static, str>), + Element(fn(TextStyle, &AppContext) -> AnyElement), + } + + pub struct MessageNotification { + message: NotificationMessage, + on_click: Option)>>, + click_message: Option>, + } + + pub enum MessageNotificationEvent { + Dismiss, + } + + impl Entity for MessageNotification { + type Event = MessageNotificationEvent; + } + + impl MessageNotification { + pub fn new(message: S) -> MessageNotification + where + S: Into>, + { + Self { + message: NotificationMessage::Text(message.into()), + on_click: None, + click_message: None, + } + } + + pub fn new_element( + message: fn(TextStyle, &AppContext) -> AnyElement, + ) -> MessageNotification { + Self { + message: NotificationMessage::Element(message), + on_click: None, + click_message: None, + } + } + + pub fn with_click_message(mut self, message: S) -> Self + where + S: Into>, + { + self.click_message = Some(message.into()); + self + } + + pub fn on_click(mut self, on_click: F) -> Self + where + F: 'static + Fn(&mut ViewContext), + { + self.on_click = Some(Arc::new(on_click)); + self + } + + pub fn dismiss(&mut self, _: &CancelMessageNotification, cx: &mut ViewContext) { + cx.emit(MessageNotificationEvent::Dismiss); + } + } + + impl View for MessageNotification { + fn ui_name() -> &'static str { + "MessageNotification" + } + + fn render(&mut self, cx: &mut gpui2::ViewContext) -> gpui::AnyElement { + let theme = theme2::current(cx).clone(); + let theme = &theme.simple_message_notification; + + enum MessageNotificationTag {} + + let click_message = self.click_message.clone(); + let message = match &self.message { + NotificationMessage::Text(text) => { + Text::new(text.to_owned(), theme.message.text.clone()).into_any() + } + NotificationMessage::Element(e) => e(theme.message.text.clone(), cx), + }; + let on_click = self.on_click.clone(); + let has_click_action = on_click.is_some(); + + Flex::column() + .with_child( + Flex::row() + .with_child( + message + .contained() + .with_style(theme.message.container) + .aligned() + .top() + .left() + .flex(1., true), + ) + .with_child( + MouseEventHandler::new::(0, cx, |state, _| { + let style = theme.dismiss_button.style_for(state); + Svg::new("icons/x.svg") + .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) + }) + .with_padding(Padding::uniform(5.)) + .on_click(MouseButton::Left, move |_, this, cx| { + this.dismiss(&Default::default(), cx); + }) + .with_cursor_style(CursorStyle::PointingHand) + .aligned() + .constrained() + .with_height(cx.font_cache().line_height(theme.message.text.font_size)) + .aligned() + .top() + .flex_float(), + ), + ) + .with_children({ + click_message + .map(|click_message| { + MouseEventHandler::new::( + 0, + cx, + |state, _| { + let style = theme.action_message.style_for(state); + + Flex::row() + .with_child( + Text::new(click_message, style.text.clone()) + .contained() + .with_style(style.container), + ) + .contained() + }, + ) + .on_click(MouseButton::Left, move |_, this, cx| { + if let Some(on_click) = on_click.as_ref() { + on_click(cx); + this.dismiss(&Default::default(), cx); + } + }) + // Since we're not using a proper overlay, we have to capture these extra events + .on_down(MouseButton::Left, |_, _, _| {}) + .on_up(MouseButton::Left, |_, _, _| {}) + .with_cursor_style(if has_click_action { + CursorStyle::PointingHand + } else { + CursorStyle::Arrow + }) + }) + .into_iter() + }) + .into_any() + } + } + + impl Notification for MessageNotification { + fn should_dismiss_notification_on_event(&self, event: &::Event) -> bool { + match event { + MessageNotificationEvent::Dismiss => true, + } + } + } +} + +pub trait NotifyResultExt { + type Ok; + + fn notify_err( + self, + workspace: &mut Workspace, + cx: &mut ViewContext, + ) -> Option; +} + +impl NotifyResultExt for Result +where + E: std::fmt::Debug, +{ + type Ok = T; + + fn notify_err(self, workspace: &mut Workspace, cx: &mut ViewContext) -> Option { + match self { + Ok(value) => Some(value), + Err(err) => { + workspace.show_notification(0, cx, |cx| { + cx.add_view(|_cx| { + simple_message_notification::MessageNotification::new(format!( + "Error: {:?}", + err, + )) + }) + }); + + None + } + } + } +} diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs new file mode 100644 index 0000000000..e885408221 --- /dev/null +++ b/crates/workspace2/src/pane.rs @@ -0,0 +1,2742 @@ +mod dragged_item_receiver; + +use super::{ItemHandle, SplitDirection}; +pub use crate::toolbar::Toolbar; +use crate::{ + item::{ItemSettings, WeakItemHandle}, + notify_of_new_dock, AutosaveSetting, Item, NewCenterTerminal, NewFile, NewSearch, ToggleZoom, + Workspace, WorkspaceSettings, +}; +use anyhow::Result; +use collections::{HashMap, HashSet, VecDeque}; +// use context_menu::{ContextMenu, ContextMenuItem}; + +use dragged_item_receiver::dragged_item_receiver; +use fs2::repository::GitFileStatus; +use futures::StreamExt; +use gpui2::{ + actions, + elements::*, + geometry::{ + rect::RectF, + vector::{vec2f, Vector2F}, + }, + impl_actions, + keymap_matcher::KeymapContext, + platform::{CursorStyle, MouseButton, NavigationDirection, PromptLevel}, + Action, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, EventContext, + ModelHandle, MouseRegion, Quad, Task, View, ViewContext, ViewHandle, WeakViewHandle, + WindowContext, +}; +use project2::{Project, ProjectEntryId, ProjectPath}; +use serde::Deserialize; +use std::{ + any::Any, + cell::RefCell, + cmp, mem, + path::{Path, PathBuf}, + rc::Rc, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, + }, +}; +use theme2::{Theme, ThemeSettings}; +use util::truncate_and_remove_front; + +#[derive(PartialEq, Clone, Copy, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub enum SaveIntent { + /// write all files (even if unchanged) + /// prompt before overwriting on-disk changes + Save, + /// write any files that have local changes + /// prompt before overwriting on-disk changes + SaveAll, + /// always prompt for a new path + SaveAs, + /// prompt "you have unsaved changes" before writing + Close, + /// write all dirty files, don't prompt on conflict + Overwrite, + /// skip all save-related behavior + Skip, +} + +#[derive(Clone, Deserialize, PartialEq)] +pub struct ActivateItem(pub usize); + +#[derive(Clone, PartialEq)] +pub struct CloseItemById { + pub item_id: usize, + pub pane: WeakViewHandle, +} + +#[derive(Clone, PartialEq)] +pub struct CloseItemsToTheLeftById { + pub item_id: usize, + pub pane: WeakViewHandle, +} + +#[derive(Clone, PartialEq)] +pub struct CloseItemsToTheRightById { + pub item_id: usize, + pub pane: WeakViewHandle, +} + +#[derive(Clone, PartialEq, Debug, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct CloseActiveItem { + pub save_intent: Option, +} + +#[derive(Clone, PartialEq, Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CloseAllItems { + pub save_intent: Option, +} + +actions!( + pane, + [ + ActivatePrevItem, + ActivateNextItem, + ActivateLastItem, + CloseInactiveItems, + CloseCleanItems, + CloseItemsToTheLeft, + CloseItemsToTheRight, + GoBack, + GoForward, + ReopenClosedItem, + SplitLeft, + SplitUp, + SplitRight, + SplitDown, + ] +); + +impl_actions!(pane, [ActivateItem, CloseActiveItem, CloseAllItems]); + +const MAX_NAVIGATION_HISTORY_LEN: usize = 1024; + +pub type BackgroundActions = fn() -> &'static [(&'static str, &'static dyn Action)]; + +pub fn init(cx: &mut AppContext) { + cx.add_action(Pane::toggle_zoom); + cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| { + pane.activate_item(action.0, true, true, cx); + }); + cx.add_action(|pane: &mut Pane, _: &ActivateLastItem, cx| { + pane.activate_item(pane.items.len() - 1, true, true, cx); + }); + cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| { + pane.activate_prev_item(true, cx); + }); + cx.add_action(|pane: &mut Pane, _: &ActivateNextItem, cx| { + pane.activate_next_item(true, cx); + }); + cx.add_async_action(Pane::close_active_item); + cx.add_async_action(Pane::close_inactive_items); + cx.add_async_action(Pane::close_clean_items); + cx.add_async_action(Pane::close_items_to_the_left); + cx.add_async_action(Pane::close_items_to_the_right); + cx.add_async_action(Pane::close_all_items); + cx.add_action(|pane: &mut Pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx)); + cx.add_action(|pane: &mut Pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx)); + cx.add_action(|pane: &mut Pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx)); + cx.add_action(|pane: &mut Pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx)); +} + +#[derive(Debug)] +pub enum Event { + AddItem { item: Box }, + ActivateItem { local: bool }, + Remove, + RemoveItem { item_id: usize }, + Split(SplitDirection), + ChangeItemTitle, + Focus, + ZoomIn, + ZoomOut, +} + +pub struct Pane { + items: Vec>, + activation_history: Vec, + zoomed: bool, + active_item_index: usize, + last_focused_view_by_item: HashMap, + autoscroll: bool, + nav_history: NavHistory, + toolbar: ViewHandle, + tab_bar_context_menu: TabBarContextMenu, + tab_context_menu: ViewHandle, + _background_actions: BackgroundActions, + workspace: WeakViewHandle, + project: ModelHandle, + has_focus: bool, + can_drop: Rc, &WindowContext) -> bool>, + can_split: bool, + render_tab_bar_buttons: Rc) -> AnyElement>, +} + +pub struct ItemNavHistory { + history: NavHistory, + item: Rc, +} + +#[derive(Clone)] +pub struct NavHistory(Rc>); + +struct NavHistoryState { + mode: NavigationMode, + backward_stack: VecDeque, + forward_stack: VecDeque, + closed_stack: VecDeque, + paths_by_item: HashMap)>, + pane: WeakViewHandle, + next_timestamp: Arc, +} + +#[derive(Copy, Clone)] +pub enum NavigationMode { + Normal, + GoingBack, + GoingForward, + ClosingItem, + ReopeningClosedItem, + Disabled, +} + +impl Default for NavigationMode { + fn default() -> Self { + Self::Normal + } +} + +pub struct NavigationEntry { + pub item: Rc, + pub data: Option>, + pub timestamp: usize, +} + +pub struct DraggedItem { + pub handle: Box, + pub pane: WeakViewHandle, +} + +pub enum ReorderBehavior { + None, + MoveAfterActive, + MoveToIndex(usize), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum TabBarContextMenuKind { + New, + Split, +} + +struct TabBarContextMenu { + kind: TabBarContextMenuKind, + handle: ViewHandle, +} + +impl TabBarContextMenu { + fn handle_if_kind(&self, kind: TabBarContextMenuKind) -> Option> { + if self.kind == kind { + return Some(self.handle.clone()); + } + None + } +} + +#[allow(clippy::too_many_arguments)] +fn nav_button)>( + svg_path: &'static str, + style: theme2::Interactive, + nav_button_height: f32, + tooltip_style: TooltipStyle, + enabled: bool, + on_click: F, + tooltip_action: A, + action_name: &str, + cx: &mut ViewContext, +) -> AnyElement { + MouseEventHandler::new::(0, cx, |state, _| { + let style = if enabled { + style.style_for(state) + } 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(nav_button_height) + .aligned() + .top() + }) + .with_cursor_style(if enabled { + CursorStyle::PointingHand + } else { + CursorStyle::default() + }) + .on_click(MouseButton::Left, move |_, toolbar, cx| { + on_click(toolbar, cx) + }) + .with_tooltip::( + 0, + action_name.to_string(), + Some(Box::new(tooltip_action)), + tooltip_style, + cx, + ) + .contained() + .into_any_named("nav button") +} + +impl Pane { + pub fn new( + workspace: WeakViewHandle, + project: ModelHandle, + background_actions: BackgroundActions, + next_timestamp: Arc, + cx: &mut ViewContext, + ) -> Self { + let pane_view_id = cx.view_id(); + let handle = cx.weak_handle(); + let context_menu = cx.add_view(|cx| ContextMenu::new(pane_view_id, cx)); + context_menu.update(cx, |menu, _| { + menu.set_position_mode(OverlayPositionMode::Local) + }); + + Self { + items: Vec::new(), + activation_history: Vec::new(), + zoomed: false, + active_item_index: 0, + last_focused_view_by_item: Default::default(), + autoscroll: false, + nav_history: NavHistory(Rc::new(RefCell::new(NavHistoryState { + mode: NavigationMode::Normal, + backward_stack: Default::default(), + forward_stack: Default::default(), + closed_stack: Default::default(), + paths_by_item: Default::default(), + pane: handle.clone(), + next_timestamp, + }))), + toolbar: cx.add_view(|_| Toolbar::new()), + tab_bar_context_menu: TabBarContextMenu { + kind: TabBarContextMenuKind::New, + handle: context_menu, + }, + tab_context_menu: cx.add_view(|cx| ContextMenu::new(pane_view_id, cx)), + _background_actions: background_actions, + workspace, + project, + has_focus: false, + can_drop: Rc::new(|_, _| true), + can_split: true, + render_tab_bar_buttons: Rc::new(move |pane, cx| { + Flex::row() + // New menu + .with_child(Self::render_tab_bar_button( + 0, + "icons/plus.svg", + false, + Some(("New...".into(), None)), + cx, + |pane, cx| pane.deploy_new_menu(cx), + |pane, cx| { + pane.tab_bar_context_menu + .handle + .update(cx, |menu, _| menu.delay_cancel()) + }, + pane.tab_bar_context_menu + .handle_if_kind(TabBarContextMenuKind::New), + )) + .with_child(Self::render_tab_bar_button( + 1, + "icons/split.svg", + false, + Some(("Split Pane".into(), None)), + cx, + |pane, cx| pane.deploy_split_menu(cx), + |pane, cx| { + pane.tab_bar_context_menu + .handle + .update(cx, |menu, _| menu.delay_cancel()) + }, + pane.tab_bar_context_menu + .handle_if_kind(TabBarContextMenuKind::Split), + )) + .with_child({ + let icon_path; + let tooltip_label; + if pane.is_zoomed() { + icon_path = "icons/minimize.svg"; + tooltip_label = "Zoom In"; + } else { + icon_path = "icons/maximize.svg"; + tooltip_label = "Zoom In"; + } + + Pane::render_tab_bar_button( + 2, + icon_path, + pane.is_zoomed(), + Some((tooltip_label, Some(Box::new(ToggleZoom)))), + cx, + move |pane, cx| pane.toggle_zoom(&Default::default(), cx), + move |_, _| {}, + None, + ) + }) + .into_any() + }), + } + } + + pub(crate) fn workspace(&self) -> &WeakViewHandle { + &self.workspace + } + + pub fn has_focus(&self) -> bool { + self.has_focus + } + + pub fn active_item_index(&self) -> usize { + self.active_item_index + } + + pub fn on_can_drop(&mut self, can_drop: F) + where + F: 'static + Fn(&DragAndDrop, &WindowContext) -> bool, + { + self.can_drop = Rc::new(can_drop); + } + + pub fn set_can_split(&mut self, can_split: bool, cx: &mut ViewContext) { + self.can_split = can_split; + cx.notify(); + } + + pub fn set_can_navigate(&mut self, can_navigate: bool, cx: &mut ViewContext) { + self.toolbar.update(cx, |toolbar, cx| { + toolbar.set_can_navigate(can_navigate, cx); + }); + cx.notify(); + } + + pub fn set_render_tab_bar_buttons(&mut self, cx: &mut ViewContext, render: F) + where + F: 'static + Fn(&mut Pane, &mut ViewContext) -> AnyElement, + { + self.render_tab_bar_buttons = Rc::new(render); + cx.notify(); + } + + pub fn nav_history_for_item(&self, item: &ViewHandle) -> ItemNavHistory { + ItemNavHistory { + history: self.nav_history.clone(), + item: Rc::new(item.downgrade()), + } + } + + pub fn nav_history(&self) -> &NavHistory { + &self.nav_history + } + + pub fn nav_history_mut(&mut self) -> &mut NavHistory { + &mut self.nav_history + } + + pub fn disable_history(&mut self) { + self.nav_history.disable(); + } + + pub fn enable_history(&mut self) { + self.nav_history.enable(); + } + + pub fn can_navigate_backward(&self) -> bool { + !self.nav_history.0.borrow().backward_stack.is_empty() + } + + pub fn can_navigate_forward(&self) -> bool { + !self.nav_history.0.borrow().forward_stack.is_empty() + } + + fn history_updated(&mut self, cx: &mut ViewContext) { + self.toolbar.update(cx, |_, cx| cx.notify()); + } + + pub(crate) fn open_item( + &mut self, + project_entry_id: ProjectEntryId, + focus_item: bool, + cx: &mut ViewContext, + build_item: impl FnOnce(&mut ViewContext) -> Box, + ) -> Box { + let mut existing_item = None; + for (index, item) in self.items.iter().enumerate() { + if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [project_entry_id] + { + let item = item.boxed_clone(); + existing_item = Some((index, item)); + break; + } + } + + if let Some((index, existing_item)) = existing_item { + self.activate_item(index, focus_item, focus_item, cx); + existing_item + } else { + let new_item = build_item(cx); + self.add_item(new_item.clone(), true, focus_item, None, cx); + new_item + } + } + + pub fn add_item( + &mut self, + item: Box, + activate_pane: bool, + focus_item: bool, + destination_index: Option, + cx: &mut ViewContext, + ) { + if item.is_singleton(cx) { + if let Some(&entry_id) = item.project_entry_ids(cx).get(0) { + let project = self.project.read(cx); + if let Some(project_path) = project.path_for_entry(entry_id, cx) { + let abs_path = project.absolute_path(&project_path, cx); + self.nav_history + .0 + .borrow_mut() + .paths_by_item + .insert(item.id(), (project_path, abs_path)); + } + } + } + // If no destination index is specified, add or move the item after the active item. + let mut insertion_index = { + cmp::min( + if let Some(destination_index) = destination_index { + destination_index + } else { + self.active_item_index + 1 + }, + self.items.len(), + ) + }; + + // Does the item already exist? + let project_entry_id = if item.is_singleton(cx) { + item.project_entry_ids(cx).get(0).copied() + } else { + None + }; + + let existing_item_index = self.items.iter().position(|existing_item| { + if existing_item.id() == item.id() { + true + } else if existing_item.is_singleton(cx) { + existing_item + .project_entry_ids(cx) + .get(0) + .map_or(false, |existing_entry_id| { + Some(existing_entry_id) == project_entry_id.as_ref() + }) + } else { + false + } + }); + + if let Some(existing_item_index) = existing_item_index { + // If the item already exists, move it to the desired destination and activate it + + if existing_item_index != insertion_index { + let existing_item_is_active = existing_item_index == self.active_item_index; + + // If the caller didn't specify a destination and the added item is already + // the active one, don't move it + if existing_item_is_active && destination_index.is_none() { + insertion_index = existing_item_index; + } else { + self.items.remove(existing_item_index); + if existing_item_index < self.active_item_index { + self.active_item_index -= 1; + } + insertion_index = insertion_index.min(self.items.len()); + + self.items.insert(insertion_index, item.clone()); + + if existing_item_is_active { + self.active_item_index = insertion_index; + } else if insertion_index <= self.active_item_index { + self.active_item_index += 1; + } + } + + cx.notify(); + } + + self.activate_item(insertion_index, activate_pane, focus_item, cx); + } else { + self.items.insert(insertion_index, item.clone()); + if insertion_index <= self.active_item_index { + self.active_item_index += 1; + } + + self.activate_item(insertion_index, activate_pane, focus_item, cx); + cx.notify(); + } + + cx.emit(Event::AddItem { item }); + } + + pub fn items_len(&self) -> usize { + self.items.len() + } + + pub fn items(&self) -> impl Iterator> + DoubleEndedIterator { + self.items.iter() + } + + pub fn items_of_type(&self) -> impl '_ + Iterator> { + self.items + .iter() + .filter_map(|item| item.as_any().clone().downcast()) + } + + pub fn active_item(&self) -> Option> { + self.items.get(self.active_item_index).cloned() + } + + pub fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option { + self.items + .get(self.active_item_index)? + .pixel_position_of_cursor(cx) + } + + pub fn item_for_entry( + &self, + entry_id: ProjectEntryId, + cx: &AppContext, + ) -> Option> { + self.items.iter().find_map(|item| { + if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] { + Some(item.boxed_clone()) + } else { + None + } + }) + } + + pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option { + self.items.iter().position(|i| i.id() == item.id()) + } + + pub fn toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext) { + // Potentially warn the user of the new keybinding + let workspace_handle = self.workspace().clone(); + cx.spawn(|_, mut cx| async move { notify_of_new_dock(&workspace_handle, &mut cx) }) + .detach(); + + if self.zoomed { + cx.emit(Event::ZoomOut); + } else if !self.items.is_empty() { + if !self.has_focus { + cx.focus_self(); + } + cx.emit(Event::ZoomIn); + } + } + + pub fn activate_item( + &mut self, + index: usize, + activate_pane: bool, + focus_item: bool, + cx: &mut ViewContext, + ) { + use NavigationMode::{GoingBack, GoingForward}; + + if index < self.items.len() { + let prev_active_item_ix = mem::replace(&mut self.active_item_index, index); + if prev_active_item_ix != self.active_item_index + || matches!(self.nav_history.mode(), GoingBack | GoingForward) + { + if let Some(prev_item) = self.items.get(prev_active_item_ix) { + prev_item.deactivated(cx); + } + + cx.emit(Event::ActivateItem { + local: activate_pane, + }); + } + + if let Some(newly_active_item) = self.items.get(index) { + self.activation_history + .retain(|&previously_active_item_id| { + previously_active_item_id != newly_active_item.id() + }); + self.activation_history.push(newly_active_item.id()); + } + + self.update_toolbar(cx); + + if focus_item { + self.focus_active_item(cx); + } + + self.autoscroll = true; + cx.notify(); + } + } + + pub fn activate_prev_item(&mut self, activate_pane: bool, cx: &mut ViewContext) { + let mut index = self.active_item_index; + if index > 0 { + index -= 1; + } else if !self.items.is_empty() { + index = self.items.len() - 1; + } + self.activate_item(index, activate_pane, activate_pane, cx); + } + + pub fn activate_next_item(&mut self, activate_pane: bool, cx: &mut ViewContext) { + let mut index = self.active_item_index; + if index + 1 < self.items.len() { + index += 1; + } else { + index = 0; + } + self.activate_item(index, activate_pane, activate_pane, cx); + } + + pub fn close_active_item( + &mut self, + action: &CloseActiveItem, + cx: &mut ViewContext, + ) -> Option>> { + if self.items.is_empty() { + return None; + } + let active_item_id = self.items[self.active_item_index].id(); + Some(self.close_item_by_id( + active_item_id, + action.save_intent.unwrap_or(SaveIntent::Close), + cx, + )) + } + + pub fn close_item_by_id( + &mut self, + item_id_to_close: usize, + save_intent: SaveIntent, + cx: &mut ViewContext, + ) -> Task> { + self.close_items(cx, save_intent, move |view_id| view_id == item_id_to_close) + } + + pub fn close_inactive_items( + &mut self, + _: &CloseInactiveItems, + cx: &mut ViewContext, + ) -> Option>> { + if self.items.is_empty() { + return None; + } + + let active_item_id = self.items[self.active_item_index].id(); + Some(self.close_items(cx, SaveIntent::Close, move |item_id| { + item_id != active_item_id + })) + } + + pub fn close_clean_items( + &mut self, + _: &CloseCleanItems, + cx: &mut ViewContext, + ) -> Option>> { + let item_ids: Vec<_> = self + .items() + .filter(|item| !item.is_dirty(cx)) + .map(|item| item.id()) + .collect(); + Some(self.close_items(cx, SaveIntent::Close, move |item_id| { + item_ids.contains(&item_id) + })) + } + + pub fn close_items_to_the_left( + &mut self, + _: &CloseItemsToTheLeft, + cx: &mut ViewContext, + ) -> Option>> { + if self.items.is_empty() { + return None; + } + let active_item_id = self.items[self.active_item_index].id(); + Some(self.close_items_to_the_left_by_id(active_item_id, cx)) + } + + pub fn close_items_to_the_left_by_id( + &mut self, + item_id: usize, + cx: &mut ViewContext, + ) -> Task> { + let item_ids: Vec<_> = self + .items() + .take_while(|item| item.id() != item_id) + .map(|item| item.id()) + .collect(); + self.close_items(cx, SaveIntent::Close, move |item_id| { + item_ids.contains(&item_id) + }) + } + + pub fn close_items_to_the_right( + &mut self, + _: &CloseItemsToTheRight, + cx: &mut ViewContext, + ) -> Option>> { + if self.items.is_empty() { + return None; + } + let active_item_id = self.items[self.active_item_index].id(); + Some(self.close_items_to_the_right_by_id(active_item_id, cx)) + } + + pub fn close_items_to_the_right_by_id( + &mut self, + item_id: usize, + cx: &mut ViewContext, + ) -> Task> { + let item_ids: Vec<_> = self + .items() + .rev() + .take_while(|item| item.id() != item_id) + .map(|item| item.id()) + .collect(); + self.close_items(cx, SaveIntent::Close, move |item_id| { + item_ids.contains(&item_id) + }) + } + + pub fn close_all_items( + &mut self, + action: &CloseAllItems, + cx: &mut ViewContext, + ) -> Option>> { + if self.items.is_empty() { + return None; + } + + Some( + self.close_items(cx, action.save_intent.unwrap_or(SaveIntent::Close), |_| { + true + }), + ) + } + + pub(super) fn file_names_for_prompt( + items: &mut dyn Iterator>, + all_dirty_items: usize, + cx: &AppContext, + ) -> String { + /// Quantity of item paths displayed in prompt prior to cutoff.. + const FILE_NAMES_CUTOFF_POINT: usize = 10; + let mut file_names: Vec<_> = items + .filter_map(|item| { + item.project_path(cx).and_then(|project_path| { + project_path + .path + .file_name() + .and_then(|name| name.to_str().map(ToOwned::to_owned)) + }) + }) + .take(FILE_NAMES_CUTOFF_POINT) + .collect(); + let should_display_followup_text = + all_dirty_items > FILE_NAMES_CUTOFF_POINT || file_names.len() != all_dirty_items; + if should_display_followup_text { + let not_shown_files = all_dirty_items - file_names.len(); + if not_shown_files == 1 { + file_names.push(".. 1 file not shown".into()); + } else { + file_names.push(format!(".. {} files not shown", not_shown_files).into()); + } + } + let file_names = file_names.join("\n"); + format!( + "Do you want to save changes to the following {} files?\n{file_names}", + all_dirty_items + ) + } + + pub fn close_items( + &mut self, + cx: &mut ViewContext, + mut save_intent: SaveIntent, + should_close: impl 'static + Fn(usize) -> bool, + ) -> Task> { + // Find the items to close. + let mut items_to_close = Vec::new(); + let mut dirty_items = Vec::new(); + for item in &self.items { + if should_close(item.id()) { + items_to_close.push(item.boxed_clone()); + if item.is_dirty(cx) { + dirty_items.push(item.boxed_clone()); + } + } + } + + // If a buffer is open both in a singleton editor and in a multibuffer, make sure + // to focus the singleton buffer when prompting to save that buffer, as opposed + // to focusing the multibuffer, because this gives the user a more clear idea + // of what content they would be saving. + items_to_close.sort_by_key(|item| !item.is_singleton(cx)); + + let workspace = self.workspace.clone(); + cx.spawn(|pane, mut cx| async move { + if save_intent == SaveIntent::Close && dirty_items.len() > 1 { + let mut answer = pane.update(&mut cx, |_, cx| { + let prompt = + Self::file_names_for_prompt(&mut dirty_items.iter(), dirty_items.len(), cx); + cx.prompt( + PromptLevel::Warning, + &prompt, + &["Save all", "Discard all", "Cancel"], + ) + })?; + match answer.next().await { + Some(0) => save_intent = SaveIntent::SaveAll, + Some(1) => save_intent = SaveIntent::Skip, + _ => {} + } + } + let mut saved_project_items_ids = HashSet::default(); + for item in items_to_close.clone() { + // Find the item's current index and its set of project item models. Avoid + // storing these in advance, in case they have changed since this task + // was started. + let (item_ix, mut project_item_ids) = pane.read_with(&cx, |pane, cx| { + (pane.index_for_item(&*item), item.project_item_model_ids(cx)) + })?; + let item_ix = if let Some(ix) = item_ix { + ix + } else { + continue; + }; + + // Check if this view has any project items that are not open anywhere else + // in the workspace, AND that the user has not already been prompted to save. + // If there are any such project entries, prompt the user to save this item. + let project = workspace.read_with(&cx, |workspace, cx| { + for item in workspace.items(cx) { + if !items_to_close + .iter() + .any(|item_to_close| item_to_close.id() == item.id()) + { + let other_project_item_ids = item.project_item_model_ids(cx); + project_item_ids.retain(|id| !other_project_item_ids.contains(id)); + } + } + workspace.project().clone() + })?; + let should_save = project_item_ids + .iter() + .any(|id| saved_project_items_ids.insert(*id)); + + if should_save + && !Self::save_item( + project.clone(), + &pane, + item_ix, + &*item, + save_intent, + &mut cx, + ) + .await? + { + break; + } + + // Remove the item from the pane. + pane.update(&mut cx, |pane, cx| { + if let Some(item_ix) = pane.items.iter().position(|i| i.id() == item.id()) { + pane.remove_item(item_ix, false, cx); + } + })?; + } + + pane.update(&mut cx, |_, cx| cx.notify())?; + Ok(()) + }) + } + + pub fn remove_item( + &mut self, + item_index: usize, + activate_pane: bool, + cx: &mut ViewContext, + ) { + self.activation_history + .retain(|&history_entry| history_entry != self.items[item_index].id()); + + if item_index == self.active_item_index { + let index_to_activate = self + .activation_history + .pop() + .and_then(|last_activated_item| { + self.items.iter().enumerate().find_map(|(index, item)| { + (item.id() == last_activated_item).then_some(index) + }) + }) + // We didn't have a valid activation history entry, so fallback + // to activating the item to the left + .unwrap_or_else(|| item_index.min(self.items.len()).saturating_sub(1)); + + let should_activate = activate_pane || self.has_focus; + self.activate_item(index_to_activate, should_activate, should_activate, cx); + } + + let item = self.items.remove(item_index); + + cx.emit(Event::RemoveItem { item_id: item.id() }); + if self.items.is_empty() { + item.deactivated(cx); + self.update_toolbar(cx); + cx.emit(Event::Remove); + } + + if item_index < self.active_item_index { + self.active_item_index -= 1; + } + + self.nav_history.set_mode(NavigationMode::ClosingItem); + item.deactivated(cx); + self.nav_history.set_mode(NavigationMode::Normal); + + if let Some(path) = item.project_path(cx) { + let abs_path = self + .nav_history + .0 + .borrow() + .paths_by_item + .get(&item.id()) + .and_then(|(_, abs_path)| abs_path.clone()); + + self.nav_history + .0 + .borrow_mut() + .paths_by_item + .insert(item.id(), (path, abs_path)); + } else { + self.nav_history + .0 + .borrow_mut() + .paths_by_item + .remove(&item.id()); + } + + if self.items.is_empty() && self.zoomed { + cx.emit(Event::ZoomOut); + } + + cx.notify(); + } + + pub async fn save_item( + project: ModelHandle, + pane: &WeakViewHandle, + item_ix: usize, + item: &dyn ItemHandle, + save_intent: SaveIntent, + cx: &mut AsyncAppContext, + ) -> Result { + const CONFLICT_MESSAGE: &str = + "This file has changed on disk since you started editing it. Do you want to overwrite it?"; + + if save_intent == SaveIntent::Skip { + return Ok(true); + } + + let (mut has_conflict, mut is_dirty, mut can_save, can_save_as) = cx.read(|cx| { + ( + item.has_conflict(cx), + item.is_dirty(cx), + item.can_save(cx), + item.is_singleton(cx), + ) + }); + + // when saving a single buffer, we ignore whether or not it's dirty. + if save_intent == SaveIntent::Save { + is_dirty = true; + } + + if save_intent == SaveIntent::SaveAs { + is_dirty = true; + has_conflict = false; + can_save = false; + } + + if save_intent == SaveIntent::Overwrite { + has_conflict = false; + } + + if has_conflict && can_save { + let mut answer = pane.update(cx, |pane, cx| { + pane.activate_item(item_ix, true, true, cx); + cx.prompt( + PromptLevel::Warning, + CONFLICT_MESSAGE, + &["Overwrite", "Discard", "Cancel"], + ) + })?; + match answer.next().await { + Some(0) => pane.update(cx, |_, cx| item.save(project, cx))?.await?, + Some(1) => pane.update(cx, |_, cx| item.reload(project, cx))?.await?, + _ => return Ok(false), + } + } else if is_dirty && (can_save || can_save_as) { + if save_intent == SaveIntent::Close { + let will_autosave = cx.read(|cx| { + matches!( + settings::get::(cx).autosave, + AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange + ) && Self::can_autosave_item(&*item, cx) + }); + if !will_autosave { + let mut answer = pane.update(cx, |pane, cx| { + pane.activate_item(item_ix, true, true, cx); + let prompt = dirty_message_for(item.project_path(cx)); + cx.prompt( + PromptLevel::Warning, + &prompt, + &["Save", "Don't Save", "Cancel"], + ) + })?; + match answer.next().await { + Some(0) => {} + Some(1) => return Ok(true), // Don't save his file + _ => return Ok(false), // Cancel + } + } + } + + if can_save { + pane.update(cx, |_, cx| item.save(project, cx))?.await?; + } else if can_save_as { + let start_abs_path = project + .read_with(cx, |project, cx| { + let worktree = project.visible_worktrees(cx).next()?; + Some(worktree.read(cx).as_local()?.abs_path().to_path_buf()) + }) + .unwrap_or_else(|| Path::new("").into()); + + let mut abs_path = cx.update(|cx| cx.prompt_for_new_path(&start_abs_path)); + if let Some(abs_path) = abs_path.next().await.flatten() { + pane.update(cx, |_, cx| item.save_as(project, abs_path, cx))? + .await?; + } else { + return Ok(false); + } + } + } + Ok(true) + } + + fn can_autosave_item(item: &dyn ItemHandle, cx: &AppContext) -> bool { + let is_deleted = item.project_entry_ids(cx).is_empty(); + item.is_dirty(cx) && !item.has_conflict(cx) && item.can_save(cx) && !is_deleted + } + + pub fn autosave_item( + item: &dyn ItemHandle, + project: ModelHandle, + cx: &mut WindowContext, + ) -> Task> { + if Self::can_autosave_item(item, cx) { + item.save(project, cx) + } else { + Task::ready(Ok(())) + } + } + + pub fn focus_active_item(&mut self, cx: &mut ViewContext) { + if let Some(active_item) = self.active_item() { + cx.focus(active_item.as_any()); + } + } + + pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext) { + cx.emit(Event::Split(direction)); + } + + fn deploy_split_menu(&mut self, cx: &mut ViewContext) { + self.tab_bar_context_menu.handle.update(cx, |menu, cx| { + menu.toggle( + Default::default(), + AnchorCorner::TopRight, + vec![ + ContextMenuItem::action("Split Right", SplitRight), + ContextMenuItem::action("Split Left", SplitLeft), + ContextMenuItem::action("Split Up", SplitUp), + ContextMenuItem::action("Split Down", SplitDown), + ], + cx, + ); + }); + + self.tab_bar_context_menu.kind = TabBarContextMenuKind::Split; + } + + fn deploy_new_menu(&mut self, cx: &mut ViewContext) { + self.tab_bar_context_menu.handle.update(cx, |menu, cx| { + menu.toggle( + Default::default(), + AnchorCorner::TopRight, + vec![ + ContextMenuItem::action("New File", NewFile), + ContextMenuItem::action("New Terminal", NewCenterTerminal), + ContextMenuItem::action("New Search", NewSearch), + ], + cx, + ); + }); + + self.tab_bar_context_menu.kind = TabBarContextMenuKind::New; + } + + fn deploy_tab_context_menu( + &mut self, + position: Vector2F, + target_item_id: usize, + cx: &mut ViewContext, + ) { + let active_item_id = self.items[self.active_item_index].id(); + let is_active_item = target_item_id == active_item_id; + let target_pane = cx.weak_handle(); + + // The `CloseInactiveItems` action should really be called "CloseOthers" and the behaviour should be dynamically based on the tab the action is ran on. Currently, this is a weird action because you can run it on a non-active tab and it will close everything by the actual active tab + + self.tab_context_menu.update(cx, |menu, cx| { + menu.show( + position, + AnchorCorner::TopLeft, + if is_active_item { + vec![ + ContextMenuItem::action( + "Close Active Item", + CloseActiveItem { save_intent: None }, + ), + ContextMenuItem::action("Close Inactive Items", CloseInactiveItems), + ContextMenuItem::action("Close Clean Items", CloseCleanItems), + ContextMenuItem::action("Close Items To The Left", CloseItemsToTheLeft), + ContextMenuItem::action("Close Items To The Right", CloseItemsToTheRight), + ContextMenuItem::action( + "Close All Items", + CloseAllItems { save_intent: None }, + ), + ] + } else { + // In the case of the user right clicking on a non-active tab, for some item-closing commands, we need to provide the id of the tab, for the others, we can reuse the existing command. + vec![ + ContextMenuItem::handler("Close Inactive Item", { + let pane = target_pane.clone(); + move |cx| { + if let Some(pane) = pane.upgrade(cx) { + pane.update(cx, |pane, cx| { + pane.close_item_by_id( + target_item_id, + SaveIntent::Close, + cx, + ) + .detach_and_log_err(cx); + }) + } + } + }), + ContextMenuItem::action("Close Inactive Items", CloseInactiveItems), + ContextMenuItem::action("Close Clean Items", CloseCleanItems), + ContextMenuItem::handler("Close Items To The Left", { + let pane = target_pane.clone(); + move |cx| { + if let Some(pane) = pane.upgrade(cx) { + pane.update(cx, |pane, cx| { + pane.close_items_to_the_left_by_id(target_item_id, cx) + .detach_and_log_err(cx); + }) + } + } + }), + ContextMenuItem::handler("Close Items To The Right", { + let pane = target_pane.clone(); + move |cx| { + if let Some(pane) = pane.upgrade(cx) { + pane.update(cx, |pane, cx| { + pane.close_items_to_the_right_by_id(target_item_id, cx) + .detach_and_log_err(cx); + }) + } + } + }), + ContextMenuItem::action( + "Close All Items", + CloseAllItems { save_intent: None }, + ), + ] + }, + cx, + ); + }); + } + + pub fn toolbar(&self) -> &ViewHandle { + &self.toolbar + } + + pub fn handle_deleted_project_item( + &mut self, + entry_id: ProjectEntryId, + cx: &mut ViewContext, + ) -> Option<()> { + let (item_index_to_delete, item_id) = self.items().enumerate().find_map(|(i, item)| { + if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] { + Some((i, item.id())) + } else { + None + } + })?; + + self.remove_item(item_index_to_delete, false, cx); + self.nav_history.remove_item(item_id); + + Some(()) + } + + fn update_toolbar(&mut self, cx: &mut ViewContext) { + let active_item = self + .items + .get(self.active_item_index) + .map(|item| item.as_ref()); + self.toolbar.update(cx, |toolbar, cx| { + toolbar.set_active_item(active_item, cx); + }); + } + + fn render_tabs(&mut self, cx: &mut ViewContext) -> impl Element { + let theme = theme::current(cx).clone(); + + let pane = cx.handle().downgrade(); + let autoscroll = if mem::take(&mut self.autoscroll) { + Some(self.active_item_index) + } else { + None + }; + + let pane_active = self.has_focus; + + enum Tabs {} + let mut row = Flex::row().scrollable::(1, autoscroll, cx); + for (ix, (item, detail)) in self + .items + .iter() + .cloned() + .zip(self.tab_details(cx)) + .enumerate() + { + let git_status = item + .project_path(cx) + .and_then(|path| self.project.read(cx).entry_for_path(&path, cx)) + .and_then(|entry| entry.git_status()); + + let detail = if detail == 0 { None } else { Some(detail) }; + let tab_active = ix == self.active_item_index; + + row.add_child({ + enum TabDragReceiver {} + let mut receiver = + dragged_item_receiver::(self, ix, ix, true, None, cx, { + let item = item.clone(); + let pane = pane.clone(); + let detail = detail.clone(); + + let theme = theme::current(cx).clone(); + let mut tooltip_theme = theme.tooltip.clone(); + tooltip_theme.max_text_width = None; + let tab_tooltip_text = + item.tab_tooltip_text(cx).map(|text| text.into_owned()); + + let mut tab_style = theme + .workspace + .tab_bar + .tab_style(pane_active, tab_active) + .clone(); + let should_show_status = settings::get::(cx).git_status; + if should_show_status && git_status != None { + tab_style.label.text.color = match git_status.unwrap() { + GitFileStatus::Added => tab_style.git.inserted, + GitFileStatus::Modified => tab_style.git.modified, + GitFileStatus::Conflict => tab_style.git.conflict, + }; + } + + move |mouse_state, cx| { + let hovered = mouse_state.hovered(); + + enum Tab {} + let mouse_event_handler = + MouseEventHandler::new::(ix, cx, |_, cx| { + Self::render_tab( + &item, + pane.clone(), + ix == 0, + detail, + hovered, + &tab_style, + cx, + ) + }) + .on_down(MouseButton::Left, move |_, this, cx| { + this.activate_item(ix, true, true, cx); + }) + .on_click(MouseButton::Middle, { + let item_id = item.id(); + move |_, pane, cx| { + pane.close_item_by_id(item_id, SaveIntent::Close, cx) + .detach_and_log_err(cx); + } + }) + .on_down( + MouseButton::Right, + move |event, pane, cx| { + pane.deploy_tab_context_menu(event.position, item.id(), cx); + }, + ); + + if let Some(tab_tooltip_text) = tab_tooltip_text { + mouse_event_handler + .with_tooltip::( + ix, + tab_tooltip_text, + None, + tooltip_theme, + cx, + ) + .into_any() + } else { + mouse_event_handler.into_any() + } + } + }); + + if !pane_active || !tab_active { + receiver = receiver.with_cursor_style(CursorStyle::PointingHand); + } + + receiver.as_draggable( + DraggedItem { + handle: item, + pane: pane.clone(), + }, + { + let theme = theme::current(cx).clone(); + + let detail = detail.clone(); + move |_, dragged_item: &DraggedItem, cx: &mut ViewContext| { + let tab_style = &theme.workspace.tab_bar.dragged_tab; + Self::render_dragged_tab( + &dragged_item.handle, + dragged_item.pane.clone(), + false, + detail, + false, + &tab_style, + cx, + ) + } + }, + ) + }) + } + + // Use the inactive tab style along with the current pane's active status to decide how to render + // the filler + let filler_index = self.items.len(); + let filler_style = theme.workspace.tab_bar.tab_style(pane_active, false); + enum Filler {} + row.add_child( + dragged_item_receiver::(self, 0, filler_index, true, None, cx, |_, _| { + Empty::new() + .contained() + .with_style(filler_style.container) + .with_border(filler_style.container.border) + }) + .flex(1., true) + .into_any_named("filler"), + ); + + row + } + + fn tab_details(&self, cx: &AppContext) -> Vec { + let mut tab_details = (0..self.items.len()).map(|_| 0).collect::>(); + + let mut tab_descriptions = HashMap::default(); + let mut done = false; + while !done { + done = true; + + // Store item indices by their tab description. + for (ix, (item, detail)) in self.items.iter().zip(&tab_details).enumerate() { + if let Some(description) = item.tab_description(*detail, cx) { + if *detail == 0 + || Some(&description) != item.tab_description(detail - 1, cx).as_ref() + { + tab_descriptions + .entry(description) + .or_insert(Vec::new()) + .push(ix); + } + } + } + + // If two or more items have the same tab description, increase their level + // of detail and try again. + for (_, item_ixs) in tab_descriptions.drain() { + if item_ixs.len() > 1 { + done = false; + for ix in item_ixs { + tab_details[ix] += 1; + } + } + } + } + + tab_details + } + + fn render_tab( + item: &Box, + pane: WeakViewHandle, + first: bool, + detail: Option, + hovered: bool, + tab_style: &theme::Tab, + cx: &mut ViewContext, + ) -> AnyElement { + let title = item.tab_content(detail, &tab_style, cx); + Self::render_tab_with_title(title, item, pane, first, hovered, tab_style, cx) + } + + fn render_dragged_tab( + item: &Box, + pane: WeakViewHandle, + first: bool, + detail: Option, + hovered: bool, + tab_style: &theme::Tab, + cx: &mut ViewContext, + ) -> AnyElement { + let title = item.dragged_tab_content(detail, &tab_style, cx); + Self::render_tab_with_title(title, item, pane, first, hovered, tab_style, cx) + } + + fn render_tab_with_title( + title: AnyElement, + item: &Box, + pane: WeakViewHandle, + first: bool, + hovered: bool, + tab_style: &theme::Tab, + cx: &mut ViewContext, + ) -> AnyElement { + let mut container = tab_style.container.clone(); + if first { + container.border.left = false; + } + + let buffer_jewel_element = { + let diameter = 7.0; + let icon_color = if item.has_conflict(cx) { + Some(tab_style.icon_conflict) + } else if item.is_dirty(cx) { + Some(tab_style.icon_dirty) + } else { + None + }; + + Canvas::new(move |bounds, _, _, cx| { + if let Some(color) = icon_color { + let square = RectF::new(bounds.origin(), vec2f(diameter, diameter)); + cx.scene().push_quad(Quad { + bounds: square, + background: Some(color), + border: Default::default(), + corner_radii: (diameter / 2.).into(), + }); + } + }) + .constrained() + .with_width(diameter) + .with_height(diameter) + .aligned() + }; + + let title_element = title.aligned().contained().with_style(ContainerStyle { + margin: Margin { + left: tab_style.spacing, + right: tab_style.spacing, + ..Default::default() + }, + ..Default::default() + }); + + let close_element = if hovered { + let item_id = item.id(); + enum TabCloseButton {} + let icon = Svg::new("icons/x.svg"); + MouseEventHandler::new::(item_id, cx, |mouse_state, _| { + if mouse_state.hovered() { + icon.with_color(tab_style.icon_close_active) + } else { + icon.with_color(tab_style.icon_close) + } + }) + .with_padding(Padding::uniform(4.)) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, { + let pane = pane.clone(); + move |_, _, cx| { + let pane = pane.clone(); + cx.window_context().defer(move |cx| { + if let Some(pane) = pane.upgrade(cx) { + pane.update(cx, |pane, cx| { + pane.close_item_by_id(item_id, SaveIntent::Close, cx) + .detach_and_log_err(cx); + }); + } + }); + } + }) + .into_any_named("close-tab-icon") + .constrained() + } else { + Empty::new().constrained() + } + .with_width(tab_style.close_icon_width) + .aligned(); + + let close_right = settings::get::(cx).close_position.right(); + + if close_right { + Flex::row() + .with_child(buffer_jewel_element) + .with_child(title_element) + .with_child(close_element) + } else { + Flex::row() + .with_child(close_element) + .with_child(title_element) + .with_child(buffer_jewel_element) + } + .contained() + .with_style(container) + .constrained() + .with_height(tab_style.height) + .into_any() + } + + pub fn render_tab_bar_button< + F1: 'static + Fn(&mut Pane, &mut EventContext), + F2: 'static + Fn(&mut Pane, &mut EventContext), + >( + index: usize, + icon: &'static str, + is_active: bool, + tooltip: Option<(&'static str, Option>)>, + cx: &mut ViewContext, + on_click: F1, + on_down: F2, + context_menu: Option>, + ) -> AnyElement { + enum TabBarButton {} + + let mut button = MouseEventHandler::new::(index, cx, |mouse_state, cx| { + let theme = &settings::get::(cx).theme.workspace.tab_bar; + let style = theme.pane_button.in_state(is_active).style_for(mouse_state); + Svg::new(icon) + .with_color(style.color) + .constrained() + .with_width(style.icon_width) + .aligned() + .constrained() + .with_width(style.button_width) + .with_height(style.button_width) + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_down(MouseButton::Left, move |_, pane, cx| on_down(pane, cx)) + .on_click(MouseButton::Left, move |_, pane, cx| on_click(pane, cx)) + .into_any(); + if let Some((tooltip, action)) = tooltip { + let tooltip_style = settings::get::(cx).theme.tooltip.clone(); + button = button + .with_tooltip::(index, tooltip, action, tooltip_style, cx) + .into_any(); + } + + Stack::new() + .with_child(button) + .with_children( + context_menu.map(|menu| ChildView::new(&menu, cx).aligned().bottom().right()), + ) + .flex(1., false) + .into_any_named("tab bar button") + } + + fn render_blank_pane(&self, theme: &Theme, _cx: &mut ViewContext) -> AnyElement { + let background = theme.workspace.background; + Empty::new() + .contained() + .with_background_color(background) + .into_any() + } + + pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext) { + self.zoomed = zoomed; + cx.notify(); + } + + pub fn is_zoomed(&self) -> bool { + self.zoomed + } +} + +impl Entity for Pane { + type Event = Event; +} + +impl View for Pane { + fn ui_name() -> &'static str { + "Pane" + } + + fn render(&mut self, cx: &mut ViewContext) -> AnyElement { + enum MouseNavigationHandler {} + + MouseEventHandler::new::(0, cx, |_, cx| { + let active_item_index = self.active_item_index; + + if let Some(active_item) = self.active_item() { + Flex::column() + .with_child({ + let theme = theme::current(cx).clone(); + + let mut stack = Stack::new(); + + enum TabBarEventHandler {} + stack.add_child( + MouseEventHandler::new::(0, cx, |_, _| { + Empty::new() + .contained() + .with_style(theme.workspace.tab_bar.container) + }) + .on_down( + MouseButton::Left, + move |_, this, cx| { + this.activate_item(active_item_index, true, true, cx); + }, + ), + ); + let tooltip_style = theme.tooltip.clone(); + let tab_bar_theme = theme.workspace.tab_bar.clone(); + + let nav_button_height = tab_bar_theme.height; + let button_style = tab_bar_theme.nav_button; + let border_for_nav_buttons = tab_bar_theme + .tab_style(false, false) + .container + .border + .clone(); + + let mut tab_row = Flex::row() + .with_child(nav_button( + "icons/arrow_left.svg", + button_style.clone(), + nav_button_height, + tooltip_style.clone(), + self.can_navigate_backward(), + { + move |pane, cx| { + if let Some(workspace) = pane.workspace.upgrade(cx) { + let pane = cx.weak_handle(); + cx.window_context().defer(move |cx| { + workspace.update(cx, |workspace, cx| { + workspace + .go_back(pane, cx) + .detach_and_log_err(cx) + }) + }) + } + } + }, + super::GoBack, + "Go Back", + cx, + )) + .with_child( + nav_button( + "icons/arrow_right.svg", + button_style.clone(), + nav_button_height, + tooltip_style, + self.can_navigate_forward(), + { + move |pane, cx| { + if let Some(workspace) = pane.workspace.upgrade(cx) { + let pane = cx.weak_handle(); + cx.window_context().defer(move |cx| { + workspace.update(cx, |workspace, cx| { + workspace + .go_forward(pane, cx) + .detach_and_log_err(cx) + }) + }) + } + } + }, + super::GoForward, + "Go Forward", + cx, + ) + .contained() + .with_border(border_for_nav_buttons), + ) + .with_child(self.render_tabs(cx).flex(1., true).into_any_named("tabs")); + + if self.has_focus { + let render_tab_bar_buttons = self.render_tab_bar_buttons.clone(); + tab_row.add_child( + (render_tab_bar_buttons)(self, cx) + .contained() + .with_style(theme.workspace.tab_bar.pane_button_container) + .flex(1., false) + .into_any(), + ) + } + + stack.add_child(tab_row); + stack + .constrained() + .with_height(theme.workspace.tab_bar.height) + .flex(1., false) + .into_any_named("tab bar") + }) + .with_child({ + enum PaneContentTabDropTarget {} + dragged_item_receiver::( + self, + 0, + self.active_item_index + 1, + !self.can_split, + if self.can_split { Some(100.) } else { None }, + cx, + { + let toolbar = self.toolbar.clone(); + let toolbar_hidden = toolbar.read(cx).hidden(); + move |_, cx| { + Flex::column() + .with_children( + (!toolbar_hidden) + .then(|| ChildView::new(&toolbar, cx).expanded()), + ) + .with_child( + ChildView::new(active_item.as_any(), cx).flex(1., true), + ) + } + }, + ) + .flex(1., true) + }) + .with_child(ChildView::new(&self.tab_context_menu, cx)) + .into_any() + } else { + enum EmptyPane {} + let theme = theme::current(cx).clone(); + + dragged_item_receiver::(self, 0, 0, false, None, cx, |_, cx| { + self.render_blank_pane(&theme, cx) + }) + .on_down(MouseButton::Left, |_, _, cx| { + cx.focus_parent(); + }) + .into_any() + } + }) + .on_down( + MouseButton::Navigate(NavigationDirection::Back), + move |_, pane, cx| { + if let Some(workspace) = pane.workspace.upgrade(cx) { + let pane = cx.weak_handle(); + cx.window_context().defer(move |cx| { + workspace.update(cx, |workspace, cx| { + workspace.go_back(pane, cx).detach_and_log_err(cx) + }) + }) + } + }, + ) + .on_down(MouseButton::Navigate(NavigationDirection::Forward), { + move |_, pane, cx| { + if let Some(workspace) = pane.workspace.upgrade(cx) { + let pane = cx.weak_handle(); + cx.window_context().defer(move |cx| { + workspace.update(cx, |workspace, cx| { + workspace.go_forward(pane, cx).detach_and_log_err(cx) + }) + }) + } + } + }) + .into_any_named("pane") + } + + fn focus_in(&mut self, focused: AnyViewHandle, cx: &mut ViewContext) { + if !self.has_focus { + self.has_focus = true; + cx.emit(Event::Focus); + cx.notify(); + } + + self.toolbar.update(cx, |toolbar, cx| { + toolbar.focus_changed(true, cx); + }); + + if let Some(active_item) = self.active_item() { + if cx.is_self_focused() { + // Pane was focused directly. We need to either focus a view inside the active item, + // or focus the active item itself + if let Some(weak_last_focused_view) = + self.last_focused_view_by_item.get(&active_item.id()) + { + if let Some(last_focused_view) = weak_last_focused_view.upgrade(cx) { + cx.focus(&last_focused_view); + return; + } else { + self.last_focused_view_by_item.remove(&active_item.id()); + } + } + + cx.focus(active_item.as_any()); + } else if focused != self.tab_bar_context_menu.handle { + self.last_focused_view_by_item + .insert(active_item.id(), focused.downgrade()); + } + } + } + + fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { + self.has_focus = false; + self.toolbar.update(cx, |toolbar, cx| { + toolbar.focus_changed(false, cx); + }); + cx.notify(); + } + + fn update_keymap_context(&self, keymap: &mut KeymapContext, _: &AppContext) { + Self::reset_to_default_keymap_context(keymap); + } +} + +impl ItemNavHistory { + pub fn push(&mut self, data: Option, cx: &mut WindowContext) { + self.history.push(data, self.item.clone(), cx); + } + + pub fn pop_backward(&mut self, cx: &mut WindowContext) -> Option { + self.history.pop(NavigationMode::GoingBack, cx) + } + + pub fn pop_forward(&mut self, cx: &mut WindowContext) -> Option { + self.history.pop(NavigationMode::GoingForward, cx) + } +} + +impl NavHistory { + pub fn for_each_entry( + &self, + cx: &AppContext, + mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option)), + ) { + let borrowed_history = self.0.borrow(); + borrowed_history + .forward_stack + .iter() + .chain(borrowed_history.backward_stack.iter()) + .chain(borrowed_history.closed_stack.iter()) + .for_each(|entry| { + if let Some(project_and_abs_path) = + borrowed_history.paths_by_item.get(&entry.item.id()) + { + f(entry, project_and_abs_path.clone()); + } else if let Some(item) = entry.item.upgrade(cx) { + if let Some(path) = item.project_path(cx) { + f(entry, (path, None)); + } + } + }) + } + + pub fn set_mode(&mut self, mode: NavigationMode) { + self.0.borrow_mut().mode = mode; + } + + pub fn mode(&self) -> NavigationMode { + self.0.borrow().mode + } + + pub fn disable(&mut self) { + self.0.borrow_mut().mode = NavigationMode::Disabled; + } + + pub fn enable(&mut self) { + self.0.borrow_mut().mode = NavigationMode::Normal; + } + + pub fn pop(&mut self, mode: NavigationMode, cx: &mut WindowContext) -> Option { + let mut state = self.0.borrow_mut(); + let entry = match mode { + NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => { + return None + } + NavigationMode::GoingBack => &mut state.backward_stack, + NavigationMode::GoingForward => &mut state.forward_stack, + NavigationMode::ReopeningClosedItem => &mut state.closed_stack, + } + .pop_back(); + if entry.is_some() { + state.did_update(cx); + } + entry + } + + pub fn push( + &mut self, + data: Option, + item: Rc, + cx: &mut WindowContext, + ) { + let state = &mut *self.0.borrow_mut(); + match state.mode { + NavigationMode::Disabled => {} + NavigationMode::Normal | NavigationMode::ReopeningClosedItem => { + if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN { + state.backward_stack.pop_front(); + } + state.backward_stack.push_back(NavigationEntry { + item, + data: data.map(|data| Box::new(data) as Box), + timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst), + }); + state.forward_stack.clear(); + } + NavigationMode::GoingBack => { + if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN { + state.forward_stack.pop_front(); + } + state.forward_stack.push_back(NavigationEntry { + item, + data: data.map(|data| Box::new(data) as Box), + timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst), + }); + } + NavigationMode::GoingForward => { + if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN { + state.backward_stack.pop_front(); + } + state.backward_stack.push_back(NavigationEntry { + item, + data: data.map(|data| Box::new(data) as Box), + timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst), + }); + } + NavigationMode::ClosingItem => { + if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN { + state.closed_stack.pop_front(); + } + state.closed_stack.push_back(NavigationEntry { + item, + data: data.map(|data| Box::new(data) as Box), + timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst), + }); + } + } + state.did_update(cx); + } + + pub fn remove_item(&mut self, item_id: usize) { + let mut state = self.0.borrow_mut(); + state.paths_by_item.remove(&item_id); + state + .backward_stack + .retain(|entry| entry.item.id() != item_id); + state + .forward_stack + .retain(|entry| entry.item.id() != item_id); + state + .closed_stack + .retain(|entry| entry.item.id() != item_id); + } + + pub fn path_for_item(&self, item_id: usize) -> Option<(ProjectPath, Option)> { + self.0.borrow().paths_by_item.get(&item_id).cloned() + } +} + +impl NavHistoryState { + pub fn did_update(&self, cx: &mut WindowContext) { + if let Some(pane) = self.pane.upgrade(cx) { + cx.defer(move |cx| { + pane.update(cx, |pane, cx| pane.history_updated(cx)); + }); + } + } +} + +pub struct PaneBackdrop { + child_view: usize, + child: AnyElement, +} + +impl PaneBackdrop { + pub fn new(pane_item_view: usize, child: AnyElement) -> Self { + PaneBackdrop { + child, + child_view: pane_item_view, + } + } +} + +impl Element for PaneBackdrop { + type LayoutState = (); + + type PaintState = (); + + fn layout( + &mut self, + constraint: gpui::SizeConstraint, + view: &mut V, + cx: &mut ViewContext, + ) -> (Vector2F, Self::LayoutState) { + let size = self.child.layout(constraint, view, cx); + (size, ()) + } + + fn paint( + &mut self, + bounds: RectF, + visible_bounds: RectF, + _: &mut Self::LayoutState, + view: &mut V, + cx: &mut ViewContext, + ) -> Self::PaintState { + let background = theme::current(cx).editor.background; + + let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default(); + + cx.scene().push_quad(gpui::Quad { + bounds: RectF::new(bounds.origin(), bounds.size()), + background: Some(background), + ..Default::default() + }); + + let child_view_id = self.child_view; + cx.scene().push_mouse_region( + MouseRegion::new::(child_view_id, 0, visible_bounds).on_down( + gpui::platform::MouseButton::Left, + move |_, _: &mut V, cx| { + let window = cx.window(); + cx.app_context().focus(window, Some(child_view_id)) + }, + ), + ); + + cx.scene().push_layer(Some(bounds)); + self.child.paint(bounds.origin(), visible_bounds, view, cx); + cx.scene().pop_layer(); + } + + fn rect_for_text_range( + &self, + range_utf16: std::ops::Range, + _bounds: RectF, + _visible_bounds: RectF, + _layout: &Self::LayoutState, + _paint: &Self::PaintState, + view: &V, + cx: &gpui::ViewContext, + ) -> Option { + self.child.rect_for_text_range(range_utf16, view, cx) + } + + fn debug( + &self, + _bounds: RectF, + _layout: &Self::LayoutState, + _paint: &Self::PaintState, + view: &V, + cx: &gpui::ViewContext, + ) -> serde_json::Value { + gpui::json::json!({ + "type": "Pane Back Drop", + "view": self.child_view, + "child": self.child.debug(view, cx), + }) + } +} + +fn dirty_message_for(buffer_path: Option) -> String { + let path = buffer_path + .as_ref() + .and_then(|p| p.path.to_str()) + .unwrap_or(&"This buffer"); + let path = truncate_and_remove_front(path, 80); + format!("{path} contains unsaved edits. Do you want to save it?") +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::item::test::{TestItem, TestProjectItem}; + use gpui::TestAppContext; + use project::FakeFs; + use settings::SettingsStore; + + #[gpui::test] + async fn test_remove_active_empty(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.background()); + + let project = Project::test(fs, None, cx).await; + let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let workspace = window.root(cx); + let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + + pane.update(cx, |pane, cx| { + assert!(pane + .close_active_item(&CloseActiveItem { save_intent: None }, cx) + .is_none()) + }); + } + + #[gpui::test] + async fn test_add_item_with_new_item(cx: &mut TestAppContext) { + cx.foreground().forbid_parking(); + init_test(cx); + let fs = FakeFs::new(cx.background()); + + let project = Project::test(fs, None, cx).await; + let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let workspace = window.root(cx); + let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + + // 1. Add with a destination index + // a. Add before the active item + set_labeled_items(&pane, ["A", "B*", "C"], cx); + pane.update(cx, |pane, cx| { + pane.add_item( + Box::new(cx.add_view(|_| TestItem::new().with_label("D"))), + false, + false, + Some(0), + cx, + ); + }); + assert_item_labels(&pane, ["D*", "A", "B", "C"], cx); + + // b. Add after the active item + set_labeled_items(&pane, ["A", "B*", "C"], cx); + pane.update(cx, |pane, cx| { + pane.add_item( + Box::new(cx.add_view(|_| TestItem::new().with_label("D"))), + false, + false, + Some(2), + cx, + ); + }); + assert_item_labels(&pane, ["A", "B", "D*", "C"], cx); + + // c. Add at the end of the item list (including off the length) + set_labeled_items(&pane, ["A", "B*", "C"], cx); + pane.update(cx, |pane, cx| { + pane.add_item( + Box::new(cx.add_view(|_| TestItem::new().with_label("D"))), + false, + false, + Some(5), + cx, + ); + }); + assert_item_labels(&pane, ["A", "B", "C", "D*"], cx); + + // 2. Add without a destination index + // a. Add with active item at the start of the item list + set_labeled_items(&pane, ["A*", "B", "C"], cx); + pane.update(cx, |pane, cx| { + pane.add_item( + Box::new(cx.add_view(|_| TestItem::new().with_label("D"))), + false, + false, + None, + cx, + ); + }); + set_labeled_items(&pane, ["A", "D*", "B", "C"], cx); + + // b. Add with active item at the end of the item list + set_labeled_items(&pane, ["A", "B", "C*"], cx); + pane.update(cx, |pane, cx| { + pane.add_item( + Box::new(cx.add_view(|_| TestItem::new().with_label("D"))), + false, + false, + None, + cx, + ); + }); + assert_item_labels(&pane, ["A", "B", "C", "D*"], cx); + } + + #[gpui::test] + async fn test_add_item_with_existing_item(cx: &mut TestAppContext) { + cx.foreground().forbid_parking(); + init_test(cx); + let fs = FakeFs::new(cx.background()); + + let project = Project::test(fs, None, cx).await; + let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let workspace = window.root(cx); + let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + + // 1. Add with a destination index + // 1a. Add before the active item + let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx); + pane.update(cx, |pane, cx| { + pane.add_item(d, false, false, Some(0), cx); + }); + assert_item_labels(&pane, ["D*", "A", "B", "C"], cx); + + // 1b. Add after the active item + let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx); + pane.update(cx, |pane, cx| { + pane.add_item(d, false, false, Some(2), cx); + }); + assert_item_labels(&pane, ["A", "B", "D*", "C"], cx); + + // 1c. Add at the end of the item list (including off the length) + let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx); + pane.update(cx, |pane, cx| { + pane.add_item(a, false, false, Some(5), cx); + }); + assert_item_labels(&pane, ["B", "C", "D", "A*"], cx); + + // 1d. Add same item to active index + let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx); + pane.update(cx, |pane, cx| { + pane.add_item(b, false, false, Some(1), cx); + }); + assert_item_labels(&pane, ["A", "B*", "C"], cx); + + // 1e. Add item to index after same item in last position + let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx); + pane.update(cx, |pane, cx| { + pane.add_item(c, false, false, Some(2), cx); + }); + assert_item_labels(&pane, ["A", "B", "C*"], cx); + + // 2. Add without a destination index + // 2a. Add with active item at the start of the item list + let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx); + pane.update(cx, |pane, cx| { + pane.add_item(d, false, false, None, cx); + }); + assert_item_labels(&pane, ["A", "D*", "B", "C"], cx); + + // 2b. Add with active item at the end of the item list + let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx); + pane.update(cx, |pane, cx| { + pane.add_item(a, false, false, None, cx); + }); + assert_item_labels(&pane, ["B", "C", "D", "A*"], cx); + + // 2c. Add active item to active item at end of list + let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx); + pane.update(cx, |pane, cx| { + pane.add_item(c, false, false, None, cx); + }); + assert_item_labels(&pane, ["A", "B", "C*"], cx); + + // 2d. Add active item to active item at start of list + let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx); + pane.update(cx, |pane, cx| { + pane.add_item(a, false, false, None, cx); + }); + assert_item_labels(&pane, ["A*", "B", "C"], cx); + } + + #[gpui::test] + async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) { + cx.foreground().forbid_parking(); + init_test(cx); + let fs = FakeFs::new(cx.background()); + + let project = Project::test(fs, None, cx).await; + let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let workspace = window.root(cx); + let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + + // singleton view + pane.update(cx, |pane, cx| { + let item = TestItem::new() + .with_singleton(true) + .with_label("buffer 1") + .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)]); + + pane.add_item(Box::new(cx.add_view(|_| item)), false, false, None, cx); + }); + assert_item_labels(&pane, ["buffer 1*"], cx); + + // new singleton view with the same project entry + pane.update(cx, |pane, cx| { + let item = TestItem::new() + .with_singleton(true) + .with_label("buffer 1") + .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]); + + pane.add_item(Box::new(cx.add_view(|_| item)), false, false, None, cx); + }); + assert_item_labels(&pane, ["buffer 1*"], cx); + + // new singleton view with different project entry + pane.update(cx, |pane, cx| { + let item = TestItem::new() + .with_singleton(true) + .with_label("buffer 2") + .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)]); + pane.add_item(Box::new(cx.add_view(|_| item)), false, false, None, cx); + }); + assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx); + + // new multibuffer view with the same project entry + pane.update(cx, |pane, cx| { + let item = TestItem::new() + .with_singleton(false) + .with_label("multibuffer 1") + .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]); + + pane.add_item(Box::new(cx.add_view(|_| item)), false, false, None, cx); + }); + assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx); + + // another multibuffer view with the same project entry + pane.update(cx, |pane, cx| { + let item = TestItem::new() + .with_singleton(false) + .with_label("multibuffer 1b") + .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]); + + pane.add_item(Box::new(cx.add_view(|_| item)), false, false, None, cx); + }); + assert_item_labels( + &pane, + ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"], + cx, + ); + } + + #[gpui::test] + async fn test_remove_item_ordering(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.background()); + + let project = Project::test(fs, None, cx).await; + let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let workspace = window.root(cx); + let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + + add_labeled_item(&pane, "A", false, cx); + add_labeled_item(&pane, "B", false, cx); + add_labeled_item(&pane, "C", false, cx); + add_labeled_item(&pane, "D", false, cx); + assert_item_labels(&pane, ["A", "B", "C", "D*"], cx); + + pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx)); + add_labeled_item(&pane, "1", false, cx); + assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx); + + pane.update(cx, |pane, cx| { + pane.close_active_item(&CloseActiveItem { save_intent: None }, cx) + }) + .unwrap() + .await + .unwrap(); + assert_item_labels(&pane, ["A", "B*", "C", "D"], cx); + + pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx)); + assert_item_labels(&pane, ["A", "B", "C", "D*"], cx); + + pane.update(cx, |pane, cx| { + pane.close_active_item(&CloseActiveItem { save_intent: None }, cx) + }) + .unwrap() + .await + .unwrap(); + assert_item_labels(&pane, ["A", "B*", "C"], cx); + + pane.update(cx, |pane, cx| { + pane.close_active_item(&CloseActiveItem { save_intent: None }, cx) + }) + .unwrap() + .await + .unwrap(); + assert_item_labels(&pane, ["A", "C*"], cx); + + pane.update(cx, |pane, cx| { + pane.close_active_item(&CloseActiveItem { save_intent: None }, cx) + }) + .unwrap() + .await + .unwrap(); + assert_item_labels(&pane, ["A*"], cx); + } + + #[gpui::test] + async fn test_close_inactive_items(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.background()); + + let project = Project::test(fs, None, cx).await; + let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let workspace = window.root(cx); + let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + + set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx); + + pane.update(cx, |pane, cx| { + pane.close_inactive_items(&CloseInactiveItems, cx) + }) + .unwrap() + .await + .unwrap(); + assert_item_labels(&pane, ["C*"], cx); + } + + #[gpui::test] + async fn test_close_clean_items(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.background()); + + let project = Project::test(fs, None, cx).await; + let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let workspace = window.root(cx); + let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + + add_labeled_item(&pane, "A", true, cx); + add_labeled_item(&pane, "B", false, cx); + add_labeled_item(&pane, "C", true, cx); + add_labeled_item(&pane, "D", false, cx); + add_labeled_item(&pane, "E", false, cx); + assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx); + + pane.update(cx, |pane, cx| pane.close_clean_items(&CloseCleanItems, cx)) + .unwrap() + .await + .unwrap(); + assert_item_labels(&pane, ["A^", "C*^"], cx); + } + + #[gpui::test] + async fn test_close_items_to_the_left(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.background()); + + let project = Project::test(fs, None, cx).await; + let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let workspace = window.root(cx); + let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + + set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx); + + pane.update(cx, |pane, cx| { + pane.close_items_to_the_left(&CloseItemsToTheLeft, cx) + }) + .unwrap() + .await + .unwrap(); + assert_item_labels(&pane, ["C*", "D", "E"], cx); + } + + #[gpui::test] + async fn test_close_items_to_the_right(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.background()); + + let project = Project::test(fs, None, cx).await; + let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let workspace = window.root(cx); + let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + + set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx); + + pane.update(cx, |pane, cx| { + pane.close_items_to_the_right(&CloseItemsToTheRight, cx) + }) + .unwrap() + .await + .unwrap(); + assert_item_labels(&pane, ["A", "B", "C*"], cx); + } + + #[gpui::test] + async fn test_close_all_items(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.background()); + + let project = Project::test(fs, None, cx).await; + let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let workspace = window.root(cx); + let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + + add_labeled_item(&pane, "A", false, cx); + add_labeled_item(&pane, "B", false, cx); + add_labeled_item(&pane, "C", false, cx); + assert_item_labels(&pane, ["A", "B", "C*"], cx); + + pane.update(cx, |pane, cx| { + pane.close_all_items(&CloseAllItems { save_intent: None }, cx) + }) + .unwrap() + .await + .unwrap(); + assert_item_labels(&pane, [], cx); + + add_labeled_item(&pane, "A", true, cx); + add_labeled_item(&pane, "B", true, cx); + add_labeled_item(&pane, "C", true, cx); + assert_item_labels(&pane, ["A^", "B^", "C*^"], cx); + + let save = pane + .update(cx, |pane, cx| { + pane.close_all_items(&CloseAllItems { save_intent: None }, cx) + }) + .unwrap(); + + cx.foreground().run_until_parked(); + window.simulate_prompt_answer(2, cx); + save.await.unwrap(); + assert_item_labels(&pane, [], cx); + } + + fn init_test(cx: &mut TestAppContext) { + cx.update(|cx| { + cx.set_global(SettingsStore::test(cx)); + theme::init((), cx); + crate::init_settings(cx); + Project::init_settings(cx); + }); + } + + fn add_labeled_item( + pane: &ViewHandle, + label: &str, + is_dirty: bool, + cx: &mut TestAppContext, + ) -> Box> { + pane.update(cx, |pane, cx| { + let labeled_item = + Box::new(cx.add_view(|_| TestItem::new().with_label(label).with_dirty(is_dirty))); + pane.add_item(labeled_item.clone(), false, false, None, cx); + labeled_item + }) + } + + fn set_labeled_items( + pane: &ViewHandle, + labels: [&str; COUNT], + cx: &mut TestAppContext, + ) -> [Box>; COUNT] { + pane.update(cx, |pane, cx| { + pane.items.clear(); + let mut active_item_index = 0; + + let mut index = 0; + let items = labels.map(|mut label| { + if label.ends_with("*") { + label = label.trim_end_matches("*"); + active_item_index = index; + } + + let labeled_item = Box::new(cx.add_view(|_| TestItem::new().with_label(label))); + pane.add_item(labeled_item.clone(), false, false, None, cx); + index += 1; + labeled_item + }); + + pane.activate_item(active_item_index, false, false, cx); + + items + }) + } + + // Assert the item label, with the active item label suffixed with a '*' + fn assert_item_labels( + pane: &ViewHandle, + expected_states: [&str; COUNT], + cx: &mut TestAppContext, + ) { + pane.read_with(cx, |pane, cx| { + let actual_states = pane + .items + .iter() + .enumerate() + .map(|(ix, item)| { + let mut state = item + .as_any() + .downcast_ref::() + .unwrap() + .read(cx) + .label + .clone(); + if ix == pane.active_item_index { + state.push('*'); + } + if item.is_dirty(cx) { + state.push('^'); + } + state + }) + .collect::>(); + + assert_eq!( + actual_states, expected_states, + "pane items do not match expectation" + ); + }) + } +} diff --git a/crates/workspace2/src/pane/dragged_item_receiver.rs b/crates/workspace2/src/pane/dragged_item_receiver.rs new file mode 100644 index 0000000000..292529e787 --- /dev/null +++ b/crates/workspace2/src/pane/dragged_item_receiver.rs @@ -0,0 +1,239 @@ +use super::DraggedItem; +use crate::{Pane, SplitDirection, Workspace}; +use gpui2::{ + color::Color, + elements::{Canvas, MouseEventHandler, ParentElement, Stack}, + geometry::{rect::RectF, vector::Vector2F}, + platform::MouseButton, + scene::MouseUp, + AppContext, Element, EventContext, MouseState, Quad, ViewContext, WeakViewHandle, +}; +use project2::ProjectEntryId; + +pub fn dragged_item_receiver( + pane: &Pane, + region_id: usize, + drop_index: usize, + allow_same_pane: bool, + split_margin: Option, + cx: &mut ViewContext, + render_child: F, +) -> MouseEventHandler +where + Tag: 'static, + D: Element, + F: FnOnce(&mut MouseState, &mut ViewContext) -> D, +{ + let drag_and_drop = cx.global::>(); + let drag_position = if (pane.can_drop)(drag_and_drop, cx) { + drag_and_drop + .currently_dragged::(cx.window()) + .map(|(drag_position, _)| drag_position) + .or_else(|| { + drag_and_drop + .currently_dragged::(cx.window()) + .map(|(drag_position, _)| drag_position) + }) + } else { + None + }; + + let mut handler = MouseEventHandler::above::(region_id, cx, |state, cx| { + // Observing hovered will cause a render when the mouse enters regardless + // of if mouse position was accessed before + let drag_position = if state.dragging() { + drag_position + } else { + None + }; + Stack::new() + .with_child(render_child(state, cx)) + .with_children(drag_position.map(|drag_position| { + Canvas::new(move |bounds, _, _, cx| { + if bounds.contains_point(drag_position) { + let overlay_region = split_margin + .and_then(|split_margin| { + drop_split_direction(drag_position, bounds, split_margin) + .map(|dir| (dir, split_margin)) + }) + .map(|(dir, margin)| dir.along_edge(bounds, margin)) + .unwrap_or(bounds); + + cx.scene().push_stacking_context(None, None); + let background = overlay_color(cx); + cx.scene().push_quad(Quad { + bounds: overlay_region, + background: Some(background), + border: Default::default(), + corner_radii: Default::default(), + }); + cx.scene().pop_stacking_context(); + } + }) + })) + }); + + if drag_position.is_some() { + handler = handler + .on_up(MouseButton::Left, { + move |event, pane, cx| { + let workspace = pane.workspace.clone(); + let pane = cx.weak_handle(); + handle_dropped_item( + event, + workspace, + &pane, + drop_index, + allow_same_pane, + split_margin, + cx, + ); + cx.notify(); + } + }) + .on_move(|_, _, cx| { + let drag_and_drop = cx.global::>(); + + if drag_and_drop + .currently_dragged::(cx.window()) + .is_some() + || drag_and_drop + .currently_dragged::(cx.window()) + .is_some() + { + cx.notify(); + } else { + cx.propagate_event(); + } + }) + } + + handler +} + +pub fn handle_dropped_item( + event: MouseUp, + workspace: WeakViewHandle, + pane: &WeakViewHandle, + index: usize, + allow_same_pane: bool, + split_margin: Option, + cx: &mut EventContext, +) { + enum Action { + Move(WeakViewHandle, usize), + Open(ProjectEntryId), + } + let drag_and_drop = cx.global::>(); + let action = if let Some((_, dragged_item)) = + drag_and_drop.currently_dragged::(cx.window()) + { + Action::Move(dragged_item.pane.clone(), dragged_item.handle.id()) + } else if let Some((_, project_entry)) = + drag_and_drop.currently_dragged::(cx.window()) + { + Action::Open(*project_entry) + } else { + cx.propagate_event(); + return; + }; + + if let Some(split_direction) = + split_margin.and_then(|margin| drop_split_direction(event.position, event.region, margin)) + { + let pane_to_split = pane.clone(); + match action { + Action::Move(from, item_id_to_move) => { + cx.window_context().defer(move |cx| { + if let Some(workspace) = workspace.upgrade(cx) { + workspace.update(cx, |workspace, cx| { + workspace.split_pane_with_item( + pane_to_split, + split_direction, + from, + item_id_to_move, + cx, + ); + }) + } + }); + } + Action::Open(project_entry) => { + cx.window_context().defer(move |cx| { + if let Some(workspace) = workspace.upgrade(cx) { + workspace.update(cx, |workspace, cx| { + if let Some(task) = workspace.split_pane_with_project_entry( + pane_to_split, + split_direction, + project_entry, + cx, + ) { + task.detach_and_log_err(cx); + } + }) + } + }); + } + }; + } else { + match action { + Action::Move(from, item_id) => { + if pane != &from || allow_same_pane { + let pane = pane.clone(); + cx.window_context().defer(move |cx| { + if let Some(((workspace, from), to)) = workspace + .upgrade(cx) + .zip(from.upgrade(cx)) + .zip(pane.upgrade(cx)) + { + workspace.update(cx, |workspace, cx| { + workspace.move_item(from, to, item_id, index, cx); + }) + } + }); + } else { + cx.propagate_event(); + } + } + Action::Open(project_entry) => { + let pane = pane.clone(); + cx.window_context().defer(move |cx| { + if let Some(workspace) = workspace.upgrade(cx) { + workspace.update(cx, |workspace, cx| { + if let Some(path) = + workspace.project.read(cx).path_for_entry(project_entry, cx) + { + workspace + .open_path(path, Some(pane), true, cx) + .detach_and_log_err(cx); + } + }); + } + }); + } + } + } +} + +fn drop_split_direction( + position: Vector2F, + region: RectF, + split_margin: f32, +) -> Option { + let mut min_direction = None; + let mut min_distance = split_margin; + for direction in SplitDirection::all() { + let edge_distance = (direction.edge(region) - direction.axis().component(position)).abs(); + + if edge_distance < min_distance { + min_direction = Some(direction); + min_distance = edge_distance; + } + } + + min_direction +} + +fn overlay_color(cx: &AppContext) -> Color { + theme2::current(cx).workspace.drop_target_overlay_color +} diff --git a/crates/workspace2/src/pane_group.rs b/crates/workspace2/src/pane_group.rs new file mode 100644 index 0000000000..aef03dcda0 --- /dev/null +++ b/crates/workspace2/src/pane_group.rs @@ -0,0 +1,989 @@ +use crate::{pane_group::element::PaneAxisElement, AppState, FollowerState, Pane, Workspace}; +use anyhow::{anyhow, Result}; +use call::{ActiveCall, ParticipantLocation}; +use collections::HashMap; +use gpui::{ + elements::*, + geometry::{rect::RectF, vector::Vector2F}, + platform::{CursorStyle, MouseButton}, + AnyViewHandle, Axis, ModelHandle, ViewContext, ViewHandle, +}; +use project::Project; +use serde::Deserialize; +use std::{cell::RefCell, rc::Rc, sync::Arc}; +use theme::Theme; + +const HANDLE_HITBOX_SIZE: f32 = 4.0; +const HORIZONTAL_MIN_SIZE: f32 = 80.; +const VERTICAL_MIN_SIZE: f32 = 100.; + +#[derive(Clone, Debug, PartialEq)] +pub struct PaneGroup { + pub(crate) root: Member, +} + +impl PaneGroup { + pub(crate) fn with_root(root: Member) -> Self { + Self { root } + } + + pub fn new(pane: ViewHandle) -> Self { + Self { + root: Member::Pane(pane), + } + } + + pub fn split( + &mut self, + old_pane: &ViewHandle, + new_pane: &ViewHandle, + direction: SplitDirection, + ) -> Result<()> { + match &mut self.root { + Member::Pane(pane) => { + if pane == old_pane { + self.root = Member::new_axis(old_pane.clone(), new_pane.clone(), direction); + Ok(()) + } else { + Err(anyhow!("Pane not found")) + } + } + Member::Axis(axis) => axis.split(old_pane, new_pane, direction), + } + } + + pub fn bounding_box_for_pane(&self, pane: &ViewHandle) -> Option { + match &self.root { + Member::Pane(_) => None, + Member::Axis(axis) => axis.bounding_box_for_pane(pane), + } + } + + pub fn pane_at_pixel_position(&self, coordinate: Vector2F) -> Option<&ViewHandle> { + match &self.root { + Member::Pane(pane) => Some(pane), + Member::Axis(axis) => axis.pane_at_pixel_position(coordinate), + } + } + + /// Returns: + /// - Ok(true) if it found and removed a pane + /// - Ok(false) if it found but did not remove the pane + /// - Err(_) if it did not find the pane + pub fn remove(&mut self, pane: &ViewHandle) -> Result { + match &mut self.root { + Member::Pane(_) => Ok(false), + Member::Axis(axis) => { + if let Some(last_pane) = axis.remove(pane)? { + self.root = last_pane; + } + Ok(true) + } + } + } + + pub fn swap(&mut self, from: &ViewHandle, to: &ViewHandle) { + match &mut self.root { + Member::Pane(_) => {} + Member::Axis(axis) => axis.swap(from, to), + }; + } + + pub(crate) fn render( + &self, + project: &ModelHandle, + theme: &Theme, + follower_states: &HashMap, FollowerState>, + active_call: Option<&ModelHandle>, + active_pane: &ViewHandle, + zoomed: Option<&AnyViewHandle>, + app_state: &Arc, + cx: &mut ViewContext, + ) -> AnyElement { + self.root.render( + project, + 0, + theme, + follower_states, + active_call, + active_pane, + zoomed, + app_state, + cx, + ) + } + + pub(crate) fn panes(&self) -> Vec<&ViewHandle> { + let mut panes = Vec::new(); + self.root.collect_panes(&mut panes); + panes + } +} + +#[derive(Clone, Debug, PartialEq)] +pub(crate) enum Member { + Axis(PaneAxis), + Pane(ViewHandle), +} + +impl Member { + fn new_axis( + old_pane: ViewHandle, + new_pane: ViewHandle, + direction: SplitDirection, + ) -> Self { + use Axis::*; + use SplitDirection::*; + + let axis = match direction { + Up | Down => Vertical, + Left | Right => Horizontal, + }; + + let members = match direction { + Up | Left => vec![Member::Pane(new_pane), Member::Pane(old_pane)], + Down | Right => vec![Member::Pane(old_pane), Member::Pane(new_pane)], + }; + + Member::Axis(PaneAxis::new(axis, members)) + } + + fn contains(&self, needle: &ViewHandle) -> bool { + match self { + Member::Axis(axis) => axis.members.iter().any(|member| member.contains(needle)), + Member::Pane(pane) => pane == needle, + } + } + + pub fn render( + &self, + project: &ModelHandle, + basis: usize, + theme: &Theme, + follower_states: &HashMap, FollowerState>, + active_call: Option<&ModelHandle>, + active_pane: &ViewHandle, + zoomed: Option<&AnyViewHandle>, + app_state: &Arc, + cx: &mut ViewContext, + ) -> AnyElement { + enum FollowIntoExternalProject {} + + match self { + Member::Pane(pane) => { + let pane_element = if Some(&**pane) == zoomed { + Empty::new().into_any() + } else { + ChildView::new(pane, cx).into_any() + }; + + let leader = follower_states.get(pane).and_then(|state| { + let room = active_call?.read(cx).room()?.read(cx); + room.remote_participant_for_peer_id(state.leader_id) + }); + + let mut leader_border = Border::default(); + let mut leader_status_box = None; + if let Some(leader) = &leader { + let leader_color = theme + .editor + .selection_style_for_room_participant(leader.participant_index.0) + .cursor; + leader_border = Border::all(theme.workspace.leader_border_width, leader_color); + leader_border + .color + .fade_out(1. - theme.workspace.leader_border_opacity); + leader_border.overlay = true; + + leader_status_box = match leader.location { + ParticipantLocation::SharedProject { + project_id: leader_project_id, + } => { + if Some(leader_project_id) == project.read(cx).remote_id() { + None + } else { + let leader_user = leader.user.clone(); + let leader_user_id = leader.user.id; + Some( + MouseEventHandler::new::( + pane.id(), + cx, + |_, _| { + Label::new( + format!( + "Follow {} to their active project", + leader_user.github_login, + ), + theme + .workspace + .external_location_message + .text + .clone(), + ) + .contained() + .with_style( + theme.workspace.external_location_message.container, + ) + }, + ) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, this, cx| { + crate::join_remote_project( + leader_project_id, + leader_user_id, + this.app_state().clone(), + cx, + ) + .detach_and_log_err(cx); + }) + .aligned() + .bottom() + .right() + .into_any(), + ) + } + } + ParticipantLocation::UnsharedProject => Some( + Label::new( + format!( + "{} is viewing an unshared Zed project", + leader.user.github_login + ), + theme.workspace.external_location_message.text.clone(), + ) + .contained() + .with_style(theme.workspace.external_location_message.container) + .aligned() + .bottom() + .right() + .into_any(), + ), + ParticipantLocation::External => Some( + Label::new( + format!( + "{} is viewing a window outside of Zed", + leader.user.github_login + ), + theme.workspace.external_location_message.text.clone(), + ) + .contained() + .with_style(theme.workspace.external_location_message.container) + .aligned() + .bottom() + .right() + .into_any(), + ), + }; + } + + Stack::new() + .with_child(pane_element.contained().with_border(leader_border)) + .with_children(leader_status_box) + .into_any() + } + Member::Axis(axis) => axis.render( + project, + basis + 1, + theme, + follower_states, + active_call, + active_pane, + zoomed, + app_state, + cx, + ), + } + } + + fn collect_panes<'a>(&'a self, panes: &mut Vec<&'a ViewHandle>) { + match self { + Member::Axis(axis) => { + for member in &axis.members { + member.collect_panes(panes); + } + } + Member::Pane(pane) => panes.push(pane), + } + } +} + +#[derive(Clone, Debug, PartialEq)] +pub(crate) struct PaneAxis { + pub axis: Axis, + pub members: Vec, + pub flexes: Rc>>, + pub bounding_boxes: Rc>>>, +} + +impl PaneAxis { + pub fn new(axis: Axis, members: Vec) -> Self { + let flexes = Rc::new(RefCell::new(vec![1.; members.len()])); + let bounding_boxes = Rc::new(RefCell::new(vec![None; members.len()])); + Self { + axis, + members, + flexes, + bounding_boxes, + } + } + + pub fn load(axis: Axis, members: Vec, flexes: Option>) -> Self { + let flexes = flexes.unwrap_or_else(|| vec![1.; members.len()]); + debug_assert!(members.len() == flexes.len()); + + let flexes = Rc::new(RefCell::new(flexes)); + let bounding_boxes = Rc::new(RefCell::new(vec![None; members.len()])); + Self { + axis, + members, + flexes, + bounding_boxes, + } + } + + fn split( + &mut self, + old_pane: &ViewHandle, + new_pane: &ViewHandle, + direction: SplitDirection, + ) -> Result<()> { + for (mut idx, member) in self.members.iter_mut().enumerate() { + match member { + Member::Axis(axis) => { + if axis.split(old_pane, new_pane, direction).is_ok() { + return Ok(()); + } + } + Member::Pane(pane) => { + if pane == old_pane { + if direction.axis() == self.axis { + if direction.increasing() { + idx += 1; + } + + self.members.insert(idx, Member::Pane(new_pane.clone())); + *self.flexes.borrow_mut() = vec![1.; self.members.len()]; + } else { + *member = + Member::new_axis(old_pane.clone(), new_pane.clone(), direction); + } + return Ok(()); + } + } + } + } + Err(anyhow!("Pane not found")) + } + + fn remove(&mut self, pane_to_remove: &ViewHandle) -> Result> { + let mut found_pane = false; + let mut remove_member = None; + for (idx, member) in self.members.iter_mut().enumerate() { + match member { + Member::Axis(axis) => { + if let Ok(last_pane) = axis.remove(pane_to_remove) { + if let Some(last_pane) = last_pane { + *member = last_pane; + } + found_pane = true; + break; + } + } + Member::Pane(pane) => { + if pane == pane_to_remove { + found_pane = true; + remove_member = Some(idx); + break; + } + } + } + } + + if found_pane { + if let Some(idx) = remove_member { + self.members.remove(idx); + *self.flexes.borrow_mut() = vec![1.; self.members.len()]; + } + + if self.members.len() == 1 { + let result = self.members.pop(); + *self.flexes.borrow_mut() = vec![1.; self.members.len()]; + Ok(result) + } else { + Ok(None) + } + } else { + Err(anyhow!("Pane not found")) + } + } + + fn swap(&mut self, from: &ViewHandle, to: &ViewHandle) { + for member in self.members.iter_mut() { + match member { + Member::Axis(axis) => axis.swap(from, to), + Member::Pane(pane) => { + if pane == from { + *member = Member::Pane(to.clone()); + } else if pane == to { + *member = Member::Pane(from.clone()) + } + } + } + } + } + + fn bounding_box_for_pane(&self, pane: &ViewHandle) -> Option { + debug_assert!(self.members.len() == self.bounding_boxes.borrow().len()); + + for (idx, member) in self.members.iter().enumerate() { + match member { + Member::Pane(found) => { + if pane == found { + return self.bounding_boxes.borrow()[idx]; + } + } + Member::Axis(axis) => { + if let Some(rect) = axis.bounding_box_for_pane(pane) { + return Some(rect); + } + } + } + } + None + } + + fn pane_at_pixel_position(&self, coordinate: Vector2F) -> Option<&ViewHandle> { + debug_assert!(self.members.len() == self.bounding_boxes.borrow().len()); + + let bounding_boxes = self.bounding_boxes.borrow(); + + for (idx, member) in self.members.iter().enumerate() { + if let Some(coordinates) = bounding_boxes[idx] { + if coordinates.contains_point(coordinate) { + return match member { + Member::Pane(found) => Some(found), + Member::Axis(axis) => axis.pane_at_pixel_position(coordinate), + }; + } + } + } + None + } + + fn render( + &self, + project: &ModelHandle, + basis: usize, + theme: &Theme, + follower_states: &HashMap, FollowerState>, + active_call: Option<&ModelHandle>, + active_pane: &ViewHandle, + zoomed: Option<&AnyViewHandle>, + app_state: &Arc, + cx: &mut ViewContext, + ) -> AnyElement { + debug_assert!(self.members.len() == self.flexes.borrow().len()); + + let mut pane_axis = PaneAxisElement::new( + self.axis, + basis, + self.flexes.clone(), + self.bounding_boxes.clone(), + ); + let mut active_pane_ix = None; + + let mut members = self.members.iter().enumerate().peekable(); + while let Some((ix, member)) = members.next() { + let last = members.peek().is_none(); + + if member.contains(active_pane) { + active_pane_ix = Some(ix); + } + + let mut member = member.render( + project, + (basis + ix) * 10, + theme, + follower_states, + active_call, + active_pane, + zoomed, + app_state, + cx, + ); + + if !last { + let mut border = theme.workspace.pane_divider; + border.left = false; + border.right = false; + border.top = false; + border.bottom = false; + + match self.axis { + Axis::Vertical => border.bottom = true, + Axis::Horizontal => border.right = true, + } + + member = member.contained().with_border(border).into_any(); + } + + pane_axis = pane_axis.with_child(member.into_any()); + } + pane_axis.set_active_pane(active_pane_ix); + pane_axis.into_any() + } +} + +#[derive(Clone, Copy, Debug, Deserialize, PartialEq)] +pub enum SplitDirection { + Up, + Down, + Left, + Right, +} + +impl SplitDirection { + pub fn all() -> [Self; 4] { + [Self::Up, Self::Down, Self::Left, Self::Right] + } + + pub fn edge(&self, rect: RectF) -> f32 { + match self { + Self::Up => rect.min_y(), + Self::Down => rect.max_y(), + Self::Left => rect.min_x(), + Self::Right => rect.max_x(), + } + } + + // Returns a new rectangle which shares an edge in SplitDirection and has `size` along SplitDirection + pub fn along_edge(&self, rect: RectF, size: f32) -> RectF { + match self { + Self::Up => RectF::new(rect.origin(), Vector2F::new(rect.width(), size)), + Self::Down => RectF::new( + rect.lower_left() - Vector2F::new(0., size), + Vector2F::new(rect.width(), size), + ), + Self::Left => RectF::new(rect.origin(), Vector2F::new(size, rect.height())), + Self::Right => RectF::new( + rect.upper_right() - Vector2F::new(size, 0.), + Vector2F::new(size, rect.height()), + ), + } + } + + pub fn axis(&self) -> Axis { + match self { + Self::Up | Self::Down => Axis::Vertical, + Self::Left | Self::Right => Axis::Horizontal, + } + } + + pub fn increasing(&self) -> bool { + match self { + Self::Left | Self::Up => false, + Self::Down | Self::Right => true, + } + } +} + +mod element { + use std::{cell::RefCell, iter::from_fn, ops::Range, rc::Rc}; + + use gpui::{ + geometry::{ + rect::RectF, + vector::{vec2f, Vector2F}, + }, + json::{self, ToJson}, + platform::{CursorStyle, MouseButton}, + scene::MouseDrag, + AnyElement, Axis, CursorRegion, Element, EventContext, MouseRegion, RectFExt, + SizeConstraint, Vector2FExt, ViewContext, + }; + + use crate::{ + pane_group::{HANDLE_HITBOX_SIZE, HORIZONTAL_MIN_SIZE, VERTICAL_MIN_SIZE}, + Workspace, WorkspaceSettings, + }; + + pub struct PaneAxisElement { + axis: Axis, + basis: usize, + active_pane_ix: Option, + flexes: Rc>>, + children: Vec>, + bounding_boxes: Rc>>>, + } + + impl PaneAxisElement { + pub fn new( + axis: Axis, + basis: usize, + flexes: Rc>>, + bounding_boxes: Rc>>>, + ) -> Self { + Self { + axis, + basis, + flexes, + bounding_boxes, + active_pane_ix: None, + children: Default::default(), + } + } + + pub fn set_active_pane(&mut self, active_pane_ix: Option) { + self.active_pane_ix = active_pane_ix; + } + + fn layout_children( + &mut self, + active_pane_magnification: f32, + constraint: SizeConstraint, + remaining_space: &mut f32, + remaining_flex: &mut f32, + cross_axis_max: &mut f32, + view: &mut Workspace, + cx: &mut ViewContext, + ) { + let flexes = self.flexes.borrow(); + let cross_axis = self.axis.invert(); + for (ix, child) in self.children.iter_mut().enumerate() { + let flex = if active_pane_magnification != 1. { + if let Some(active_pane_ix) = self.active_pane_ix { + if ix == active_pane_ix { + active_pane_magnification + } else { + 1. + } + } else { + 1. + } + } else { + flexes[ix] + }; + + let child_size = if *remaining_flex == 0.0 { + *remaining_space + } else { + let space_per_flex = *remaining_space / *remaining_flex; + space_per_flex * flex + }; + + let child_constraint = match self.axis { + Axis::Horizontal => SizeConstraint::new( + vec2f(child_size, constraint.min.y()), + vec2f(child_size, constraint.max.y()), + ), + Axis::Vertical => SizeConstraint::new( + vec2f(constraint.min.x(), child_size), + vec2f(constraint.max.x(), child_size), + ), + }; + let child_size = child.layout(child_constraint, view, cx); + *remaining_space -= child_size.along(self.axis); + *remaining_flex -= flex; + *cross_axis_max = cross_axis_max.max(child_size.along(cross_axis)); + } + } + + fn handle_resize( + flexes: Rc>>, + axis: Axis, + preceding_ix: usize, + child_start: Vector2F, + drag_bounds: RectF, + ) -> impl Fn(MouseDrag, &mut Workspace, &mut EventContext) { + let size = move |ix, flexes: &[f32]| { + drag_bounds.length_along(axis) * (flexes[ix] / flexes.len() as f32) + }; + + move |drag, workspace: &mut Workspace, cx| { + if drag.end { + // TODO: Clear cascading resize state + return; + } + let min_size = match axis { + Axis::Horizontal => HORIZONTAL_MIN_SIZE, + Axis::Vertical => VERTICAL_MIN_SIZE, + }; + let mut flexes = flexes.borrow_mut(); + + // Don't allow resizing to less than the minimum size, if elements are already too small + if min_size - 1. > size(preceding_ix, flexes.as_slice()) { + return; + } + + let mut proposed_current_pixel_change = (drag.position - child_start).along(axis) + - size(preceding_ix, flexes.as_slice()); + + let flex_changes = |pixel_dx, target_ix, next: isize, flexes: &[f32]| { + let flex_change = pixel_dx / drag_bounds.length_along(axis); + let current_target_flex = flexes[target_ix] + flex_change; + let next_target_flex = + flexes[(target_ix as isize + next) as usize] - flex_change; + (current_target_flex, next_target_flex) + }; + + let mut successors = from_fn({ + let forward = proposed_current_pixel_change > 0.; + let mut ix_offset = 0; + let len = flexes.len(); + move || { + let result = if forward { + (preceding_ix + 1 + ix_offset < len).then(|| preceding_ix + ix_offset) + } else { + (preceding_ix as isize - ix_offset as isize >= 0) + .then(|| preceding_ix - ix_offset) + }; + + ix_offset += 1; + + result + } + }); + + while proposed_current_pixel_change.abs() > 0. { + let Some(current_ix) = successors.next() else { + break; + }; + + let next_target_size = f32::max( + size(current_ix + 1, flexes.as_slice()) - proposed_current_pixel_change, + min_size, + ); + + let current_target_size = f32::max( + size(current_ix, flexes.as_slice()) + + size(current_ix + 1, flexes.as_slice()) + - next_target_size, + min_size, + ); + + let current_pixel_change = + current_target_size - size(current_ix, flexes.as_slice()); + + let (current_target_flex, next_target_flex) = + flex_changes(current_pixel_change, current_ix, 1, flexes.as_slice()); + + flexes[current_ix] = current_target_flex; + flexes[current_ix + 1] = next_target_flex; + + proposed_current_pixel_change -= current_pixel_change; + } + + workspace.schedule_serialize(cx); + cx.notify(); + } + } + } + + impl Extend> for PaneAxisElement { + fn extend>>(&mut self, children: T) { + self.children.extend(children); + } + } + + impl Element for PaneAxisElement { + type LayoutState = f32; + type PaintState = (); + + fn layout( + &mut self, + constraint: SizeConstraint, + view: &mut Workspace, + cx: &mut ViewContext, + ) -> (Vector2F, Self::LayoutState) { + debug_assert!(self.children.len() == self.flexes.borrow().len()); + + let active_pane_magnification = + settings::get::(cx).active_pane_magnification; + + let mut remaining_flex = 0.; + + if active_pane_magnification != 1. { + let active_pane_flex = self + .active_pane_ix + .map(|_| active_pane_magnification) + .unwrap_or(1.); + remaining_flex += self.children.len() as f32 - 1. + active_pane_flex; + } else { + for flex in self.flexes.borrow().iter() { + remaining_flex += flex; + } + } + + let mut cross_axis_max: f32 = 0.0; + let mut remaining_space = constraint.max_along(self.axis); + + if remaining_space.is_infinite() { + panic!("flex contains flexible children but has an infinite constraint along the flex axis"); + } + + self.layout_children( + active_pane_magnification, + constraint, + &mut remaining_space, + &mut remaining_flex, + &mut cross_axis_max, + view, + cx, + ); + + let mut size = match self.axis { + Axis::Horizontal => vec2f(constraint.max.x() - remaining_space, cross_axis_max), + Axis::Vertical => vec2f(cross_axis_max, constraint.max.y() - remaining_space), + }; + + if constraint.min.x().is_finite() { + size.set_x(size.x().max(constraint.min.x())); + } + if constraint.min.y().is_finite() { + size.set_y(size.y().max(constraint.min.y())); + } + + if size.x() > constraint.max.x() { + size.set_x(constraint.max.x()); + } + if size.y() > constraint.max.y() { + size.set_y(constraint.max.y()); + } + + (size, remaining_space) + } + + fn paint( + &mut self, + bounds: RectF, + visible_bounds: RectF, + remaining_space: &mut Self::LayoutState, + view: &mut Workspace, + cx: &mut ViewContext, + ) -> Self::PaintState { + let can_resize = settings::get::(cx).active_pane_magnification == 1.; + let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default(); + + let overflowing = *remaining_space < 0.; + if overflowing { + cx.scene().push_layer(Some(visible_bounds)); + } + + let mut child_origin = bounds.origin(); + + let mut bounding_boxes = self.bounding_boxes.borrow_mut(); + bounding_boxes.clear(); + + let mut children_iter = self.children.iter_mut().enumerate().peekable(); + while let Some((ix, child)) = children_iter.next() { + let child_start = child_origin.clone(); + child.paint(child_origin, visible_bounds, view, cx); + + bounding_boxes.push(Some(RectF::new(child_origin, child.size()))); + + match self.axis { + Axis::Horizontal => child_origin += vec2f(child.size().x(), 0.0), + Axis::Vertical => child_origin += vec2f(0.0, child.size().y()), + } + + if can_resize && children_iter.peek().is_some() { + cx.scene().push_stacking_context(None, None); + + let handle_origin = match self.axis { + Axis::Horizontal => child_origin - vec2f(HANDLE_HITBOX_SIZE / 2., 0.0), + Axis::Vertical => child_origin - vec2f(0.0, HANDLE_HITBOX_SIZE / 2.), + }; + + let handle_bounds = match self.axis { + Axis::Horizontal => RectF::new( + handle_origin, + vec2f(HANDLE_HITBOX_SIZE, visible_bounds.height()), + ), + Axis::Vertical => RectF::new( + handle_origin, + vec2f(visible_bounds.width(), HANDLE_HITBOX_SIZE), + ), + }; + + let style = match self.axis { + Axis::Horizontal => CursorStyle::ResizeLeftRight, + Axis::Vertical => CursorStyle::ResizeUpDown, + }; + + cx.scene().push_cursor_region(CursorRegion { + bounds: handle_bounds, + style, + }); + + enum ResizeHandle {} + let mut mouse_region = MouseRegion::new::( + cx.view_id(), + self.basis + ix, + handle_bounds, + ); + mouse_region = mouse_region + .on_drag( + MouseButton::Left, + Self::handle_resize( + self.flexes.clone(), + self.axis, + ix, + child_start, + visible_bounds.clone(), + ), + ) + .on_click(MouseButton::Left, { + let flexes = self.flexes.clone(); + move |e, v: &mut Workspace, cx| { + if e.click_count >= 2 { + let mut borrow = flexes.borrow_mut(); + *borrow = vec![1.; borrow.len()]; + v.schedule_serialize(cx); + cx.notify(); + } + } + }); + cx.scene().push_mouse_region(mouse_region); + + cx.scene().pop_stacking_context(); + } + } + + if overflowing { + cx.scene().pop_layer(); + } + } + + fn rect_for_text_range( + &self, + range_utf16: Range, + _: RectF, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + view: &Workspace, + cx: &ViewContext, + ) -> Option { + self.children + .iter() + .find_map(|child| child.rect_for_text_range(range_utf16.clone(), view, cx)) + } + + fn debug( + &self, + bounds: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + view: &Workspace, + cx: &ViewContext, + ) -> json::Value { + serde_json::json!({ + "type": "PaneAxis", + "bounds": bounds.to_json(), + "axis": self.axis.to_json(), + "flexes": *self.flexes.borrow(), + "children": self.children.iter().map(|child| child.debug(view, cx)).collect::>() + }) + } + } +} diff --git a/crates/workspace2/src/persistence.rs b/crates/workspace2/src/persistence.rs new file mode 100644 index 0000000000..2a4062c079 --- /dev/null +++ b/crates/workspace2/src/persistence.rs @@ -0,0 +1,972 @@ +#![allow(dead_code)] + +pub mod model; + +use std::path::Path; + +use anyhow::{anyhow, bail, Context, Result}; +use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql}; +use gpui::{platform::WindowBounds, Axis}; + +use util::{unzip_option, ResultExt}; +use uuid::Uuid; + +use crate::WorkspaceId; + +use model::{ + GroupId, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace, + WorkspaceLocation, +}; + +use self::model::DockStructure; + +define_connection! { + // Current schema shape using pseudo-rust syntax: + // + // workspaces( + // workspace_id: usize, // Primary key for workspaces + // workspace_location: Bincode>, + // dock_visible: bool, // Deprecated + // dock_anchor: DockAnchor, // Deprecated + // dock_pane: Option, // Deprecated + // left_sidebar_open: boolean, + // timestamp: String, // UTC YYYY-MM-DD HH:MM:SS + // window_state: String, // WindowBounds Discriminant + // window_x: Option, // WindowBounds::Fixed RectF x + // window_y: Option, // WindowBounds::Fixed RectF y + // window_width: Option, // WindowBounds::Fixed RectF width + // window_height: Option, // WindowBounds::Fixed RectF height + // display: Option, // Display id + // ) + // + // pane_groups( + // group_id: usize, // Primary key for pane_groups + // workspace_id: usize, // References workspaces table + // parent_group_id: Option, // None indicates that this is the root node + // position: Optiopn, // None indicates that this is the root node + // axis: Option, // 'Vertical', 'Horizontal' + // flexes: Option>, // A JSON array of floats + // ) + // + // panes( + // pane_id: usize, // Primary key for panes + // workspace_id: usize, // References workspaces table + // active: bool, + // ) + // + // center_panes( + // pane_id: usize, // Primary key for center_panes + // parent_group_id: Option, // References pane_groups. If none, this is the root + // position: Option, // None indicates this is the root + // ) + // + // CREATE TABLE items( + // item_id: usize, // This is the item's view id, so this is not unique + // workspace_id: usize, // References workspaces table + // pane_id: usize, // References panes table + // kind: String, // Indicates which view this connects to. This is the key in the item_deserializers global + // position: usize, // Position of the item in the parent pane. This is equivalent to panes' position column + // active: bool, // Indicates if this item is the active one in the pane + // ) + pub static ref DB: WorkspaceDb<()> = + &[sql!( + CREATE TABLE workspaces( + workspace_id INTEGER PRIMARY KEY, + workspace_location BLOB UNIQUE, + dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed. + dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed. + dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed. + left_sidebar_open INTEGER, // Boolean + timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY(dock_pane) REFERENCES panes(pane_id) + ) STRICT; + + CREATE TABLE pane_groups( + group_id INTEGER PRIMARY KEY, + workspace_id INTEGER NOT NULL, + parent_group_id INTEGER, // NULL indicates that this is a root node + position INTEGER, // NULL indicates that this is a root node + axis TEXT NOT NULL, // Enum: 'Vertical' / 'Horizontal' + FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) + ON DELETE CASCADE + ON UPDATE CASCADE, + FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE + ) STRICT; + + CREATE TABLE panes( + pane_id INTEGER PRIMARY KEY, + workspace_id INTEGER NOT NULL, + active INTEGER NOT NULL, // Boolean + FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) + ON DELETE CASCADE + ON UPDATE CASCADE + ) STRICT; + + CREATE TABLE center_panes( + pane_id INTEGER PRIMARY KEY, + parent_group_id INTEGER, // NULL means that this is a root pane + position INTEGER, // NULL means that this is a root pane + FOREIGN KEY(pane_id) REFERENCES panes(pane_id) + ON DELETE CASCADE, + FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE + ) STRICT; + + CREATE TABLE items( + item_id INTEGER NOT NULL, // This is the item's view id, so this is not unique + workspace_id INTEGER NOT NULL, + pane_id INTEGER NOT NULL, + kind TEXT NOT NULL, + position INTEGER NOT NULL, + active INTEGER NOT NULL, + FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) + ON DELETE CASCADE + ON UPDATE CASCADE, + FOREIGN KEY(pane_id) REFERENCES panes(pane_id) + ON DELETE CASCADE, + PRIMARY KEY(item_id, workspace_id) + ) STRICT; + ), + sql!( + ALTER TABLE workspaces ADD COLUMN window_state TEXT; + ALTER TABLE workspaces ADD COLUMN window_x REAL; + ALTER TABLE workspaces ADD COLUMN window_y REAL; + ALTER TABLE workspaces ADD COLUMN window_width REAL; + ALTER TABLE workspaces ADD COLUMN window_height REAL; + ALTER TABLE workspaces ADD COLUMN display BLOB; + ), + // Drop foreign key constraint from workspaces.dock_pane to panes table. + sql!( + CREATE TABLE workspaces_2( + workspace_id INTEGER PRIMARY KEY, + workspace_location BLOB UNIQUE, + dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed. + dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed. + dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed. + left_sidebar_open INTEGER, // Boolean + timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, + window_state TEXT, + window_x REAL, + window_y REAL, + window_width REAL, + window_height REAL, + display BLOB + ) STRICT; + INSERT INTO workspaces_2 SELECT * FROM workspaces; + DROP TABLE workspaces; + ALTER TABLE workspaces_2 RENAME TO workspaces; + ), + // Add panels related information + sql!( + ALTER TABLE workspaces ADD COLUMN left_dock_visible INTEGER; //bool + ALTER TABLE workspaces ADD COLUMN left_dock_active_panel TEXT; + ALTER TABLE workspaces ADD COLUMN right_dock_visible INTEGER; //bool + ALTER TABLE workspaces ADD COLUMN right_dock_active_panel TEXT; + ALTER TABLE workspaces ADD COLUMN bottom_dock_visible INTEGER; //bool + ALTER TABLE workspaces ADD COLUMN bottom_dock_active_panel TEXT; + ), + // Add panel zoom persistence + sql!( + ALTER TABLE workspaces ADD COLUMN left_dock_zoom INTEGER; //bool + ALTER TABLE workspaces ADD COLUMN right_dock_zoom INTEGER; //bool + ALTER TABLE workspaces ADD COLUMN bottom_dock_zoom INTEGER; //bool + ), + // Add pane group flex data + sql!( + ALTER TABLE pane_groups ADD COLUMN flexes TEXT; + ) + ]; +} + +impl WorkspaceDb { + /// Returns a serialized workspace for the given worktree_roots. If the passed array + /// is empty, the most recent workspace is returned instead. If no workspace for the + /// passed roots is stored, returns none. + pub fn workspace_for_roots>( + &self, + worktree_roots: &[P], + ) -> Option { + let workspace_location: WorkspaceLocation = worktree_roots.into(); + + // Note that we re-assign the workspace_id here in case it's empty + // and we've grabbed the most recent workspace + let (workspace_id, workspace_location, bounds, display, docks): ( + WorkspaceId, + WorkspaceLocation, + Option, + Option, + DockStructure, + ) = self + .select_row_bound(sql! { + SELECT + workspace_id, + workspace_location, + window_state, + window_x, + window_y, + window_width, + window_height, + display, + left_dock_visible, + left_dock_active_panel, + left_dock_zoom, + right_dock_visible, + right_dock_active_panel, + right_dock_zoom, + bottom_dock_visible, + bottom_dock_active_panel, + bottom_dock_zoom + FROM workspaces + WHERE workspace_location = ? + }) + .and_then(|mut prepared_statement| (prepared_statement)(&workspace_location)) + .context("No workspaces found") + .warn_on_err() + .flatten()?; + + Some(SerializedWorkspace { + id: workspace_id, + location: workspace_location.clone(), + center_group: self + .get_center_pane_group(workspace_id) + .context("Getting center group") + .log_err()?, + bounds, + display, + docks, + }) + } + + /// Saves a workspace using the worktree roots. Will garbage collect any workspaces + /// that used this workspace previously + pub async fn save_workspace(&self, workspace: SerializedWorkspace) { + self.write(move |conn| { + conn.with_savepoint("update_worktrees", || { + // Clear out panes and pane_groups + conn.exec_bound(sql!( + DELETE FROM pane_groups WHERE workspace_id = ?1; + DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id) + .expect("Clearing old panes"); + + conn.exec_bound(sql!( + DELETE FROM workspaces WHERE workspace_location = ? AND workspace_id != ? + ))?((&workspace.location, workspace.id.clone())) + .context("clearing out old locations")?; + + // Upsert + conn.exec_bound(sql!( + INSERT INTO workspaces( + workspace_id, + workspace_location, + left_dock_visible, + left_dock_active_panel, + left_dock_zoom, + right_dock_visible, + right_dock_active_panel, + right_dock_zoom, + bottom_dock_visible, + bottom_dock_active_panel, + bottom_dock_zoom, + timestamp + ) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, CURRENT_TIMESTAMP) + ON CONFLICT DO + UPDATE SET + workspace_location = ?2, + left_dock_visible = ?3, + left_dock_active_panel = ?4, + left_dock_zoom = ?5, + right_dock_visible = ?6, + right_dock_active_panel = ?7, + right_dock_zoom = ?8, + bottom_dock_visible = ?9, + bottom_dock_active_panel = ?10, + bottom_dock_zoom = ?11, + timestamp = CURRENT_TIMESTAMP + ))?((workspace.id, &workspace.location, workspace.docks)) + .context("Updating workspace")?; + + // Save center pane group + Self::save_pane_group(conn, workspace.id, &workspace.center_group, None) + .context("save pane group in save workspace")?; + + Ok(()) + }) + .log_err(); + }) + .await; + } + + query! { + pub async fn next_id() -> Result { + INSERT INTO workspaces DEFAULT VALUES RETURNING workspace_id + } + } + + query! { + fn recent_workspaces() -> Result> { + SELECT workspace_id, workspace_location + FROM workspaces + WHERE workspace_location IS NOT NULL + ORDER BY timestamp DESC + } + } + + query! { + async fn delete_stale_workspace(id: WorkspaceId) -> Result<()> { + DELETE FROM workspaces + WHERE workspace_id IS ? + } + } + + // Returns the recent locations which are still valid on disk and deletes ones which no longer + // exist. + pub async fn recent_workspaces_on_disk(&self) -> Result> { + let mut result = Vec::new(); + let mut delete_tasks = Vec::new(); + for (id, location) in self.recent_workspaces()? { + if location.paths().iter().all(|path| path.exists()) + && location.paths().iter().any(|path| path.is_dir()) + { + result.push((id, location)); + } else { + delete_tasks.push(self.delete_stale_workspace(id)); + } + } + + futures::future::join_all(delete_tasks).await; + Ok(result) + } + + pub async fn last_workspace(&self) -> Result> { + Ok(self + .recent_workspaces_on_disk() + .await? + .into_iter() + .next() + .map(|(_, location)| location)) + } + + fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result { + Ok(self + .get_pane_group(workspace_id, None)? + .into_iter() + .next() + .unwrap_or_else(|| { + SerializedPaneGroup::Pane(SerializedPane { + active: true, + children: vec![], + }) + })) + } + + fn get_pane_group( + &self, + workspace_id: WorkspaceId, + group_id: Option, + ) -> Result> { + type GroupKey = (Option, WorkspaceId); + type GroupOrPane = ( + Option, + Option, + Option, + Option, + Option, + ); + self.select_bound::(sql!( + SELECT group_id, axis, pane_id, active, flexes + FROM (SELECT + group_id, + axis, + NULL as pane_id, + NULL as active, + position, + parent_group_id, + workspace_id, + flexes + FROM pane_groups + UNION + SELECT + NULL, + NULL, + center_panes.pane_id, + panes.active as active, + position, + parent_group_id, + panes.workspace_id as workspace_id, + NULL + FROM center_panes + JOIN panes ON center_panes.pane_id = panes.pane_id) + WHERE parent_group_id IS ? AND workspace_id = ? + ORDER BY position + ))?((group_id, workspace_id))? + .into_iter() + .map(|(group_id, axis, pane_id, active, flexes)| { + if let Some((group_id, axis)) = group_id.zip(axis) { + let flexes = flexes + .map(|flexes| serde_json::from_str::>(&flexes)) + .transpose()?; + + Ok(SerializedPaneGroup::Group { + axis, + children: self.get_pane_group(workspace_id, Some(group_id))?, + flexes, + }) + } else if let Some((pane_id, active)) = pane_id.zip(active) { + Ok(SerializedPaneGroup::Pane(SerializedPane::new( + self.get_items(pane_id)?, + active, + ))) + } else { + bail!("Pane Group Child was neither a pane group or a pane"); + } + }) + // Filter out panes and pane groups which don't have any children or items + .filter(|pane_group| match pane_group { + Ok(SerializedPaneGroup::Group { children, .. }) => !children.is_empty(), + Ok(SerializedPaneGroup::Pane(pane)) => !pane.children.is_empty(), + _ => true, + }) + .collect::>() + } + + fn save_pane_group( + conn: &Connection, + workspace_id: WorkspaceId, + pane_group: &SerializedPaneGroup, + parent: Option<(GroupId, usize)>, + ) -> Result<()> { + match pane_group { + SerializedPaneGroup::Group { + axis, + children, + flexes, + } => { + let (parent_id, position) = unzip_option(parent); + + let flex_string = flexes + .as_ref() + .map(|flexes| serde_json::json!(flexes).to_string()); + + let group_id = conn.select_row_bound::<_, i64>(sql!( + INSERT INTO pane_groups( + workspace_id, + parent_group_id, + position, + axis, + flexes + ) + VALUES (?, ?, ?, ?, ?) + RETURNING group_id + ))?(( + workspace_id, + parent_id, + position, + *axis, + flex_string, + ))? + .ok_or_else(|| anyhow!("Couldn't retrieve group_id from inserted pane_group"))?; + + for (position, group) in children.iter().enumerate() { + Self::save_pane_group(conn, workspace_id, group, Some((group_id, position)))? + } + + Ok(()) + } + SerializedPaneGroup::Pane(pane) => { + Self::save_pane(conn, workspace_id, &pane, parent)?; + Ok(()) + } + } + } + + fn save_pane( + conn: &Connection, + workspace_id: WorkspaceId, + pane: &SerializedPane, + parent: Option<(GroupId, usize)>, + ) -> Result { + let pane_id = conn.select_row_bound::<_, i64>(sql!( + INSERT INTO panes(workspace_id, active) + VALUES (?, ?) + RETURNING pane_id + ))?((workspace_id, pane.active))? + .ok_or_else(|| anyhow!("Could not retrieve inserted pane_id"))?; + + let (parent_id, order) = unzip_option(parent); + conn.exec_bound(sql!( + INSERT INTO center_panes(pane_id, parent_group_id, position) + VALUES (?, ?, ?) + ))?((pane_id, parent_id, order))?; + + Self::save_items(conn, workspace_id, pane_id, &pane.children).context("Saving items")?; + + Ok(pane_id) + } + + fn get_items(&self, pane_id: PaneId) -> Result> { + Ok(self.select_bound(sql!( + SELECT kind, item_id, active FROM items + WHERE pane_id = ? + ORDER BY position + ))?(pane_id)?) + } + + fn save_items( + conn: &Connection, + workspace_id: WorkspaceId, + pane_id: PaneId, + items: &[SerializedItem], + ) -> Result<()> { + let mut insert = conn.exec_bound(sql!( + INSERT INTO items(workspace_id, pane_id, position, kind, item_id, active) VALUES (?, ?, ?, ?, ?, ?) + )).context("Preparing insertion")?; + for (position, item) in items.iter().enumerate() { + insert((workspace_id, pane_id, position, item))?; + } + + Ok(()) + } + + query! { + pub async fn update_timestamp(workspace_id: WorkspaceId) -> Result<()> { + UPDATE workspaces + SET timestamp = CURRENT_TIMESTAMP + WHERE workspace_id = ? + } + } + + query! { + pub async fn set_window_bounds(workspace_id: WorkspaceId, bounds: WindowBounds, display: Uuid) -> Result<()> { + UPDATE workspaces + SET window_state = ?2, + window_x = ?3, + window_y = ?4, + window_width = ?5, + window_height = ?6, + display = ?7 + WHERE workspace_id = ?1 + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use db::open_test_db; + + #[gpui::test] + async fn test_next_id_stability() { + env_logger::try_init().ok(); + + let db = WorkspaceDb(open_test_db("test_next_id_stability").await); + + db.write(|conn| { + conn.migrate( + "test_table", + &[sql!( + CREATE TABLE test_table( + text TEXT, + workspace_id INTEGER, + FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) + ON DELETE CASCADE + ) STRICT; + )], + ) + .unwrap(); + }) + .await; + + let id = db.next_id().await.unwrap(); + // Assert the empty row got inserted + assert_eq!( + Some(id), + db.select_row_bound::(sql!( + SELECT workspace_id FROM workspaces WHERE workspace_id = ? + )) + .unwrap()(id) + .unwrap() + ); + + db.write(move |conn| { + conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?))) + .unwrap()(("test-text-1", id)) + .unwrap() + }) + .await; + + let test_text_1 = db + .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?)) + .unwrap()(1) + .unwrap() + .unwrap(); + assert_eq!(test_text_1, "test-text-1"); + } + + #[gpui::test] + async fn test_workspace_id_stability() { + env_logger::try_init().ok(); + + let db = WorkspaceDb(open_test_db("test_workspace_id_stability").await); + + db.write(|conn| { + conn.migrate( + "test_table", + &[sql!( + CREATE TABLE test_table( + text TEXT, + workspace_id INTEGER, + FOREIGN KEY(workspace_id) + REFERENCES workspaces(workspace_id) + ON DELETE CASCADE + ) STRICT;)], + ) + }) + .await + .unwrap(); + + let mut workspace_1 = SerializedWorkspace { + id: 1, + location: (["/tmp", "/tmp2"]).into(), + center_group: Default::default(), + bounds: Default::default(), + display: Default::default(), + docks: Default::default(), + }; + + let workspace_2 = SerializedWorkspace { + id: 2, + location: (["/tmp"]).into(), + center_group: Default::default(), + bounds: Default::default(), + display: Default::default(), + docks: Default::default(), + }; + + db.save_workspace(workspace_1.clone()).await; + + db.write(|conn| { + conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?))) + .unwrap()(("test-text-1", 1)) + .unwrap(); + }) + .await; + + db.save_workspace(workspace_2.clone()).await; + + db.write(|conn| { + conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?))) + .unwrap()(("test-text-2", 2)) + .unwrap(); + }) + .await; + + workspace_1.location = (["/tmp", "/tmp3"]).into(); + db.save_workspace(workspace_1.clone()).await; + db.save_workspace(workspace_1).await; + db.save_workspace(workspace_2).await; + + let test_text_2 = db + .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?)) + .unwrap()(2) + .unwrap() + .unwrap(); + assert_eq!(test_text_2, "test-text-2"); + + let test_text_1 = db + .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?)) + .unwrap()(1) + .unwrap() + .unwrap(); + assert_eq!(test_text_1, "test-text-1"); + } + + fn group(axis: gpui::Axis, children: Vec) -> SerializedPaneGroup { + SerializedPaneGroup::Group { + axis, + flexes: None, + children, + } + } + + #[gpui::test] + async fn test_full_workspace_serialization() { + env_logger::try_init().ok(); + + let db = WorkspaceDb(open_test_db("test_full_workspace_serialization").await); + + // ----------------- + // | 1,2 | 5,6 | + // | - - - | | + // | 3,4 | | + // ----------------- + let center_group = group( + gpui::Axis::Horizontal, + vec![ + group( + gpui::Axis::Vertical, + vec![ + SerializedPaneGroup::Pane(SerializedPane::new( + vec![ + SerializedItem::new("Terminal", 5, false), + SerializedItem::new("Terminal", 6, true), + ], + false, + )), + SerializedPaneGroup::Pane(SerializedPane::new( + vec![ + SerializedItem::new("Terminal", 7, true), + SerializedItem::new("Terminal", 8, false), + ], + false, + )), + ], + ), + SerializedPaneGroup::Pane(SerializedPane::new( + vec![ + SerializedItem::new("Terminal", 9, false), + SerializedItem::new("Terminal", 10, true), + ], + false, + )), + ], + ); + + let workspace = SerializedWorkspace { + id: 5, + location: (["/tmp", "/tmp2"]).into(), + center_group, + bounds: Default::default(), + display: Default::default(), + docks: Default::default(), + }; + + db.save_workspace(workspace.clone()).await; + let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]); + + assert_eq!(workspace, round_trip_workspace.unwrap()); + + // Test guaranteed duplicate IDs + db.save_workspace(workspace.clone()).await; + db.save_workspace(workspace.clone()).await; + + let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]); + assert_eq!(workspace, round_trip_workspace.unwrap()); + } + + #[gpui::test] + async fn test_workspace_assignment() { + env_logger::try_init().ok(); + + let db = WorkspaceDb(open_test_db("test_basic_functionality").await); + + let workspace_1 = SerializedWorkspace { + id: 1, + location: (["/tmp", "/tmp2"]).into(), + center_group: Default::default(), + bounds: Default::default(), + display: Default::default(), + docks: Default::default(), + }; + + let mut workspace_2 = SerializedWorkspace { + id: 2, + location: (["/tmp"]).into(), + center_group: Default::default(), + bounds: Default::default(), + display: Default::default(), + docks: Default::default(), + }; + + db.save_workspace(workspace_1.clone()).await; + db.save_workspace(workspace_2.clone()).await; + + // Test that paths are treated as a set + assert_eq!( + db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(), + workspace_1 + ); + assert_eq!( + db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(), + workspace_1 + ); + + // Make sure that other keys work + assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2); + assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None); + + // Test 'mutate' case of updating a pre-existing id + workspace_2.location = (["/tmp", "/tmp2"]).into(); + + db.save_workspace(workspace_2.clone()).await; + assert_eq!( + db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(), + workspace_2 + ); + + // Test other mechanism for mutating + let mut workspace_3 = SerializedWorkspace { + id: 3, + location: (&["/tmp", "/tmp2"]).into(), + center_group: Default::default(), + bounds: Default::default(), + display: Default::default(), + docks: Default::default(), + }; + + db.save_workspace(workspace_3.clone()).await; + assert_eq!( + db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(), + workspace_3 + ); + + // Make sure that updating paths differently also works + workspace_3.location = (["/tmp3", "/tmp4", "/tmp2"]).into(); + db.save_workspace(workspace_3.clone()).await; + assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None); + assert_eq!( + db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"]) + .unwrap(), + workspace_3 + ); + } + + use crate::persistence::model::SerializedWorkspace; + use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup}; + + fn default_workspace>( + workspace_id: &[P], + center_group: &SerializedPaneGroup, + ) -> SerializedWorkspace { + SerializedWorkspace { + id: 4, + location: workspace_id.into(), + center_group: center_group.clone(), + bounds: Default::default(), + display: Default::default(), + docks: Default::default(), + } + } + + #[gpui::test] + async fn test_simple_split() { + env_logger::try_init().ok(); + + let db = WorkspaceDb(open_test_db("simple_split").await); + + // ----------------- + // | 1,2 | 5,6 | + // | - - - | | + // | 3,4 | | + // ----------------- + let center_pane = group( + gpui::Axis::Horizontal, + vec![ + group( + gpui::Axis::Vertical, + vec![ + SerializedPaneGroup::Pane(SerializedPane::new( + vec![ + SerializedItem::new("Terminal", 1, false), + SerializedItem::new("Terminal", 2, true), + ], + false, + )), + SerializedPaneGroup::Pane(SerializedPane::new( + vec![ + SerializedItem::new("Terminal", 4, false), + SerializedItem::new("Terminal", 3, true), + ], + true, + )), + ], + ), + SerializedPaneGroup::Pane(SerializedPane::new( + vec![ + SerializedItem::new("Terminal", 5, true), + SerializedItem::new("Terminal", 6, false), + ], + false, + )), + ], + ); + + let workspace = default_workspace(&["/tmp"], ¢er_pane); + + db.save_workspace(workspace.clone()).await; + + let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap(); + + assert_eq!(workspace.center_group, new_workspace.center_group); + } + + #[gpui::test] + async fn test_cleanup_panes() { + env_logger::try_init().ok(); + + let db = WorkspaceDb(open_test_db("test_cleanup_panes").await); + + let center_pane = group( + gpui::Axis::Horizontal, + vec![ + group( + gpui::Axis::Vertical, + vec![ + SerializedPaneGroup::Pane(SerializedPane::new( + vec![ + SerializedItem::new("Terminal", 1, false), + SerializedItem::new("Terminal", 2, true), + ], + false, + )), + SerializedPaneGroup::Pane(SerializedPane::new( + vec![ + SerializedItem::new("Terminal", 4, false), + SerializedItem::new("Terminal", 3, true), + ], + true, + )), + ], + ), + SerializedPaneGroup::Pane(SerializedPane::new( + vec![ + SerializedItem::new("Terminal", 5, false), + SerializedItem::new("Terminal", 6, true), + ], + false, + )), + ], + ); + + let id = &["/tmp"]; + + let mut workspace = default_workspace(id, ¢er_pane); + + db.save_workspace(workspace.clone()).await; + + workspace.center_group = group( + gpui::Axis::Vertical, + vec![ + SerializedPaneGroup::Pane(SerializedPane::new( + vec![ + SerializedItem::new("Terminal", 1, false), + SerializedItem::new("Terminal", 2, true), + ], + false, + )), + SerializedPaneGroup::Pane(SerializedPane::new( + vec![ + SerializedItem::new("Terminal", 4, true), + SerializedItem::new("Terminal", 3, false), + ], + true, + )), + ], + ); + + db.save_workspace(workspace.clone()).await; + + let new_workspace = db.workspace_for_roots(id).unwrap(); + + assert_eq!(workspace.center_group, new_workspace.center_group); + } +} diff --git a/crates/workspace2/src/persistence/model.rs b/crates/workspace2/src/persistence/model.rs new file mode 100644 index 0000000000..5f4c29cd5b --- /dev/null +++ b/crates/workspace2/src/persistence/model.rs @@ -0,0 +1,344 @@ +use crate::{item::ItemHandle, ItemDeserializers, Member, Pane, PaneAxis, Workspace, WorkspaceId}; +use anyhow::{Context, Result}; +use async_recursion::async_recursion; +use db::sqlez::{ + bindable::{Bind, Column, StaticColumnCount}, + statement::Statement, +}; +use gpui::{ + platform::WindowBounds, AsyncAppContext, Axis, ModelHandle, Task, ViewHandle, WeakViewHandle, +}; +use project::Project; +use std::{ + path::{Path, PathBuf}, + sync::Arc, +}; +use util::ResultExt; +use uuid::Uuid; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct WorkspaceLocation(Arc>); + +impl WorkspaceLocation { + pub fn paths(&self) -> Arc> { + self.0.clone() + } +} + +impl, T: IntoIterator> From for WorkspaceLocation { + fn from(iterator: T) -> Self { + let mut roots = iterator + .into_iter() + .map(|p| p.as_ref().to_path_buf()) + .collect::>(); + roots.sort(); + Self(Arc::new(roots)) + } +} + +impl StaticColumnCount for WorkspaceLocation {} +impl Bind for &WorkspaceLocation { + fn bind(&self, statement: &Statement, start_index: i32) -> Result { + bincode::serialize(&self.0) + .expect("Bincode serialization of paths should not fail") + .bind(statement, start_index) + } +} + +impl Column for WorkspaceLocation { + fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { + let blob = statement.column_blob(start_index)?; + Ok(( + WorkspaceLocation(bincode::deserialize(blob).context("Bincode failed")?), + start_index + 1, + )) + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct SerializedWorkspace { + pub id: WorkspaceId, + pub location: WorkspaceLocation, + pub center_group: SerializedPaneGroup, + pub bounds: Option, + pub display: Option, + pub docks: DockStructure, +} + +#[derive(Debug, PartialEq, Clone, Default)] +pub struct DockStructure { + pub(crate) left: DockData, + pub(crate) right: DockData, + pub(crate) bottom: DockData, +} + +impl Column for DockStructure { + fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { + let (left, next_index) = DockData::column(statement, start_index)?; + let (right, next_index) = DockData::column(statement, next_index)?; + let (bottom, next_index) = DockData::column(statement, next_index)?; + Ok(( + DockStructure { + left, + right, + bottom, + }, + next_index, + )) + } +} + +impl Bind for DockStructure { + fn bind(&self, statement: &Statement, start_index: i32) -> Result { + let next_index = statement.bind(&self.left, start_index)?; + let next_index = statement.bind(&self.right, next_index)?; + statement.bind(&self.bottom, next_index) + } +} + +#[derive(Debug, PartialEq, Clone, Default)] +pub struct DockData { + pub(crate) visible: bool, + pub(crate) active_panel: Option, + pub(crate) zoom: bool, +} + +impl Column for DockData { + fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { + let (visible, next_index) = Option::::column(statement, start_index)?; + let (active_panel, next_index) = Option::::column(statement, next_index)?; + let (zoom, next_index) = Option::::column(statement, next_index)?; + Ok(( + DockData { + visible: visible.unwrap_or(false), + active_panel, + zoom: zoom.unwrap_or(false), + }, + next_index, + )) + } +} + +impl Bind for DockData { + fn bind(&self, statement: &Statement, start_index: i32) -> Result { + let next_index = statement.bind(&self.visible, start_index)?; + let next_index = statement.bind(&self.active_panel, next_index)?; + statement.bind(&self.zoom, next_index) + } +} + +#[derive(Debug, PartialEq, Clone)] +pub enum SerializedPaneGroup { + Group { + axis: Axis, + flexes: Option>, + children: Vec, + }, + Pane(SerializedPane), +} + +#[cfg(test)] +impl Default for SerializedPaneGroup { + fn default() -> Self { + Self::Pane(SerializedPane { + children: vec![SerializedItem::default()], + active: false, + }) + } +} + +impl SerializedPaneGroup { + #[async_recursion(?Send)] + pub(crate) async fn deserialize( + self, + project: &ModelHandle, + workspace_id: WorkspaceId, + workspace: &WeakViewHandle, + cx: &mut AsyncAppContext, + ) -> Option<( + Member, + Option>, + Vec>>, + )> { + match self { + SerializedPaneGroup::Group { + axis, + children, + flexes, + } => { + let mut current_active_pane = None; + let mut members = Vec::new(); + let mut items = Vec::new(); + for child in children { + if let Some((new_member, active_pane, new_items)) = child + .deserialize(project, workspace_id, workspace, cx) + .await + { + members.push(new_member); + items.extend(new_items); + current_active_pane = current_active_pane.or(active_pane); + } + } + + if members.is_empty() { + return None; + } + + if members.len() == 1 { + return Some((members.remove(0), current_active_pane, items)); + } + + Some(( + Member::Axis(PaneAxis::load(axis, members, flexes)), + current_active_pane, + items, + )) + } + SerializedPaneGroup::Pane(serialized_pane) => { + let pane = workspace + .update(cx, |workspace, cx| workspace.add_pane(cx).downgrade()) + .log_err()?; + let active = serialized_pane.active; + let new_items = serialized_pane + .deserialize_to(project, &pane, workspace_id, workspace, cx) + .await + .log_err()?; + + if pane + .read_with(cx, |pane, _| pane.items_len() != 0) + .log_err()? + { + let pane = pane.upgrade(cx)?; + Some((Member::Pane(pane.clone()), active.then(|| pane), new_items)) + } else { + let pane = pane.upgrade(cx)?; + workspace + .update(cx, |workspace, cx| workspace.force_remove_pane(&pane, cx)) + .log_err()?; + None + } + } + } + } +} + +#[derive(Debug, PartialEq, Eq, Default, Clone)] +pub struct SerializedPane { + pub(crate) active: bool, + pub(crate) children: Vec, +} + +impl SerializedPane { + pub fn new(children: Vec, active: bool) -> Self { + SerializedPane { children, active } + } + + pub async fn deserialize_to( + &self, + project: &ModelHandle, + pane: &WeakViewHandle, + workspace_id: WorkspaceId, + workspace: &WeakViewHandle, + cx: &mut AsyncAppContext, + ) -> Result>>> { + let mut items = Vec::new(); + let mut active_item_index = None; + for (index, item) in self.children.iter().enumerate() { + let project = project.clone(); + let item_handle = pane + .update(cx, |_, cx| { + if let Some(deserializer) = cx.global::().get(&item.kind) { + deserializer(project, workspace.clone(), workspace_id, item.item_id, cx) + } else { + Task::ready(Err(anyhow::anyhow!( + "Deserializer does not exist for item kind: {}", + item.kind + ))) + } + })? + .await + .log_err(); + + items.push(item_handle.clone()); + + if let Some(item_handle) = item_handle { + pane.update(cx, |pane, cx| { + pane.add_item(item_handle.clone(), true, true, None, cx); + })?; + } + + if item.active { + active_item_index = Some(index); + } + } + + if let Some(active_item_index) = active_item_index { + pane.update(cx, |pane, cx| { + pane.activate_item(active_item_index, false, false, cx); + })?; + } + + anyhow::Ok(items) + } +} + +pub type GroupId = i64; +pub type PaneId = i64; +pub type ItemId = usize; + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct SerializedItem { + pub kind: Arc, + pub item_id: ItemId, + pub active: bool, +} + +impl SerializedItem { + pub fn new(kind: impl AsRef, item_id: ItemId, active: bool) -> Self { + Self { + kind: Arc::from(kind.as_ref()), + item_id, + active, + } + } +} + +#[cfg(test)] +impl Default for SerializedItem { + fn default() -> Self { + SerializedItem { + kind: Arc::from("Terminal"), + item_id: 100000, + active: false, + } + } +} + +impl StaticColumnCount for SerializedItem { + fn column_count() -> usize { + 3 + } +} +impl Bind for &SerializedItem { + fn bind(&self, statement: &Statement, start_index: i32) -> Result { + let next_index = statement.bind(&self.kind, start_index)?; + let next_index = statement.bind(&self.item_id, next_index)?; + statement.bind(&self.active, next_index) + } +} + +impl Column for SerializedItem { + fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { + let (kind, next_index) = Arc::::column(statement, start_index)?; + let (item_id, next_index) = ItemId::column(statement, next_index)?; + let (active, next_index) = bool::column(statement, next_index)?; + Ok(( + SerializedItem { + kind, + item_id, + active, + }, + next_index, + )) + } +} diff --git a/crates/workspace2/src/searchable.rs b/crates/workspace2/src/searchable.rs new file mode 100644 index 0000000000..ddde5c3554 --- /dev/null +++ b/crates/workspace2/src/searchable.rs @@ -0,0 +1,282 @@ +use std::{any::Any, sync::Arc}; + +use gpui::{ + AnyViewHandle, AnyWeakViewHandle, AppContext, Subscription, Task, ViewContext, ViewHandle, + WeakViewHandle, WindowContext, +}; +use project::search::SearchQuery; + +use crate::{item::WeakItemHandle, Item, ItemHandle}; + +#[derive(Debug)] +pub enum SearchEvent { + MatchesInvalidated, + ActiveMatchChanged, +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Direction { + Prev, + Next, +} + +#[derive(Clone, Copy, Debug, Default)] +pub struct SearchOptions { + pub case: bool, + pub word: bool, + pub regex: bool, + /// Specifies whether the item supports search & replace. + pub replacement: bool, +} + +pub trait SearchableItem: Item { + type Match: Any + Sync + Send + Clone; + + fn supported_options() -> SearchOptions { + SearchOptions { + case: true, + word: true, + regex: true, + replacement: true, + } + } + fn to_search_event( + &mut self, + event: &Self::Event, + cx: &mut ViewContext, + ) -> Option; + fn clear_matches(&mut self, cx: &mut ViewContext); + fn update_matches(&mut self, matches: Vec, cx: &mut ViewContext); + fn query_suggestion(&mut self, cx: &mut ViewContext) -> String; + fn activate_match( + &mut self, + index: usize, + matches: Vec, + cx: &mut ViewContext, + ); + fn select_matches(&mut self, matches: Vec, cx: &mut ViewContext); + fn replace(&mut self, _: &Self::Match, _: &SearchQuery, _: &mut ViewContext); + fn match_index_for_direction( + &mut self, + matches: &Vec, + current_index: usize, + direction: Direction, + count: usize, + _: &mut ViewContext, + ) -> usize { + match direction { + Direction::Prev => { + let count = count % matches.len(); + if current_index >= count { + current_index - count + } else { + matches.len() - (count - current_index) + } + } + Direction::Next => (current_index + count) % matches.len(), + } + } + fn find_matches( + &mut self, + query: Arc, + cx: &mut ViewContext, + ) -> Task>; + fn active_match_index( + &mut self, + matches: Vec, + cx: &mut ViewContext, + ) -> Option; +} + +pub trait SearchableItemHandle: ItemHandle { + fn downgrade(&self) -> Box; + fn boxed_clone(&self) -> Box; + fn supported_options(&self) -> SearchOptions; + fn subscribe_to_search_events( + &self, + cx: &mut WindowContext, + handler: Box, + ) -> Subscription; + fn clear_matches(&self, cx: &mut WindowContext); + fn update_matches(&self, matches: &Vec>, cx: &mut WindowContext); + fn query_suggestion(&self, cx: &mut WindowContext) -> String; + fn activate_match( + &self, + index: usize, + matches: &Vec>, + cx: &mut WindowContext, + ); + fn select_matches(&self, matches: &Vec>, cx: &mut WindowContext); + fn replace(&self, _: &Box, _: &SearchQuery, _: &mut WindowContext); + fn match_index_for_direction( + &self, + matches: &Vec>, + current_index: usize, + direction: Direction, + count: usize, + cx: &mut WindowContext, + ) -> usize; + fn find_matches( + &self, + query: Arc, + cx: &mut WindowContext, + ) -> Task>>; + fn active_match_index( + &self, + matches: &Vec>, + cx: &mut WindowContext, + ) -> Option; +} + +impl SearchableItemHandle for ViewHandle { + fn downgrade(&self) -> Box { + Box::new(self.downgrade()) + } + + fn boxed_clone(&self) -> Box { + Box::new(self.clone()) + } + + fn supported_options(&self) -> SearchOptions { + T::supported_options() + } + + fn subscribe_to_search_events( + &self, + cx: &mut WindowContext, + handler: Box, + ) -> Subscription { + cx.subscribe(self, move |handle, event, cx| { + let search_event = handle.update(cx, |handle, cx| handle.to_search_event(event, cx)); + if let Some(search_event) = search_event { + handler(search_event, cx) + } + }) + } + + fn clear_matches(&self, cx: &mut WindowContext) { + self.update(cx, |this, cx| this.clear_matches(cx)); + } + fn update_matches(&self, matches: &Vec>, cx: &mut WindowContext) { + let matches = downcast_matches(matches); + self.update(cx, |this, cx| this.update_matches(matches, cx)); + } + fn query_suggestion(&self, cx: &mut WindowContext) -> String { + self.update(cx, |this, cx| this.query_suggestion(cx)) + } + fn activate_match( + &self, + index: usize, + matches: &Vec>, + cx: &mut WindowContext, + ) { + let matches = downcast_matches(matches); + self.update(cx, |this, cx| this.activate_match(index, matches, cx)); + } + + fn select_matches(&self, matches: &Vec>, cx: &mut WindowContext) { + let matches = downcast_matches(matches); + self.update(cx, |this, cx| this.select_matches(matches, cx)); + } + + fn match_index_for_direction( + &self, + matches: &Vec>, + current_index: usize, + direction: Direction, + count: usize, + cx: &mut WindowContext, + ) -> usize { + let matches = downcast_matches(matches); + self.update(cx, |this, cx| { + this.match_index_for_direction(&matches, current_index, direction, count, cx) + }) + } + fn find_matches( + &self, + query: Arc, + cx: &mut WindowContext, + ) -> Task>> { + let matches = self.update(cx, |this, cx| this.find_matches(query, cx)); + cx.foreground().spawn(async { + let matches = matches.await; + matches + .into_iter() + .map::, _>(|range| Box::new(range)) + .collect() + }) + } + fn active_match_index( + &self, + matches: &Vec>, + cx: &mut WindowContext, + ) -> Option { + let matches = downcast_matches(matches); + self.update(cx, |this, cx| this.active_match_index(matches, cx)) + } + + fn replace(&self, matches: &Box, query: &SearchQuery, cx: &mut WindowContext) { + let matches = matches.downcast_ref().unwrap(); + self.update(cx, |this, cx| this.replace(matches, query, cx)) + } +} + +fn downcast_matches(matches: &Vec>) -> Vec { + matches + .iter() + .map(|range| range.downcast_ref::().cloned()) + .collect::>>() + .expect( + "SearchableItemHandle function called with vec of matches of a different type than expected", + ) +} + +impl From> for AnyViewHandle { + fn from(this: Box) -> Self { + this.as_any().clone() + } +} + +impl From<&Box> for AnyViewHandle { + fn from(this: &Box) -> Self { + this.as_any().clone() + } +} + +impl PartialEq for Box { + fn eq(&self, other: &Self) -> bool { + self.id() == other.id() && self.window() == other.window() + } +} + +impl Eq for Box {} + +pub trait WeakSearchableItemHandle: WeakItemHandle { + fn upgrade(&self, cx: &AppContext) -> Option>; + + fn into_any(self) -> AnyWeakViewHandle; +} + +impl WeakSearchableItemHandle for WeakViewHandle { + fn upgrade(&self, cx: &AppContext) -> Option> { + Some(Box::new(self.upgrade(cx)?)) + } + + fn into_any(self) -> AnyWeakViewHandle { + self.into_any() + } +} + +impl PartialEq for Box { + fn eq(&self, other: &Self) -> bool { + self.id() == other.id() && self.window() == other.window() + } +} + +impl Eq for Box {} + +impl std::hash::Hash for Box { + fn hash(&self, state: &mut H) { + (self.id(), self.window().id()).hash(state) + } +} diff --git a/crates/workspace2/src/shared_screen.rs b/crates/workspace2/src/shared_screen.rs new file mode 100644 index 0000000000..b99c5f3ab9 --- /dev/null +++ b/crates/workspace2/src/shared_screen.rs @@ -0,0 +1,151 @@ +use crate::{ + item::{Item, ItemEvent}, + ItemNavHistory, WorkspaceId, +}; +use anyhow::Result; +use call::participant::{Frame, RemoteVideoTrack}; +use client::{proto::PeerId, User}; +use futures::StreamExt; +use gpui::{ + elements::*, + geometry::{rect::RectF, vector::vec2f}, + platform::MouseButton, + AppContext, Entity, Task, View, ViewContext, +}; +use smallvec::SmallVec; +use std::{ + borrow::Cow, + sync::{Arc, Weak}, +}; + +pub enum Event { + Close, +} + +pub struct SharedScreen { + track: Weak, + frame: Option, + pub peer_id: PeerId, + user: Arc, + nav_history: Option, + _maintain_frame: Task>, +} + +impl SharedScreen { + pub fn new( + track: &Arc, + peer_id: PeerId, + user: Arc, + cx: &mut ViewContext, + ) -> Self { + let mut frames = track.frames(); + Self { + track: Arc::downgrade(track), + frame: None, + peer_id, + user, + nav_history: Default::default(), + _maintain_frame: cx.spawn(|this, mut cx| async move { + while let Some(frame) = frames.next().await { + this.update(&mut cx, |this, cx| { + this.frame = Some(frame); + cx.notify(); + })?; + } + this.update(&mut cx, |_, cx| cx.emit(Event::Close))?; + Ok(()) + }), + } + } +} + +impl Entity for SharedScreen { + type Event = Event; +} + +impl View for SharedScreen { + fn ui_name() -> &'static str { + "SharedScreen" + } + + fn render(&mut self, cx: &mut ViewContext) -> AnyElement { + enum Focus {} + + let frame = self.frame.clone(); + MouseEventHandler::new::(0, cx, |_, cx| { + Canvas::new(move |bounds, _, _, cx| { + if let Some(frame) = frame.clone() { + let size = constrain_size_preserving_aspect_ratio( + bounds.size(), + vec2f(frame.width() as f32, frame.height() as f32), + ); + let origin = bounds.origin() + (bounds.size() / 2.) - size / 2.; + cx.scene().push_surface(gpui::platform::mac::Surface { + bounds: RectF::new(origin, size), + image_buffer: frame.image(), + }); + } + }) + .contained() + .with_style(theme::current(cx).shared_screen) + }) + .on_down(MouseButton::Left, |_, _, cx| cx.focus_parent()) + .into_any() + } +} + +impl Item for SharedScreen { + fn tab_tooltip_text(&self, _: &AppContext) -> Option> { + Some(format!("{}'s screen", self.user.github_login).into()) + } + fn deactivated(&mut self, cx: &mut ViewContext) { + if let Some(nav_history) = self.nav_history.as_mut() { + nav_history.push::<()>(None, cx); + } + } + + fn tab_content( + &self, + _: Option, + style: &theme::Tab, + _: &AppContext, + ) -> gpui::AnyElement { + Flex::row() + .with_child( + Svg::new("icons/desktop.svg") + .with_color(style.label.text.color) + .constrained() + .with_width(style.type_icon_width) + .aligned() + .contained() + .with_margin_right(style.spacing), + ) + .with_child( + Label::new( + format!("{}'s screen", self.user.github_login), + style.label.clone(), + ) + .aligned(), + ) + .into_any() + } + + fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext) { + self.nav_history = Some(history); + } + + fn clone_on_split( + &self, + _workspace_id: WorkspaceId, + cx: &mut ViewContext, + ) -> Option { + let track = self.track.upgrade()?; + Some(Self::new(&track, self.peer_id, self.user.clone(), cx)) + } + + fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> { + match event { + Event::Close => smallvec::smallvec!(ItemEvent::CloseItem), + } + } +} diff --git a/crates/workspace2/src/status_bar.rs b/crates/workspace2/src/status_bar.rs new file mode 100644 index 0000000000..b62dae2114 --- /dev/null +++ b/crates/workspace2/src/status_bar.rs @@ -0,0 +1,271 @@ +use std::ops::Range; + +use crate::{ItemHandle, Pane}; +use gpui::{ + elements::*, + geometry::{ + rect::RectF, + vector::{vec2f, Vector2F}, + }, + json::{json, ToJson}, + AnyElement, AnyViewHandle, Entity, SizeConstraint, Subscription, View, ViewContext, ViewHandle, + WindowContext, +}; + +pub trait StatusItemView: View { + fn set_active_pane_item( + &mut self, + active_pane_item: Option<&dyn crate::ItemHandle>, + cx: &mut ViewContext, + ); +} + +trait StatusItemViewHandle { + fn as_any(&self) -> &AnyViewHandle; + fn set_active_pane_item( + &self, + active_pane_item: Option<&dyn ItemHandle>, + cx: &mut WindowContext, + ); + fn ui_name(&self) -> &'static str; +} + +pub struct StatusBar { + left_items: Vec>, + right_items: Vec>, + active_pane: ViewHandle, + _observe_active_pane: Subscription, +} + +impl Entity for StatusBar { + type Event = (); +} + +impl View for StatusBar { + fn ui_name() -> &'static str { + "StatusBar" + } + + fn render(&mut self, cx: &mut ViewContext) -> AnyElement { + let theme = &theme::current(cx).workspace.status_bar; + + StatusBarElement { + left: Flex::row() + .with_children(self.left_items.iter().map(|i| { + ChildView::new(i.as_any(), cx) + .aligned() + .contained() + .with_margin_right(theme.item_spacing) + })) + .into_any(), + right: Flex::row() + .with_children(self.right_items.iter().rev().map(|i| { + ChildView::new(i.as_any(), cx) + .aligned() + .contained() + .with_margin_left(theme.item_spacing) + })) + .into_any(), + } + .contained() + .with_style(theme.container) + .constrained() + .with_height(theme.height) + .into_any() + } +} + +impl StatusBar { + pub fn new(active_pane: &ViewHandle, cx: &mut ViewContext) -> Self { + let mut this = Self { + left_items: Default::default(), + right_items: Default::default(), + active_pane: active_pane.clone(), + _observe_active_pane: cx + .observe(active_pane, |this, _, cx| this.update_active_pane_item(cx)), + }; + this.update_active_pane_item(cx); + this + } + + pub fn add_left_item(&mut self, item: ViewHandle, cx: &mut ViewContext) + where + T: 'static + StatusItemView, + { + self.left_items.push(Box::new(item)); + cx.notify(); + } + + pub fn item_of_type(&self) -> Option> { + self.left_items + .iter() + .chain(self.right_items.iter()) + .find_map(|item| item.as_any().clone().downcast()) + } + + pub fn position_of_item(&self) -> Option + where + T: StatusItemView, + { + for (index, item) in self.left_items.iter().enumerate() { + if item.as_ref().ui_name() == T::ui_name() { + return Some(index); + } + } + for (index, item) in self.right_items.iter().enumerate() { + if item.as_ref().ui_name() == T::ui_name() { + return Some(index + self.left_items.len()); + } + } + return None; + } + + pub fn insert_item_after( + &mut self, + position: usize, + item: ViewHandle, + cx: &mut ViewContext, + ) where + T: 'static + StatusItemView, + { + if position < self.left_items.len() { + self.left_items.insert(position + 1, Box::new(item)) + } else { + self.right_items + .insert(position + 1 - self.left_items.len(), Box::new(item)) + } + cx.notify() + } + + pub fn remove_item_at(&mut self, position: usize, cx: &mut ViewContext) { + if position < self.left_items.len() { + self.left_items.remove(position); + } else { + self.right_items.remove(position - self.left_items.len()); + } + cx.notify(); + } + + pub fn add_right_item(&mut self, item: ViewHandle, cx: &mut ViewContext) + where + T: 'static + StatusItemView, + { + self.right_items.push(Box::new(item)); + cx.notify(); + } + + pub fn set_active_pane(&mut self, active_pane: &ViewHandle, cx: &mut ViewContext) { + self.active_pane = active_pane.clone(); + self._observe_active_pane = + cx.observe(active_pane, |this, _, cx| this.update_active_pane_item(cx)); + self.update_active_pane_item(cx); + } + + fn update_active_pane_item(&mut self, cx: &mut ViewContext) { + let active_pane_item = self.active_pane.read(cx).active_item(); + for item in self.left_items.iter().chain(&self.right_items) { + item.set_active_pane_item(active_pane_item.as_deref(), cx); + } + } +} + +impl StatusItemViewHandle for ViewHandle { + fn as_any(&self) -> &AnyViewHandle { + self + } + + fn set_active_pane_item( + &self, + active_pane_item: Option<&dyn ItemHandle>, + cx: &mut WindowContext, + ) { + self.update(cx, |this, cx| { + this.set_active_pane_item(active_pane_item, cx) + }); + } + + fn ui_name(&self) -> &'static str { + T::ui_name() + } +} + +impl From<&dyn StatusItemViewHandle> for AnyViewHandle { + fn from(val: &dyn StatusItemViewHandle) -> Self { + val.as_any().clone() + } +} + +struct StatusBarElement { + left: AnyElement, + right: AnyElement, +} + +impl Element for StatusBarElement { + type LayoutState = (); + type PaintState = (); + + fn layout( + &mut self, + mut constraint: SizeConstraint, + view: &mut StatusBar, + cx: &mut ViewContext, + ) -> (Vector2F, Self::LayoutState) { + let max_width = constraint.max.x(); + constraint.min = vec2f(0., constraint.min.y()); + + let right_size = self.right.layout(constraint, view, cx); + let constraint = SizeConstraint::new( + vec2f(0., constraint.min.y()), + vec2f(max_width - right_size.x(), constraint.max.y()), + ); + + self.left.layout(constraint, view, cx); + + (vec2f(max_width, right_size.y()), ()) + } + + fn paint( + &mut self, + bounds: RectF, + visible_bounds: RectF, + _: &mut Self::LayoutState, + view: &mut StatusBar, + cx: &mut ViewContext, + ) -> Self::PaintState { + let origin_y = bounds.upper_right().y(); + let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default(); + + let left_origin = vec2f(bounds.lower_left().x(), origin_y); + self.left.paint(left_origin, visible_bounds, view, cx); + + let right_origin = vec2f(bounds.upper_right().x() - self.right.size().x(), origin_y); + self.right.paint(right_origin, visible_bounds, view, cx); + } + + fn rect_for_text_range( + &self, + _: Range, + _: RectF, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + _: &StatusBar, + _: &ViewContext, + ) -> Option { + None + } + + fn debug( + &self, + bounds: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + _: &StatusBar, + _: &ViewContext, + ) -> serde_json::Value { + json!({ + "type": "StatusBarElement", + "bounds": bounds.to_json() + }) + } +} diff --git a/crates/workspace2/src/toolbar.rs b/crates/workspace2/src/toolbar.rs new file mode 100644 index 0000000000..c3f4bb9723 --- /dev/null +++ b/crates/workspace2/src/toolbar.rs @@ -0,0 +1,301 @@ +use crate::ItemHandle; +use gpui::{ + elements::*, AnyElement, AnyViewHandle, AppContext, Entity, View, ViewContext, ViewHandle, + WindowContext, +}; + +pub trait ToolbarItemView: View { + fn set_active_pane_item( + &mut self, + active_pane_item: Option<&dyn crate::ItemHandle>, + cx: &mut ViewContext, + ) -> ToolbarItemLocation; + + fn location_for_event( + &self, + _event: &Self::Event, + current_location: ToolbarItemLocation, + _cx: &AppContext, + ) -> ToolbarItemLocation { + current_location + } + + fn pane_focus_update(&mut self, _pane_focused: bool, _cx: &mut ViewContext) {} + + /// Number of times toolbar's height will be repeated to get the effective height. + /// Useful when multiple rows one under each other are needed. + /// The rows have the same width and act as a whole when reacting to resizes and similar events. + fn row_count(&self, _cx: &ViewContext) -> usize { + 1 + } +} + +trait ToolbarItemViewHandle { + fn id(&self) -> usize; + fn as_any(&self) -> &AnyViewHandle; + fn set_active_pane_item( + &self, + active_pane_item: Option<&dyn ItemHandle>, + cx: &mut WindowContext, + ) -> ToolbarItemLocation; + fn focus_changed(&mut self, pane_focused: bool, cx: &mut WindowContext); + fn row_count(&self, cx: &WindowContext) -> usize; +} + +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum ToolbarItemLocation { + Hidden, + PrimaryLeft { flex: Option<(f32, bool)> }, + PrimaryRight { flex: Option<(f32, bool)> }, + Secondary, +} + +pub struct Toolbar { + active_item: Option>, + hidden: bool, + can_navigate: bool, + items: Vec<(Box, ToolbarItemLocation)>, +} + +impl Entity for Toolbar { + type Event = (); +} + +impl View for Toolbar { + fn ui_name() -> &'static str { + "Toolbar" + } + + fn render(&mut self, cx: &mut ViewContext) -> AnyElement { + let theme = &theme::current(cx).workspace.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; + let mut primary_items_row_count = 1; + + for (item, position) in &self.items { + match *position { + ToolbarItemLocation::Hidden => {} + + ToolbarItemLocation::PrimaryLeft { flex } => { + primary_items_row_count = primary_items_row_count.max(item.row_count(cx)); + let left_item = ChildView::new(item.as_any(), cx).aligned(); + if let Some((flex, expanded)) = flex { + primary_left_items.push(left_item.flex(flex, expanded).into_any()); + } else { + primary_left_items.push(left_item.into_any()); + } + } + + ToolbarItemLocation::PrimaryRight { flex } => { + primary_items_row_count = primary_items_row_count.max(item.row_count(cx)); + let right_item = ChildView::new(item.as_any(), cx).aligned().flex_float(); + if let Some((flex, expanded)) = flex { + primary_right_items.push(right_item.flex(flex, expanded).into_any()); + } else { + primary_right_items.push(right_item.into_any()); + } + } + + ToolbarItemLocation::Secondary => { + secondary_item = Some( + ChildView::new(item.as_any(), cx) + .constrained() + .with_height(theme.height * item.row_count(cx) as f32) + .into_any(), + ); + } + } + } + + let container_style = theme.container; + let height = theme.height * primary_items_row_count as f32; + + let mut primary_items = Flex::row().with_spacing(spacing); + primary_items.extend(primary_left_items); + primary_items.extend(primary_right_items); + + let mut toolbar = Flex::column(); + if !primary_items.is_empty() { + toolbar.add_child(primary_items.constrained().with_height(height)); + } + if let Some(secondary_item) = secondary_item { + toolbar.add_child(secondary_item); + } + + if toolbar.is_empty() { + toolbar.into_any_named("toolbar") + } else { + toolbar + .contained() + .with_style(container_style) + .into_any_named("toolbar") + } + } +} + +// <<<<<<< HEAD +// ======= +// #[allow(clippy::too_many_arguments)] +// fn nav_button)>( +// svg_path: &'static str, +// style: theme::Interactive, +// nav_button_height: f32, +// tooltip_style: TooltipStyle, +// enabled: bool, +// spacing: f32, +// on_click: F, +// tooltip_action: A, +// action_name: &'static str, +// cx: &mut ViewContext, +// ) -> AnyElement { +// MouseEventHandler::new::(0, cx, |state, _| { +// let style = if enabled { +// style.style_for(state) +// } 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(nav_button_height) +// .aligned() +// .top() +// }) +// .with_cursor_style(if enabled { +// CursorStyle::PointingHand +// } else { +// CursorStyle::default() +// }) +// .on_click(MouseButton::Left, move |_, toolbar, cx| { +// on_click(toolbar, cx) +// }) +// .with_tooltip::( +// 0, +// action_name, +// Some(Box::new(tooltip_action)), +// tooltip_style, +// cx, +// ) +// .contained() +// .with_margin_right(spacing) +// .into_any_named("nav button") +// } + +// >>>>>>> 139cbbfd3aebd0863a7d51b0c12d748764cf0b2e +impl Toolbar { + pub fn new() -> Self { + Self { + active_item: None, + items: Default::default(), + hidden: false, + can_navigate: true, + } + } + + pub fn set_can_navigate(&mut self, can_navigate: bool, cx: &mut ViewContext) { + self.can_navigate = can_navigate; + cx.notify(); + } + + pub fn add_item(&mut self, item: ViewHandle, cx: &mut ViewContext) + where + T: 'static + ToolbarItemView, + { + let location = item.set_active_pane_item(self.active_item.as_deref(), cx); + cx.subscribe(&item, |this, item, event, cx| { + if let Some((_, current_location)) = + this.items.iter_mut().find(|(i, _)| i.id() == item.id()) + { + let new_location = item + .read(cx) + .location_for_event(event, *current_location, cx); + if new_location != *current_location { + *current_location = new_location; + cx.notify(); + } + } + }) + .detach(); + self.items.push((Box::new(item), location)); + cx.notify(); + } + + pub fn set_active_item(&mut self, item: Option<&dyn ItemHandle>, cx: &mut ViewContext) { + self.active_item = item.map(|item| item.boxed_clone()); + self.hidden = self + .active_item + .as_ref() + .map(|item| !item.show_toolbar(cx)) + .unwrap_or(false); + + for (toolbar_item, current_location) in self.items.iter_mut() { + let new_location = toolbar_item.set_active_pane_item(item, cx); + if new_location != *current_location { + *current_location = new_location; + cx.notify(); + } + } + } + + pub fn focus_changed(&mut self, focused: bool, cx: &mut ViewContext) { + for (toolbar_item, _) in self.items.iter_mut() { + toolbar_item.focus_changed(focused, cx); + } + } + + pub fn item_of_type(&self) -> Option> { + self.items + .iter() + .find_map(|(item, _)| item.as_any().clone().downcast()) + } + + pub fn hidden(&self) -> bool { + self.hidden + } +} + +impl ToolbarItemViewHandle for ViewHandle { + fn id(&self) -> usize { + self.id() + } + + fn as_any(&self) -> &AnyViewHandle { + self + } + + fn set_active_pane_item( + &self, + active_pane_item: Option<&dyn ItemHandle>, + cx: &mut WindowContext, + ) -> ToolbarItemLocation { + self.update(cx, |this, cx| { + this.set_active_pane_item(active_pane_item, cx) + }) + } + + fn focus_changed(&mut self, pane_focused: bool, cx: &mut WindowContext) { + self.update(cx, |this, cx| { + this.pane_focus_update(pane_focused, cx); + cx.notify(); + }); + } + + fn row_count(&self, cx: &WindowContext) -> usize { + self.read_with(cx, |this, cx| this.row_count(cx)) + } +} + +impl From<&dyn ToolbarItemViewHandle> for AnyViewHandle { + fn from(val: &dyn ToolbarItemViewHandle) -> Self { + val.as_any().clone() + } +} diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs new file mode 100644 index 0000000000..607bc5b61c --- /dev/null +++ b/crates/workspace2/src/workspace2.rs @@ -0,0 +1,5520 @@ +// pub mod dock; +// pub mod item; +// pub mod notifications; +// pub mod pane; +// pub mod pane_group; +// mod persistence; +// pub mod searchable; +// pub mod shared_screen; +// mod status_bar; +// mod toolbar; +// mod workspace_settings; + +// use anyhow::{anyhow, Context, Result}; +// use call::ActiveCall; +// use client::{ +// proto::{self, PeerId}, +// Client, Status, TypedEnvelope, UserStore, +// }; +// use collections::{hash_map, HashMap, HashSet}; +// use drag_and_drop::DragAndDrop; +// use futures::{ +// channel::{mpsc, oneshot}, +// future::try_join_all, +// FutureExt, StreamExt, +// }; +// use gpui::{ +// actions, +// elements::*, +// geometry::{ +// rect::RectF, +// vector::{vec2f, Vector2F}, +// }, +// impl_actions, +// platform::{ +// CursorStyle, ModifiersChangedEvent, MouseButton, PathPromptOptions, Platform, PromptLevel, +// WindowBounds, WindowOptions, +// }, +// AnyModelHandle, AnyViewHandle, AnyWeakViewHandle, AnyWindowHandle, AppContext, AsyncAppContext, +// Entity, ModelContext, ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, +// ViewHandle, WeakViewHandle, WindowContext, WindowHandle, +// }; +// use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem}; +// use itertools::Itertools; +// use language::{LanguageRegistry, Rope}; +// use node_runtime::NodeRuntime; +// use std::{ +// any::TypeId, +// borrow::Cow, +// cmp, env, +// future::Future, +// path::{Path, PathBuf}, +// rc::Rc, +// str, +// sync::{atomic::AtomicUsize, Arc}, +// time::Duration, +// }; + +// use crate::{ +// notifications::{simple_message_notification::MessageNotification, NotificationTracker}, +// persistence::model::{ +// DockData, DockStructure, SerializedPane, SerializedPaneGroup, SerializedWorkspace, +// }, +// }; +// use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle}; +// use lazy_static::lazy_static; +// use notifications::{NotificationHandle, NotifyResultExt}; +// pub use pane::*; +// pub use pane_group::*; +// use persistence::{model::SerializedItem, DB}; +// pub use persistence::{ +// model::{ItemId, WorkspaceLocation}, +// WorkspaceDb, DB as WORKSPACE_DB, +// }; +// use postage::prelude::Stream; +// use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId}; +// use serde::Deserialize; +// use shared_screen::SharedScreen; +// use status_bar::StatusBar; +// pub use status_bar::StatusItemView; +// use theme::{Theme, ThemeSettings}; +// pub use toolbar::{ToolbarItemLocation, ToolbarItemView}; +// use util::ResultExt; +// pub use workspace_settings::{AutosaveSetting, GitGutterSetting, WorkspaceSettings}; + +// lazy_static! { +// static ref ZED_WINDOW_SIZE: Option = env::var("ZED_WINDOW_SIZE") +// .ok() +// .as_deref() +// .and_then(parse_pixel_position_env_var); +// static ref ZED_WINDOW_POSITION: Option = env::var("ZED_WINDOW_POSITION") +// .ok() +// .as_deref() +// .and_then(parse_pixel_position_env_var); +// } + +// pub trait Modal: View { +// fn has_focus(&self) -> bool; +// fn dismiss_on_event(event: &Self::Event) -> bool; +// } + +// trait ModalHandle { +// fn as_any(&self) -> &AnyViewHandle; +// fn has_focus(&self, cx: &WindowContext) -> bool; +// } + +// impl ModalHandle for ViewHandle { +// fn as_any(&self) -> &AnyViewHandle { +// self +// } + +// fn has_focus(&self, cx: &WindowContext) -> bool { +// self.read(cx).has_focus() +// } +// } + +// #[derive(Clone, PartialEq)] +// pub struct RemoveWorktreeFromProject(pub WorktreeId); + +// actions!( +// workspace, +// [ +// Open, +// NewFile, +// NewWindow, +// CloseWindow, +// CloseInactiveTabsAndPanes, +// AddFolderToProject, +// Unfollow, +// SaveAs, +// ReloadActiveItem, +// ActivatePreviousPane, +// ActivateNextPane, +// FollowNextCollaborator, +// NewTerminal, +// NewCenterTerminal, +// ToggleTerminalFocus, +// NewSearch, +// Feedback, +// Restart, +// Welcome, +// ToggleZoom, +// ToggleLeftDock, +// ToggleRightDock, +// ToggleBottomDock, +// CloseAllDocks, +// ] +// ); + +// #[derive(Clone, PartialEq)] +// pub struct OpenPaths { +// pub paths: Vec, +// } + +// #[derive(Clone, Deserialize, PartialEq)] +// pub struct ActivatePane(pub usize); + +// #[derive(Clone, Deserialize, PartialEq)] +// pub struct ActivatePaneInDirection(pub SplitDirection); + +// #[derive(Clone, Deserialize, PartialEq)] +// pub struct SwapPaneInDirection(pub SplitDirection); + +// #[derive(Clone, Deserialize, PartialEq)] +// pub struct NewFileInDirection(pub SplitDirection); + +// #[derive(Clone, PartialEq, Debug, Deserialize)] +// #[serde(rename_all = "camelCase")] +// pub struct SaveAll { +// pub save_intent: Option, +// } + +// #[derive(Clone, PartialEq, Debug, Deserialize)] +// #[serde(rename_all = "camelCase")] +// pub struct Save { +// pub save_intent: Option, +// } + +// #[derive(Clone, PartialEq, Debug, Deserialize, Default)] +// #[serde(rename_all = "camelCase")] +// pub struct CloseAllItemsAndPanes { +// pub save_intent: Option, +// } + +// #[derive(Deserialize)] +// pub struct Toast { +// id: usize, +// msg: Cow<'static, str>, +// #[serde(skip)] +// on_click: Option<(Cow<'static, str>, Arc)>, +// } + +// impl Toast { +// pub fn new>>(id: usize, msg: I) -> Self { +// Toast { +// id, +// msg: msg.into(), +// on_click: None, +// } +// } + +// pub fn on_click(mut self, message: M, on_click: F) -> Self +// where +// M: Into>, +// F: Fn(&mut WindowContext) + 'static, +// { +// self.on_click = Some((message.into(), Arc::new(on_click))); +// self +// } +// } + +// impl PartialEq for Toast { +// fn eq(&self, other: &Self) -> bool { +// self.id == other.id +// && self.msg == other.msg +// && self.on_click.is_some() == other.on_click.is_some() +// } +// } + +// impl Clone for Toast { +// fn clone(&self) -> Self { +// Toast { +// id: self.id, +// msg: self.msg.to_owned(), +// on_click: self.on_click.clone(), +// } +// } +// } + +// #[derive(Clone, Deserialize, PartialEq)] +// pub struct OpenTerminal { +// pub working_directory: PathBuf, +// } + +// impl_actions!( +// workspace, +// [ +// ActivatePane, +// ActivatePaneInDirection, +// SwapPaneInDirection, +// NewFileInDirection, +// Toast, +// OpenTerminal, +// SaveAll, +// Save, +// CloseAllItemsAndPanes, +// ] +// ); + +// pub type WorkspaceId = i64; + +// pub fn init_settings(cx: &mut AppContext) { +// settings::register::(cx); +// settings::register::(cx); +// } + +// pub fn init(app_state: Arc, cx: &mut AppContext) { +// init_settings(cx); +// pane::init(cx); +// notifications::init(cx); + +// cx.add_global_action({ +// let app_state = Arc::downgrade(&app_state); +// move |_: &Open, cx: &mut AppContext| { +// let mut paths = cx.prompt_for_paths(PathPromptOptions { +// files: true, +// directories: true, +// multiple: true, +// }); + +// if let Some(app_state) = app_state.upgrade() { +// cx.spawn(move |mut cx| async move { +// if let Some(paths) = paths.recv().await.flatten() { +// cx.update(|cx| { +// open_paths(&paths, &app_state, None, cx).detach_and_log_err(cx) +// }); +// } +// }) +// .detach(); +// } +// } +// }); +// cx.add_async_action(Workspace::open); + +// cx.add_async_action(Workspace::follow_next_collaborator); +// cx.add_async_action(Workspace::close); +// cx.add_async_action(Workspace::close_inactive_items_and_panes); +// cx.add_async_action(Workspace::close_all_items_and_panes); +// cx.add_global_action(Workspace::close_global); +// cx.add_global_action(restart); +// cx.add_async_action(Workspace::save_all); +// cx.add_action(Workspace::add_folder_to_project); +// cx.add_action( +// |workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext| { +// let pane = workspace.active_pane().clone(); +// workspace.unfollow(&pane, cx); +// }, +// ); +// cx.add_action( +// |workspace: &mut Workspace, action: &Save, cx: &mut ViewContext| { +// workspace +// .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx) +// .detach_and_log_err(cx); +// }, +// ); +// cx.add_action( +// |workspace: &mut Workspace, _: &SaveAs, cx: &mut ViewContext| { +// workspace +// .save_active_item(SaveIntent::SaveAs, cx) +// .detach_and_log_err(cx); +// }, +// ); +// cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| { +// workspace.activate_previous_pane(cx) +// }); +// cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| { +// workspace.activate_next_pane(cx) +// }); + +// cx.add_action( +// |workspace: &mut Workspace, action: &ActivatePaneInDirection, cx| { +// workspace.activate_pane_in_direction(action.0, cx) +// }, +// ); + +// cx.add_action( +// |workspace: &mut Workspace, action: &SwapPaneInDirection, cx| { +// workspace.swap_pane_in_direction(action.0, cx) +// }, +// ); + +// cx.add_action(|workspace: &mut Workspace, _: &ToggleLeftDock, cx| { +// workspace.toggle_dock(DockPosition::Left, cx); +// }); +// cx.add_action(|workspace: &mut Workspace, _: &ToggleRightDock, cx| { +// workspace.toggle_dock(DockPosition::Right, cx); +// }); +// cx.add_action(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| { +// workspace.toggle_dock(DockPosition::Bottom, cx); +// }); +// cx.add_action(|workspace: &mut Workspace, _: &CloseAllDocks, cx| { +// workspace.close_all_docks(cx); +// }); +// cx.add_action(Workspace::activate_pane_at_index); +// cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| { +// workspace.reopen_closed_item(cx).detach(); +// }); +// cx.add_action(|workspace: &mut Workspace, _: &GoBack, cx| { +// workspace +// .go_back(workspace.active_pane().downgrade(), cx) +// .detach(); +// }); +// cx.add_action(|workspace: &mut Workspace, _: &GoForward, cx| { +// workspace +// .go_forward(workspace.active_pane().downgrade(), cx) +// .detach(); +// }); + +// cx.add_action(|_: &mut Workspace, _: &install_cli::Install, cx| { +// cx.spawn(|workspace, mut cx| async move { +// let err = install_cli::install_cli(&cx) +// .await +// .context("Failed to create CLI symlink"); + +// workspace.update(&mut cx, |workspace, cx| { +// if matches!(err, Err(_)) { +// err.notify_err(workspace, cx); +// } else { +// workspace.show_notification(1, cx, |cx| { +// cx.add_view(|_| { +// MessageNotification::new("Successfully installed the `zed` binary") +// }) +// }); +// } +// }) +// }) +// .detach(); +// }); +// } + +// type ProjectItemBuilders = HashMap< +// TypeId, +// fn(ModelHandle, AnyModelHandle, &mut ViewContext) -> Box, +// >; +// pub fn register_project_item(cx: &mut AppContext) { +// cx.update_default_global(|builders: &mut ProjectItemBuilders, _| { +// builders.insert(TypeId::of::(), |project, model, cx| { +// let item = model.downcast::().unwrap(); +// Box::new(cx.add_view(|cx| I::for_project_item(project, item, cx))) +// }); +// }); +// } + +// type FollowableItemBuilder = fn( +// ViewHandle, +// ViewHandle, +// ViewId, +// &mut Option, +// &mut AppContext, +// ) -> Option>>>; +// type FollowableItemBuilders = HashMap< +// TypeId, +// ( +// FollowableItemBuilder, +// fn(&AnyViewHandle) -> Box, +// ), +// >; +// pub fn register_followable_item(cx: &mut AppContext) { +// cx.update_default_global(|builders: &mut FollowableItemBuilders, _| { +// builders.insert( +// TypeId::of::(), +// ( +// |pane, workspace, id, state, cx| { +// I::from_state_proto(pane, workspace, id, state, cx).map(|task| { +// cx.foreground() +// .spawn(async move { Ok(Box::new(task.await?) as Box<_>) }) +// }) +// }, +// |this| Box::new(this.clone().downcast::().unwrap()), +// ), +// ); +// }); +// } + +// type ItemDeserializers = HashMap< +// Arc, +// fn( +// ModelHandle, +// WeakViewHandle, +// WorkspaceId, +// ItemId, +// &mut ViewContext, +// ) -> Task>>, +// >; +// pub fn register_deserializable_item(cx: &mut AppContext) { +// cx.update_default_global(|deserializers: &mut ItemDeserializers, _cx| { +// if let Some(serialized_item_kind) = I::serialized_item_kind() { +// deserializers.insert( +// Arc::from(serialized_item_kind), +// |project, workspace, workspace_id, item_id, cx| { +// let task = I::deserialize(project, workspace, workspace_id, item_id, cx); +// cx.foreground() +// .spawn(async { Ok(Box::new(task.await?) as Box<_>) }) +// }, +// ); +// } +// }); +// } + +// pub struct AppState { +// pub languages: Arc, +// pub client: Arc, +// pub user_store: ModelHandle, +// pub workspace_store: ModelHandle, +// pub fs: Arc, +// pub build_window_options: +// fn(Option, Option, &dyn Platform) -> WindowOptions<'static>, +// pub initialize_workspace: +// fn(WeakViewHandle, bool, Arc, AsyncAppContext) -> Task>, +// pub background_actions: BackgroundActions, +// pub node_runtime: Arc, +// } + +// pub struct WorkspaceStore { +// workspaces: HashSet>, +// followers: Vec, +// client: Arc, +// _subscriptions: Vec, +// } + +// #[derive(PartialEq, Eq, PartialOrd, Ord, Debug)] +// struct Follower { +// project_id: Option, +// peer_id: PeerId, +// } + +// impl AppState { +// #[cfg(any(test, feature = "test-support"))] +// pub fn test(cx: &mut AppContext) -> Arc { +// use node_runtime::FakeNodeRuntime; +// use settings::SettingsStore; + +// if !cx.has_global::() { +// cx.set_global(SettingsStore::test(cx)); +// } + +// let fs = fs::FakeFs::new(cx.background().clone()); +// let languages = Arc::new(LanguageRegistry::test()); +// let http_client = util::http::FakeHttpClient::with_404_response(); +// let client = Client::new(http_client.clone(), cx); +// let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); +// let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx)); + +// theme::init((), cx); +// client::init(&client, cx); +// crate::init_settings(cx); + +// Arc::new(Self { +// client, +// fs, +// languages, +// user_store, +// // channel_store, +// workspace_store, +// node_runtime: FakeNodeRuntime::new(), +// initialize_workspace: |_, _, _, _| Task::ready(Ok(())), +// build_window_options: |_, _, _| Default::default(), +// background_actions: || &[], +// }) +// } +// } + +// struct DelayedDebouncedEditAction { +// task: Option>, +// cancel_channel: Option>, +// } + +// impl DelayedDebouncedEditAction { +// fn new() -> DelayedDebouncedEditAction { +// DelayedDebouncedEditAction { +// task: None, +// cancel_channel: None, +// } +// } + +// fn fire_new(&mut self, delay: Duration, cx: &mut ViewContext, func: F) +// where +// F: 'static + FnOnce(&mut Workspace, &mut ViewContext) -> Task>, +// { +// if let Some(channel) = self.cancel_channel.take() { +// _ = channel.send(()); +// } + +// let (sender, mut receiver) = oneshot::channel::<()>(); +// self.cancel_channel = Some(sender); + +// let previous_task = self.task.take(); +// self.task = Some(cx.spawn(|workspace, mut cx| async move { +// let mut timer = cx.background().timer(delay).fuse(); +// if let Some(previous_task) = previous_task { +// previous_task.await; +// } + +// futures::select_biased! { +// _ = receiver => return, +// _ = timer => {} +// } + +// if let Some(result) = workspace +// .update(&mut cx, |workspace, cx| (func)(workspace, cx)) +// .log_err() +// { +// result.await.log_err(); +// } +// })); +// } +// } + +// pub enum Event { +// PaneAdded(ViewHandle), +// ContactRequestedJoin(u64), +// } + +// pub struct Workspace { +// weak_self: WeakViewHandle, +// modal: Option, +// zoomed: Option, +// zoomed_position: Option, +// center: PaneGroup, +// left_dock: ViewHandle, +// bottom_dock: ViewHandle, +// right_dock: ViewHandle, +// panes: Vec>, +// panes_by_item: HashMap>, +// active_pane: ViewHandle, +// last_active_center_pane: Option>, +// last_active_view_id: Option, +// status_bar: ViewHandle, +// titlebar_item: Option, +// notifications: Vec<(TypeId, usize, Box)>, +// project: ModelHandle, +// follower_states: HashMap, FollowerState>, +// last_leaders_by_pane: HashMap, PeerId>, +// window_edited: bool, +// active_call: Option<(ModelHandle, Vec)>, +// leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>, +// database_id: WorkspaceId, +// app_state: Arc, +// subscriptions: Vec, +// _apply_leader_updates: Task>, +// _observe_current_user: Task>, +// _schedule_serialize: Option>, +// pane_history_timestamp: Arc, +// } + +// struct ActiveModal { +// view: Box, +// previously_focused_view_id: Option, +// } + +// #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +// pub struct ViewId { +// pub creator: PeerId, +// pub id: u64, +// } + +// #[derive(Default)] +// struct FollowerState { +// leader_id: PeerId, +// active_view_id: Option, +// items_by_leader_view_id: HashMap>, +// } + +// enum WorkspaceBounds {} + +// impl Workspace { +// pub fn new( +// workspace_id: WorkspaceId, +// project: ModelHandle, +// app_state: Arc, +// cx: &mut ViewContext, +// ) -> Self { +// cx.observe(&project, |_, _, cx| cx.notify()).detach(); +// cx.subscribe(&project, move |this, _, event, cx| { +// match event { +// project::Event::RemoteIdChanged(_) => { +// this.update_window_title(cx); +// } + +// project::Event::CollaboratorLeft(peer_id) => { +// this.collaborator_left(*peer_id, cx); +// } + +// project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded => { +// this.update_window_title(cx); +// this.serialize_workspace(cx); +// } + +// project::Event::DisconnectedFromHost => { +// this.update_window_edited(cx); +// cx.blur(); +// } + +// project::Event::Closed => { +// cx.remove_window(); +// } + +// project::Event::DeletedEntry(entry_id) => { +// for pane in this.panes.iter() { +// pane.update(cx, |pane, cx| { +// pane.handle_deleted_project_item(*entry_id, cx) +// }); +// } +// } + +// project::Event::Notification(message) => this.show_notification(0, cx, |cx| { +// cx.add_view(|_| MessageNotification::new(message.clone())) +// }), + +// _ => {} +// } +// cx.notify() +// }) +// .detach(); + +// let weak_handle = cx.weak_handle(); +// let pane_history_timestamp = Arc::new(AtomicUsize::new(0)); + +// let center_pane = cx.add_view(|cx| { +// Pane::new( +// weak_handle.clone(), +// project.clone(), +// app_state.background_actions, +// pane_history_timestamp.clone(), +// cx, +// ) +// }); +// cx.subscribe(¢er_pane, Self::handle_pane_event).detach(); +// cx.focus(¢er_pane); +// cx.emit(Event::PaneAdded(center_pane.clone())); + +// app_state.workspace_store.update(cx, |store, _| { +// store.workspaces.insert(weak_handle.clone()); +// }); + +// let mut current_user = app_state.user_store.read(cx).watch_current_user(); +// let mut connection_status = app_state.client.status(); +// let _observe_current_user = cx.spawn(|this, mut cx| async move { +// current_user.recv().await; +// connection_status.recv().await; +// let mut stream = +// Stream::map(current_user, drop).merge(Stream::map(connection_status, drop)); + +// while stream.recv().await.is_some() { +// this.update(&mut cx, |_, cx| cx.notify())?; +// } +// anyhow::Ok(()) +// }); + +// // All leader updates are enqueued and then processed in a single task, so +// // that each asynchronous operation can be run in order. +// let (leader_updates_tx, mut leader_updates_rx) = +// mpsc::unbounded::<(PeerId, proto::UpdateFollowers)>(); +// let _apply_leader_updates = cx.spawn(|this, mut cx| async move { +// while let Some((leader_id, update)) = leader_updates_rx.next().await { +// Self::process_leader_update(&this, leader_id, update, &mut cx) +// .await +// .log_err(); +// } + +// Ok(()) +// }); + +// cx.emit_global(WorkspaceCreated(weak_handle.clone())); + +// let left_dock = cx.add_view(|_| Dock::new(DockPosition::Left)); +// let bottom_dock = cx.add_view(|_| Dock::new(DockPosition::Bottom)); +// let right_dock = cx.add_view(|_| Dock::new(DockPosition::Right)); +// let left_dock_buttons = +// cx.add_view(|cx| PanelButtons::new(left_dock.clone(), weak_handle.clone(), cx)); +// let bottom_dock_buttons = +// cx.add_view(|cx| PanelButtons::new(bottom_dock.clone(), weak_handle.clone(), cx)); +// let right_dock_buttons = +// cx.add_view(|cx| PanelButtons::new(right_dock.clone(), weak_handle.clone(), cx)); +// let status_bar = cx.add_view(|cx| { +// let mut status_bar = StatusBar::new(¢er_pane.clone(), cx); +// status_bar.add_left_item(left_dock_buttons, cx); +// status_bar.add_right_item(right_dock_buttons, cx); +// status_bar.add_right_item(bottom_dock_buttons, cx); +// status_bar +// }); + +// cx.update_default_global::, _, _>(|drag_and_drop, _| { +// drag_and_drop.register_container(weak_handle.clone()); +// }); + +// let mut active_call = None; +// if cx.has_global::>() { +// let call = cx.global::>().clone(); +// let mut subscriptions = Vec::new(); +// subscriptions.push(cx.subscribe(&call, Self::on_active_call_event)); +// active_call = Some((call, subscriptions)); +// } + +// let subscriptions = vec![ +// cx.observe_fullscreen(|_, _, cx| cx.notify()), +// cx.observe_window_activation(Self::on_window_activation_changed), +// cx.observe_window_bounds(move |_, mut bounds, display, cx| { +// // Transform fixed bounds to be stored in terms of the containing display +// if let WindowBounds::Fixed(mut window_bounds) = bounds { +// if let Some(screen) = cx.platform().screen_by_id(display) { +// let screen_bounds = screen.bounds(); +// window_bounds +// .set_origin_x(window_bounds.origin_x() - screen_bounds.origin_x()); +// window_bounds +// .set_origin_y(window_bounds.origin_y() - screen_bounds.origin_y()); +// bounds = WindowBounds::Fixed(window_bounds); +// } +// } + +// cx.background() +// .spawn(DB.set_window_bounds(workspace_id, bounds, display)) +// .detach_and_log_err(cx); +// }), +// cx.observe(&left_dock, |this, _, cx| { +// this.serialize_workspace(cx); +// cx.notify(); +// }), +// cx.observe(&bottom_dock, |this, _, cx| { +// this.serialize_workspace(cx); +// cx.notify(); +// }), +// cx.observe(&right_dock, |this, _, cx| { +// this.serialize_workspace(cx); +// cx.notify(); +// }), +// ]; + +// cx.defer(|this, cx| this.update_window_title(cx)); +// Workspace { +// weak_self: weak_handle.clone(), +// modal: None, +// zoomed: None, +// zoomed_position: None, +// center: PaneGroup::new(center_pane.clone()), +// panes: vec![center_pane.clone()], +// panes_by_item: Default::default(), +// active_pane: center_pane.clone(), +// last_active_center_pane: Some(center_pane.downgrade()), +// last_active_view_id: None, +// status_bar, +// titlebar_item: None, +// notifications: Default::default(), +// left_dock, +// bottom_dock, +// right_dock, +// project: project.clone(), +// follower_states: Default::default(), +// last_leaders_by_pane: Default::default(), +// window_edited: false, +// active_call, +// database_id: workspace_id, +// app_state, +// _observe_current_user, +// _apply_leader_updates, +// _schedule_serialize: None, +// leader_updates_tx, +// subscriptions, +// pane_history_timestamp, +// } +// } + +// fn new_local( +// abs_paths: Vec, +// app_state: Arc, +// requesting_window: Option>, +// cx: &mut AppContext, +// ) -> Task<( +// WeakViewHandle, +// Vec, anyhow::Error>>>, +// )> { +// let project_handle = Project::local( +// app_state.client.clone(), +// app_state.node_runtime.clone(), +// app_state.user_store.clone(), +// app_state.languages.clone(), +// app_state.fs.clone(), +// cx, +// ); + +// cx.spawn(|mut cx| async move { +// let serialized_workspace = persistence::DB.workspace_for_roots(&abs_paths.as_slice()); + +// let paths_to_open = Arc::new(abs_paths); + +// // Get project paths for all of the abs_paths +// let mut worktree_roots: HashSet> = Default::default(); +// let mut project_paths: Vec<(PathBuf, Option)> = +// Vec::with_capacity(paths_to_open.len()); +// for path in paths_to_open.iter().cloned() { +// if let Some((worktree, project_entry)) = cx +// .update(|cx| { +// Workspace::project_path_for_path(project_handle.clone(), &path, true, cx) +// }) +// .await +// .log_err() +// { +// worktree_roots.insert(worktree.read_with(&mut cx, |tree, _| tree.abs_path())); +// project_paths.push((path, Some(project_entry))); +// } else { +// project_paths.push((path, None)); +// } +// } + +// let workspace_id = if let Some(serialized_workspace) = serialized_workspace.as_ref() { +// serialized_workspace.id +// } else { +// DB.next_id().await.unwrap_or(0) +// }; + +// let window = if let Some(window) = requesting_window { +// window.replace_root(&mut cx, |cx| { +// Workspace::new(workspace_id, project_handle.clone(), app_state.clone(), cx) +// }); +// window +// } else { +// { +// let window_bounds_override = window_bounds_env_override(&cx); +// let (bounds, display) = if let Some(bounds) = window_bounds_override { +// (Some(bounds), None) +// } else { +// serialized_workspace +// .as_ref() +// .and_then(|serialized_workspace| { +// let display = serialized_workspace.display?; +// let mut bounds = serialized_workspace.bounds?; + +// // Stored bounds are relative to the containing display. +// // So convert back to global coordinates if that screen still exists +// if let WindowBounds::Fixed(mut window_bounds) = bounds { +// if let Some(screen) = cx.platform().screen_by_id(display) { +// let screen_bounds = screen.bounds(); +// window_bounds.set_origin_x( +// window_bounds.origin_x() + screen_bounds.origin_x(), +// ); +// window_bounds.set_origin_y( +// window_bounds.origin_y() + screen_bounds.origin_y(), +// ); +// bounds = WindowBounds::Fixed(window_bounds); +// } else { +// // Screen no longer exists. Return none here. +// return None; +// } +// } + +// Some((bounds, display)) +// }) +// .unzip() +// }; + +// // Use the serialized workspace to construct the new window +// cx.add_window( +// (app_state.build_window_options)(bounds, display, cx.platform().as_ref()), +// |cx| { +// Workspace::new( +// workspace_id, +// project_handle.clone(), +// app_state.clone(), +// cx, +// ) +// }, +// ) +// } +// }; + +// // We haven't yielded the main thread since obtaining the window handle, +// // so the window exists. +// let workspace = window.root(&cx).unwrap(); + +// (app_state.initialize_workspace)( +// workspace.downgrade(), +// serialized_workspace.is_some(), +// app_state.clone(), +// cx.clone(), +// ) +// .await +// .log_err(); + +// window.update(&mut cx, |cx| cx.activate_window()); + +// let workspace = workspace.downgrade(); +// notify_if_database_failed(&workspace, &mut cx); +// let opened_items = open_items( +// serialized_workspace, +// &workspace, +// project_paths, +// app_state, +// cx, +// ) +// .await +// .unwrap_or_default(); + +// (workspace, opened_items) +// }) +// } + +// pub fn weak_handle(&self) -> WeakViewHandle { +// self.weak_self.clone() +// } + +// pub fn left_dock(&self) -> &ViewHandle { +// &self.left_dock +// } + +// pub fn bottom_dock(&self) -> &ViewHandle { +// &self.bottom_dock +// } + +// pub fn right_dock(&self) -> &ViewHandle { +// &self.right_dock +// } + +// pub fn add_panel(&mut self, panel: ViewHandle, cx: &mut ViewContext) +// where +// T::Event: std::fmt::Debug, +// { +// self.add_panel_with_extra_event_handler(panel, cx, |_, _, _, _| {}) +// } + +// pub fn add_panel_with_extra_event_handler( +// &mut self, +// panel: ViewHandle, +// cx: &mut ViewContext, +// handler: F, +// ) where +// T::Event: std::fmt::Debug, +// F: Fn(&mut Self, &ViewHandle, &T::Event, &mut ViewContext) + 'static, +// { +// let dock = match panel.position(cx) { +// DockPosition::Left => &self.left_dock, +// DockPosition::Bottom => &self.bottom_dock, +// DockPosition::Right => &self.right_dock, +// }; + +// self.subscriptions.push(cx.subscribe(&panel, { +// let mut dock = dock.clone(); +// let mut prev_position = panel.position(cx); +// move |this, panel, event, cx| { +// if T::should_change_position_on_event(event) { +// let new_position = panel.read(cx).position(cx); +// let mut was_visible = false; +// dock.update(cx, |dock, cx| { +// prev_position = new_position; + +// was_visible = dock.is_open() +// && dock +// .visible_panel() +// .map_or(false, |active_panel| active_panel.id() == panel.id()); +// dock.remove_panel(&panel, cx); +// }); + +// if panel.is_zoomed(cx) { +// this.zoomed_position = Some(new_position); +// } + +// dock = match panel.read(cx).position(cx) { +// DockPosition::Left => &this.left_dock, +// DockPosition::Bottom => &this.bottom_dock, +// DockPosition::Right => &this.right_dock, +// } +// .clone(); +// dock.update(cx, |dock, cx| { +// dock.add_panel(panel.clone(), cx); +// if was_visible { +// dock.set_open(true, cx); +// dock.activate_panel(dock.panels_len() - 1, cx); +// } +// }); +// } else if T::should_zoom_in_on_event(event) { +// dock.update(cx, |dock, cx| dock.set_panel_zoomed(&panel, true, cx)); +// if !panel.has_focus(cx) { +// cx.focus(&panel); +// } +// this.zoomed = Some(panel.downgrade().into_any()); +// this.zoomed_position = Some(panel.read(cx).position(cx)); +// } else if T::should_zoom_out_on_event(event) { +// dock.update(cx, |dock, cx| dock.set_panel_zoomed(&panel, false, cx)); +// if this.zoomed_position == Some(prev_position) { +// this.zoomed = None; +// this.zoomed_position = None; +// } +// cx.notify(); +// } else if T::is_focus_event(event) { +// let position = panel.read(cx).position(cx); +// this.dismiss_zoomed_items_to_reveal(Some(position), cx); +// if panel.is_zoomed(cx) { +// this.zoomed = Some(panel.downgrade().into_any()); +// this.zoomed_position = Some(position); +// } else { +// this.zoomed = None; +// this.zoomed_position = None; +// } +// this.update_active_view_for_followers(cx); +// cx.notify(); +// } else { +// handler(this, &panel, event, cx) +// } +// } +// })); + +// dock.update(cx, |dock, cx| dock.add_panel(panel, cx)); +// } + +// pub fn status_bar(&self) -> &ViewHandle { +// &self.status_bar +// } + +// pub fn app_state(&self) -> &Arc { +// &self.app_state +// } + +// pub fn user_store(&self) -> &ModelHandle { +// &self.app_state.user_store +// } + +// pub fn project(&self) -> &ModelHandle { +// &self.project +// } + +// pub fn recent_navigation_history( +// &self, +// limit: Option, +// cx: &AppContext, +// ) -> Vec<(ProjectPath, Option)> { +// let mut abs_paths_opened: HashMap> = HashMap::default(); +// let mut history: HashMap, usize)> = HashMap::default(); +// for pane in &self.panes { +// let pane = pane.read(cx); +// pane.nav_history() +// .for_each_entry(cx, |entry, (project_path, fs_path)| { +// if let Some(fs_path) = &fs_path { +// abs_paths_opened +// .entry(fs_path.clone()) +// .or_default() +// .insert(project_path.clone()); +// } +// let timestamp = entry.timestamp; +// match history.entry(project_path) { +// hash_map::Entry::Occupied(mut entry) => { +// let (_, old_timestamp) = entry.get(); +// if ×tamp > old_timestamp { +// entry.insert((fs_path, timestamp)); +// } +// } +// hash_map::Entry::Vacant(entry) => { +// entry.insert((fs_path, timestamp)); +// } +// } +// }); +// } + +// history +// .into_iter() +// .sorted_by_key(|(_, (_, timestamp))| *timestamp) +// .map(|(project_path, (fs_path, _))| (project_path, fs_path)) +// .rev() +// .filter(|(history_path, abs_path)| { +// let latest_project_path_opened = abs_path +// .as_ref() +// .and_then(|abs_path| abs_paths_opened.get(abs_path)) +// .and_then(|project_paths| { +// project_paths +// .iter() +// .max_by(|b1, b2| b1.worktree_id.cmp(&b2.worktree_id)) +// }); + +// match latest_project_path_opened { +// Some(latest_project_path_opened) => latest_project_path_opened == history_path, +// None => true, +// } +// }) +// .take(limit.unwrap_or(usize::MAX)) +// .collect() +// } + +// fn navigate_history( +// &mut self, +// pane: WeakViewHandle, +// mode: NavigationMode, +// cx: &mut ViewContext, +// ) -> Task> { +// let to_load = if let Some(pane) = pane.upgrade(cx) { +// cx.focus(&pane); + +// pane.update(cx, |pane, cx| { +// loop { +// // Retrieve the weak item handle from the history. +// let entry = pane.nav_history_mut().pop(mode, cx)?; + +// // If the item is still present in this pane, then activate it. +// if let Some(index) = entry +// .item +// .upgrade(cx) +// .and_then(|v| pane.index_for_item(v.as_ref())) +// { +// let prev_active_item_index = pane.active_item_index(); +// pane.nav_history_mut().set_mode(mode); +// pane.activate_item(index, true, true, cx); +// pane.nav_history_mut().set_mode(NavigationMode::Normal); + +// let mut navigated = prev_active_item_index != pane.active_item_index(); +// if let Some(data) = entry.data { +// navigated |= pane.active_item()?.navigate(data, cx); +// } + +// if navigated { +// break None; +// } +// } +// // If the item is no longer present in this pane, then retrieve its +// // project path in order to reopen it. +// else { +// break pane +// .nav_history() +// .path_for_item(entry.item.id()) +// .map(|(project_path, _)| (project_path, entry)); +// } +// } +// }) +// } else { +// None +// }; + +// if let Some((project_path, entry)) = to_load { +// // If the item was no longer present, then load it again from its previous path. +// let task = self.load_path(project_path, cx); +// cx.spawn(|workspace, mut cx| async move { +// let task = task.await; +// let mut navigated = false; +// if let Some((project_entry_id, build_item)) = task.log_err() { +// let prev_active_item_id = pane.update(&mut cx, |pane, _| { +// pane.nav_history_mut().set_mode(mode); +// pane.active_item().map(|p| p.id()) +// })?; + +// pane.update(&mut cx, |pane, cx| { +// let item = pane.open_item(project_entry_id, true, cx, build_item); +// navigated |= Some(item.id()) != prev_active_item_id; +// pane.nav_history_mut().set_mode(NavigationMode::Normal); +// if let Some(data) = entry.data { +// navigated |= item.navigate(data, cx); +// } +// })?; +// } + +// if !navigated { +// workspace +// .update(&mut cx, |workspace, cx| { +// Self::navigate_history(workspace, pane, mode, cx) +// })? +// .await?; +// } + +// Ok(()) +// }) +// } else { +// Task::ready(Ok(())) +// } +// } + +// pub fn go_back( +// &mut self, +// pane: WeakViewHandle, +// cx: &mut ViewContext, +// ) -> Task> { +// self.navigate_history(pane, NavigationMode::GoingBack, cx) +// } + +// pub fn go_forward( +// &mut self, +// pane: WeakViewHandle, +// cx: &mut ViewContext, +// ) -> Task> { +// self.navigate_history(pane, NavigationMode::GoingForward, cx) +// } + +// pub fn reopen_closed_item(&mut self, cx: &mut ViewContext) -> Task> { +// self.navigate_history( +// self.active_pane().downgrade(), +// NavigationMode::ReopeningClosedItem, +// cx, +// ) +// } + +// pub fn client(&self) -> &Client { +// &self.app_state.client +// } + +// pub fn set_titlebar_item(&mut self, item: AnyViewHandle, cx: &mut ViewContext) { +// self.titlebar_item = Some(item); +// cx.notify(); +// } + +// pub fn titlebar_item(&self) -> Option { +// self.titlebar_item.clone() +// } + +// /// Call the given callback with a workspace whose project is local. +// /// +// /// If the given workspace has a local project, then it will be passed +// /// to the callback. Otherwise, a new empty window will be created. +// pub fn with_local_workspace( +// &mut self, +// cx: &mut ViewContext, +// callback: F, +// ) -> Task> +// where +// T: 'static, +// F: 'static + FnOnce(&mut Workspace, &mut ViewContext) -> T, +// { +// if self.project.read(cx).is_local() { +// Task::Ready(Some(Ok(callback(self, cx)))) +// } else { +// let task = Self::new_local(Vec::new(), self.app_state.clone(), None, cx); +// cx.spawn(|_vh, mut cx| async move { +// let (workspace, _) = task.await; +// workspace.update(&mut cx, callback) +// }) +// } +// } + +// pub fn worktrees<'a>( +// &self, +// cx: &'a AppContext, +// ) -> impl 'a + Iterator> { +// self.project.read(cx).worktrees(cx) +// } + +// pub fn visible_worktrees<'a>( +// &self, +// cx: &'a AppContext, +// ) -> impl 'a + Iterator> { +// self.project.read(cx).visible_worktrees(cx) +// } + +// pub fn worktree_scans_complete(&self, cx: &AppContext) -> impl Future + 'static { +// let futures = self +// .worktrees(cx) +// .filter_map(|worktree| worktree.read(cx).as_local()) +// .map(|worktree| worktree.scan_complete()) +// .collect::>(); +// async move { +// for future in futures { +// future.await; +// } +// } +// } + +// pub fn close_global(_: &CloseWindow, cx: &mut AppContext) { +// cx.spawn(|mut cx| async move { +// let window = cx +// .windows() +// .into_iter() +// .find(|window| window.is_active(&cx).unwrap_or(false)); +// if let Some(window) = window { +// //This can only get called when the window's project connection has been lost +// //so we don't need to prompt the user for anything and instead just close the window +// window.remove(&mut cx); +// } +// }) +// .detach(); +// } + +// pub fn close( +// &mut self, +// _: &CloseWindow, +// cx: &mut ViewContext, +// ) -> Option>> { +// let window = cx.window(); +// let prepare = self.prepare_to_close(false, cx); +// Some(cx.spawn(|_, mut cx| async move { +// if prepare.await? { +// window.remove(&mut cx); +// } +// Ok(()) +// })) +// } + +// pub fn prepare_to_close( +// &mut self, +// quitting: bool, +// cx: &mut ViewContext, +// ) -> Task> { +// let active_call = self.active_call().cloned(); +// let window = cx.window(); + +// cx.spawn(|this, mut cx| async move { +// let workspace_count = cx +// .windows() +// .into_iter() +// .filter(|window| window.root_is::()) +// .count(); + +// if let Some(active_call) = active_call { +// if !quitting +// && workspace_count == 1 +// && active_call.read_with(&cx, |call, _| call.room().is_some()) +// { +// let answer = window.prompt( +// PromptLevel::Warning, +// "Do you want to leave the current call?", +// &["Close window and hang up", "Cancel"], +// &mut cx, +// ); + +// if let Some(mut answer) = answer { +// if answer.next().await == Some(1) { +// return anyhow::Ok(false); +// } else { +// active_call +// .update(&mut cx, |call, cx| call.hang_up(cx)) +// .await +// .log_err(); +// } +// } +// } +// } + +// Ok(this +// .update(&mut cx, |this, cx| { +// this.save_all_internal(SaveIntent::Close, cx) +// })? +// .await?) +// }) +// } + +// fn save_all( +// &mut self, +// action: &SaveAll, +// cx: &mut ViewContext, +// ) -> Option>> { +// let save_all = +// self.save_all_internal(action.save_intent.unwrap_or(SaveIntent::SaveAll), cx); +// Some(cx.foreground().spawn(async move { +// save_all.await?; +// Ok(()) +// })) +// } + +// fn save_all_internal( +// &mut self, +// mut save_intent: SaveIntent, +// cx: &mut ViewContext, +// ) -> Task> { +// if self.project.read(cx).is_read_only() { +// return Task::ready(Ok(true)); +// } +// let dirty_items = self +// .panes +// .iter() +// .flat_map(|pane| { +// pane.read(cx).items().filter_map(|item| { +// if item.is_dirty(cx) { +// Some((pane.downgrade(), item.boxed_clone())) +// } else { +// None +// } +// }) +// }) +// .collect::>(); + +// let project = self.project.clone(); +// cx.spawn(|workspace, mut cx| async move { +// // Override save mode and display "Save all files" prompt +// if save_intent == SaveIntent::Close && dirty_items.len() > 1 { +// let mut answer = workspace.update(&mut cx, |_, cx| { +// let prompt = Pane::file_names_for_prompt( +// &mut dirty_items.iter().map(|(_, handle)| handle), +// dirty_items.len(), +// cx, +// ); +// cx.prompt( +// PromptLevel::Warning, +// &prompt, +// &["Save all", "Discard all", "Cancel"], +// ) +// })?; +// match answer.next().await { +// Some(0) => save_intent = SaveIntent::SaveAll, +// Some(1) => save_intent = SaveIntent::Skip, +// _ => {} +// } +// } +// for (pane, item) in dirty_items { +// let (singleton, project_entry_ids) = +// cx.read(|cx| (item.is_singleton(cx), item.project_entry_ids(cx))); +// if singleton || !project_entry_ids.is_empty() { +// if let Some(ix) = +// pane.read_with(&cx, |pane, _| pane.index_for_item(item.as_ref()))? +// { +// if !Pane::save_item( +// project.clone(), +// &pane, +// ix, +// &*item, +// save_intent, +// &mut cx, +// ) +// .await? +// { +// return Ok(false); +// } +// } +// } +// } +// Ok(true) +// }) +// } + +// pub fn open(&mut self, _: &Open, cx: &mut ViewContext) -> Option>> { +// let mut paths = cx.prompt_for_paths(PathPromptOptions { +// files: true, +// directories: true, +// multiple: true, +// }); + +// Some(cx.spawn(|this, mut cx| async move { +// if let Some(paths) = paths.recv().await.flatten() { +// if let Some(task) = this +// .update(&mut cx, |this, cx| this.open_workspace_for_paths(paths, cx)) +// .log_err() +// { +// task.await? +// } +// } +// Ok(()) +// })) +// } + +// pub fn open_workspace_for_paths( +// &mut self, +// paths: Vec, +// cx: &mut ViewContext, +// ) -> Task> { +// let window = cx.window().downcast::(); +// let is_remote = self.project.read(cx).is_remote(); +// let has_worktree = self.project.read(cx).worktrees(cx).next().is_some(); +// let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx)); +// let close_task = if is_remote || has_worktree || has_dirty_items { +// None +// } else { +// Some(self.prepare_to_close(false, cx)) +// }; +// let app_state = self.app_state.clone(); + +// cx.spawn(|_, mut cx| async move { +// let window_to_replace = if let Some(close_task) = close_task { +// if !close_task.await? { +// return Ok(()); +// } +// window +// } else { +// None +// }; +// cx.update(|cx| open_paths(&paths, &app_state, window_to_replace, cx)) +// .await?; +// Ok(()) +// }) +// } + +// #[allow(clippy::type_complexity)] +// pub fn open_paths( +// &mut self, +// mut abs_paths: Vec, +// visible: bool, +// cx: &mut ViewContext, +// ) -> Task, anyhow::Error>>>> { +// log::info!("open paths {:?}", abs_paths); + +// let fs = self.app_state.fs.clone(); + +// // Sort the paths to ensure we add worktrees for parents before their children. +// abs_paths.sort_unstable(); +// cx.spawn(|this, mut cx| async move { +// let mut tasks = Vec::with_capacity(abs_paths.len()); +// for abs_path in &abs_paths { +// let project_path = match this +// .update(&mut cx, |this, cx| { +// Workspace::project_path_for_path( +// this.project.clone(), +// abs_path, +// visible, +// cx, +// ) +// }) +// .log_err() +// { +// Some(project_path) => project_path.await.log_err(), +// None => None, +// }; + +// let this = this.clone(); +// let task = cx.spawn(|mut cx| { +// let fs = fs.clone(); +// let abs_path = abs_path.clone(); +// async move { +// let (worktree, project_path) = project_path?; +// if fs.is_file(&abs_path).await { +// Some( +// this.update(&mut cx, |this, cx| { +// this.open_path(project_path, None, true, cx) +// }) +// .log_err()? +// .await, +// ) +// } else { +// this.update(&mut cx, |workspace, cx| { +// let worktree = worktree.read(cx); +// let worktree_abs_path = worktree.abs_path(); +// let entry_id = if abs_path == worktree_abs_path.as_ref() { +// worktree.root_entry() +// } else { +// abs_path +// .strip_prefix(worktree_abs_path.as_ref()) +// .ok() +// .and_then(|relative_path| { +// worktree.entry_for_path(relative_path) +// }) +// } +// .map(|entry| entry.id); +// if let Some(entry_id) = entry_id { +// workspace.project().update(cx, |_, cx| { +// cx.emit(project::Event::ActiveEntryChanged(Some(entry_id))); +// }) +// } +// }) +// .log_err()?; +// None +// } +// } +// }); +// tasks.push(task); +// } + +// futures::future::join_all(tasks).await +// }) +// } + +// fn add_folder_to_project(&mut self, _: &AddFolderToProject, cx: &mut ViewContext) { +// let mut paths = cx.prompt_for_paths(PathPromptOptions { +// files: false, +// directories: true, +// multiple: true, +// }); +// cx.spawn(|this, mut cx| async move { +// if let Some(paths) = paths.recv().await.flatten() { +// let results = this +// .update(&mut cx, |this, cx| this.open_paths(paths, true, cx))? +// .await; +// for result in results.into_iter().flatten() { +// result.log_err(); +// } +// } +// anyhow::Ok(()) +// }) +// .detach_and_log_err(cx); +// } + +// fn project_path_for_path( +// project: ModelHandle, +// abs_path: &Path, +// visible: bool, +// cx: &mut AppContext, +// ) -> Task, ProjectPath)>> { +// let entry = project.update(cx, |project, cx| { +// project.find_or_create_local_worktree(abs_path, visible, cx) +// }); +// cx.spawn(|cx| async move { +// let (worktree, path) = entry.await?; +// let worktree_id = worktree.read_with(&cx, |t, _| t.id()); +// Ok(( +// worktree, +// ProjectPath { +// worktree_id, +// path: path.into(), +// }, +// )) +// }) +// } + +// /// Returns the modal that was toggled closed if it was open. +// pub fn toggle_modal( +// &mut self, +// cx: &mut ViewContext, +// add_view: F, +// ) -> Option> +// where +// V: 'static + Modal, +// F: FnOnce(&mut Self, &mut ViewContext) -> ViewHandle, +// { +// cx.notify(); +// // Whatever modal was visible is getting clobbered. If its the same type as V, then return +// // it. Otherwise, create a new modal and set it as active. +// if let Some(already_open_modal) = self +// .dismiss_modal(cx) +// .and_then(|modal| modal.downcast::()) +// { +// cx.focus_self(); +// Some(already_open_modal) +// } else { +// let modal = add_view(self, cx); +// cx.subscribe(&modal, |this, _, event, cx| { +// if V::dismiss_on_event(event) { +// this.dismiss_modal(cx); +// } +// }) +// .detach(); +// let previously_focused_view_id = cx.focused_view_id(); +// cx.focus(&modal); +// self.modal = Some(ActiveModal { +// view: Box::new(modal), +// previously_focused_view_id, +// }); +// None +// } +// } + +// pub fn modal(&self) -> Option> { +// self.modal +// .as_ref() +// .and_then(|modal| modal.view.as_any().clone().downcast::()) +// } + +// pub fn dismiss_modal(&mut self, cx: &mut ViewContext) -> Option { +// if let Some(modal) = self.modal.take() { +// if let Some(previously_focused_view_id) = modal.previously_focused_view_id { +// if modal.view.has_focus(cx) { +// cx.window_context().focus(Some(previously_focused_view_id)); +// } +// } +// cx.notify(); +// Some(modal.view.as_any().clone()) +// } else { +// None +// } +// } + +// pub fn items<'a>( +// &'a self, +// cx: &'a AppContext, +// ) -> impl 'a + Iterator> { +// self.panes.iter().flat_map(|pane| pane.read(cx).items()) +// } + +// pub fn item_of_type(&self, cx: &AppContext) -> Option> { +// self.items_of_type(cx).max_by_key(|item| item.id()) +// } + +// pub fn items_of_type<'a, T: Item>( +// &'a self, +// cx: &'a AppContext, +// ) -> impl 'a + Iterator> { +// self.panes +// .iter() +// .flat_map(|pane| pane.read(cx).items_of_type()) +// } + +// pub fn active_item(&self, cx: &AppContext) -> Option> { +// self.active_pane().read(cx).active_item() +// } + +// fn active_project_path(&self, cx: &ViewContext) -> Option { +// self.active_item(cx).and_then(|item| item.project_path(cx)) +// } + +// pub fn save_active_item( +// &mut self, +// save_intent: SaveIntent, +// cx: &mut ViewContext, +// ) -> Task> { +// let project = self.project.clone(); +// let pane = self.active_pane(); +// let item_ix = pane.read(cx).active_item_index(); +// let item = pane.read(cx).active_item(); +// let pane = pane.downgrade(); + +// cx.spawn(|_, mut cx| async move { +// if let Some(item) = item { +// Pane::save_item(project, &pane, item_ix, item.as_ref(), save_intent, &mut cx) +// .await +// .map(|_| ()) +// } else { +// Ok(()) +// } +// }) +// } + +// pub fn close_inactive_items_and_panes( +// &mut self, +// _: &CloseInactiveTabsAndPanes, +// cx: &mut ViewContext, +// ) -> Option>> { +// self.close_all_internal(true, SaveIntent::Close, cx) +// } + +// pub fn close_all_items_and_panes( +// &mut self, +// action: &CloseAllItemsAndPanes, +// cx: &mut ViewContext, +// ) -> Option>> { +// self.close_all_internal(false, action.save_intent.unwrap_or(SaveIntent::Close), cx) +// } + +// fn close_all_internal( +// &mut self, +// retain_active_pane: bool, +// save_intent: SaveIntent, +// cx: &mut ViewContext, +// ) -> Option>> { +// let current_pane = self.active_pane(); + +// let mut tasks = Vec::new(); + +// if retain_active_pane { +// if let Some(current_pane_close) = current_pane.update(cx, |pane, cx| { +// pane.close_inactive_items(&CloseInactiveItems, cx) +// }) { +// tasks.push(current_pane_close); +// }; +// } + +// for pane in self.panes() { +// if retain_active_pane && pane.id() == current_pane.id() { +// continue; +// } + +// if let Some(close_pane_items) = pane.update(cx, |pane: &mut Pane, cx| { +// pane.close_all_items( +// &CloseAllItems { +// save_intent: Some(save_intent), +// }, +// cx, +// ) +// }) { +// tasks.push(close_pane_items) +// } +// } + +// if tasks.is_empty() { +// None +// } else { +// Some(cx.spawn(|_, _| async move { +// for task in tasks { +// task.await? +// } +// Ok(()) +// })) +// } +// } + +// pub fn toggle_dock(&mut self, dock_side: DockPosition, cx: &mut ViewContext) { +// let dock = match dock_side { +// DockPosition::Left => &self.left_dock, +// DockPosition::Bottom => &self.bottom_dock, +// DockPosition::Right => &self.right_dock, +// }; +// let mut focus_center = false; +// let mut reveal_dock = false; +// dock.update(cx, |dock, cx| { +// let other_is_zoomed = self.zoomed.is_some() && self.zoomed_position != Some(dock_side); +// let was_visible = dock.is_open() && !other_is_zoomed; +// dock.set_open(!was_visible, cx); + +// if let Some(active_panel) = dock.active_panel() { +// if was_visible { +// if active_panel.has_focus(cx) { +// focus_center = true; +// } +// } else { +// cx.focus(active_panel.as_any()); +// reveal_dock = true; +// } +// } +// }); + +// if reveal_dock { +// self.dismiss_zoomed_items_to_reveal(Some(dock_side), cx); +// } + +// if focus_center { +// cx.focus_self(); +// } + +// cx.notify(); +// self.serialize_workspace(cx); +// } + +// pub fn close_all_docks(&mut self, cx: &mut ViewContext) { +// let docks = [&self.left_dock, &self.bottom_dock, &self.right_dock]; + +// for dock in docks { +// dock.update(cx, |dock, cx| { +// dock.set_open(false, cx); +// }); +// } + +// cx.focus_self(); +// cx.notify(); +// self.serialize_workspace(cx); +// } + +// /// Transfer focus to the panel of the given type. +// pub fn focus_panel(&mut self, cx: &mut ViewContext) -> Option> { +// self.focus_or_unfocus_panel::(cx, |_, _| true)? +// .as_any() +// .clone() +// .downcast() +// } + +// /// Focus the panel of the given type if it isn't already focused. If it is +// /// already focused, then transfer focus back to the workspace center. +// pub fn toggle_panel_focus(&mut self, cx: &mut ViewContext) { +// self.focus_or_unfocus_panel::(cx, |panel, cx| !panel.has_focus(cx)); +// } + +// /// Focus or unfocus the given panel type, depending on the given callback. +// fn focus_or_unfocus_panel( +// &mut self, +// cx: &mut ViewContext, +// should_focus: impl Fn(&dyn PanelHandle, &mut ViewContext) -> bool, +// ) -> Option> { +// for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] { +// if let Some(panel_index) = dock.read(cx).panel_index_for_type::() { +// let mut focus_center = false; +// let mut reveal_dock = false; +// let panel = dock.update(cx, |dock, cx| { +// dock.activate_panel(panel_index, cx); + +// let panel = dock.active_panel().cloned(); +// if let Some(panel) = panel.as_ref() { +// if should_focus(&**panel, cx) { +// dock.set_open(true, cx); +// cx.focus(panel.as_any()); +// reveal_dock = true; +// } else { +// // if panel.is_zoomed(cx) { +// // dock.set_open(false, cx); +// // } +// focus_center = true; +// } +// } +// panel +// }); + +// if focus_center { +// cx.focus_self(); +// } + +// self.serialize_workspace(cx); +// cx.notify(); +// return panel; +// } +// } +// None +// } + +// pub fn panel(&self, cx: &WindowContext) -> Option> { +// for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] { +// let dock = dock.read(cx); +// if let Some(panel) = dock.panel::() { +// return Some(panel); +// } +// } +// None +// } + +// fn zoom_out(&mut self, cx: &mut ViewContext) { +// for pane in &self.panes { +// pane.update(cx, |pane, cx| pane.set_zoomed(false, cx)); +// } + +// self.left_dock.update(cx, |dock, cx| dock.zoom_out(cx)); +// self.bottom_dock.update(cx, |dock, cx| dock.zoom_out(cx)); +// self.right_dock.update(cx, |dock, cx| dock.zoom_out(cx)); +// self.zoomed = None; +// self.zoomed_position = None; + +// cx.notify(); +// } + +// #[cfg(any(test, feature = "test-support"))] +// pub fn zoomed_view(&self, cx: &AppContext) -> Option { +// self.zoomed.and_then(|view| view.upgrade(cx)) +// } + +// fn dismiss_zoomed_items_to_reveal( +// &mut self, +// dock_to_reveal: Option, +// cx: &mut ViewContext, +// ) { +// // If a center pane is zoomed, unzoom it. +// for pane in &self.panes { +// if pane != &self.active_pane || dock_to_reveal.is_some() { +// pane.update(cx, |pane, cx| pane.set_zoomed(false, cx)); +// } +// } + +// // If another dock is zoomed, hide it. +// let mut focus_center = false; +// for dock in [&self.left_dock, &self.right_dock, &self.bottom_dock] { +// dock.update(cx, |dock, cx| { +// if Some(dock.position()) != dock_to_reveal { +// if let Some(panel) = dock.active_panel() { +// if panel.is_zoomed(cx) { +// focus_center |= panel.has_focus(cx); +// dock.set_open(false, cx); +// } +// } +// } +// }); +// } + +// if focus_center { +// cx.focus_self(); +// } + +// if self.zoomed_position != dock_to_reveal { +// self.zoomed = None; +// self.zoomed_position = None; +// } + +// cx.notify(); +// } + +// fn add_pane(&mut self, cx: &mut ViewContext) -> ViewHandle { +// let pane = cx.add_view(|cx| { +// Pane::new( +// self.weak_handle(), +// self.project.clone(), +// self.app_state.background_actions, +// self.pane_history_timestamp.clone(), +// cx, +// ) +// }); +// cx.subscribe(&pane, Self::handle_pane_event).detach(); +// self.panes.push(pane.clone()); +// cx.focus(&pane); +// cx.emit(Event::PaneAdded(pane.clone())); +// pane +// } + +// pub fn add_item_to_center( +// &mut self, +// item: Box, +// cx: &mut ViewContext, +// ) -> bool { +// if let Some(center_pane) = self.last_active_center_pane.clone() { +// if let Some(center_pane) = center_pane.upgrade(cx) { +// center_pane.update(cx, |pane, cx| pane.add_item(item, true, true, None, cx)); +// true +// } else { +// false +// } +// } else { +// false +// } +// } + +// pub fn add_item(&mut self, item: Box, cx: &mut ViewContext) { +// self.active_pane +// .update(cx, |pane, cx| pane.add_item(item, true, true, None, cx)); +// } + +// pub fn split_item( +// &mut self, +// split_direction: SplitDirection, +// item: Box, +// cx: &mut ViewContext, +// ) { +// let new_pane = self.split_pane(self.active_pane.clone(), split_direction, cx); +// new_pane.update(cx, move |new_pane, cx| { +// new_pane.add_item(item, true, true, None, cx) +// }) +// } + +// pub fn open_abs_path( +// &mut self, +// abs_path: PathBuf, +// visible: bool, +// cx: &mut ViewContext, +// ) -> Task>> { +// cx.spawn(|workspace, mut cx| async move { +// let open_paths_task_result = workspace +// .update(&mut cx, |workspace, cx| { +// workspace.open_paths(vec![abs_path.clone()], visible, cx) +// }) +// .with_context(|| format!("open abs path {abs_path:?} task spawn"))? +// .await; +// anyhow::ensure!( +// open_paths_task_result.len() == 1, +// "open abs path {abs_path:?} task returned incorrect number of results" +// ); +// match open_paths_task_result +// .into_iter() +// .next() +// .expect("ensured single task result") +// { +// Some(open_result) => { +// open_result.with_context(|| format!("open abs path {abs_path:?} task join")) +// } +// None => anyhow::bail!("open abs path {abs_path:?} task returned None"), +// } +// }) +// } + +// pub fn split_abs_path( +// &mut self, +// abs_path: PathBuf, +// visible: bool, +// cx: &mut ViewContext, +// ) -> Task>> { +// let project_path_task = +// Workspace::project_path_for_path(self.project.clone(), &abs_path, visible, cx); +// cx.spawn(|this, mut cx| async move { +// let (_, path) = project_path_task.await?; +// this.update(&mut cx, |this, cx| this.split_path(path, cx))? +// .await +// }) +// } + +// pub fn open_path( +// &mut self, +// path: impl Into, +// pane: Option>, +// focus_item: bool, +// cx: &mut ViewContext, +// ) -> Task, anyhow::Error>> { +// let pane = pane.unwrap_or_else(|| { +// self.last_active_center_pane.clone().unwrap_or_else(|| { +// self.panes +// .first() +// .expect("There must be an active pane") +// .downgrade() +// }) +// }); + +// let task = self.load_path(path.into(), cx); +// cx.spawn(|_, mut cx| async move { +// let (project_entry_id, build_item) = task.await?; +// pane.update(&mut cx, |pane, cx| { +// pane.open_item(project_entry_id, focus_item, cx, build_item) +// }) +// }) +// } + +// pub fn split_path( +// &mut self, +// path: impl Into, +// cx: &mut ViewContext, +// ) -> Task, anyhow::Error>> { +// let pane = self.last_active_center_pane.clone().unwrap_or_else(|| { +// self.panes +// .first() +// .expect("There must be an active pane") +// .downgrade() +// }); + +// if let Member::Pane(center_pane) = &self.center.root { +// if center_pane.read(cx).items_len() == 0 { +// return self.open_path(path, Some(pane), true, cx); +// } +// } + +// let task = self.load_path(path.into(), cx); +// cx.spawn(|this, mut cx| async move { +// let (project_entry_id, build_item) = task.await?; +// this.update(&mut cx, move |this, cx| -> Option<_> { +// let pane = pane.upgrade(cx)?; +// let new_pane = this.split_pane(pane, SplitDirection::Right, cx); +// new_pane.update(cx, |new_pane, cx| { +// Some(new_pane.open_item(project_entry_id, true, cx, build_item)) +// }) +// }) +// .map(|option| option.ok_or_else(|| anyhow!("pane was dropped")))? +// }) +// } + +// pub(crate) fn load_path( +// &mut self, +// path: ProjectPath, +// cx: &mut ViewContext, +// ) -> Task< +// Result<( +// ProjectEntryId, +// impl 'static + FnOnce(&mut ViewContext) -> Box, +// )>, +// > { +// let project = self.project().clone(); +// let project_item = project.update(cx, |project, cx| project.open_path(path, cx)); +// cx.spawn(|_, mut cx| async move { +// let (project_entry_id, project_item) = project_item.await?; +// let build_item = cx.update(|cx| { +// cx.default_global::() +// .get(&project_item.model_type()) +// .ok_or_else(|| anyhow!("no item builder for project item")) +// .cloned() +// })?; +// let build_item = +// move |cx: &mut ViewContext| build_item(project, project_item, cx); +// Ok((project_entry_id, build_item)) +// }) +// } + +// pub fn open_project_item( +// &mut self, +// project_item: ModelHandle, +// cx: &mut ViewContext, +// ) -> ViewHandle +// where +// T: ProjectItem, +// { +// use project::Item as _; + +// let entry_id = project_item.read(cx).entry_id(cx); +// if let Some(item) = entry_id +// .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id, cx)) +// .and_then(|item| item.downcast()) +// { +// self.activate_item(&item, cx); +// return item; +// } + +// let item = cx.add_view(|cx| T::for_project_item(self.project().clone(), project_item, cx)); +// self.add_item(Box::new(item.clone()), cx); +// item +// } + +// pub fn split_project_item( +// &mut self, +// project_item: ModelHandle, +// cx: &mut ViewContext, +// ) -> ViewHandle +// where +// T: ProjectItem, +// { +// use project::Item as _; + +// let entry_id = project_item.read(cx).entry_id(cx); +// if let Some(item) = entry_id +// .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id, cx)) +// .and_then(|item| item.downcast()) +// { +// self.activate_item(&item, cx); +// return item; +// } + +// let item = cx.add_view(|cx| T::for_project_item(self.project().clone(), project_item, cx)); +// self.split_item(SplitDirection::Right, Box::new(item.clone()), cx); +// item +// } + +// pub fn open_shared_screen(&mut self, peer_id: PeerId, cx: &mut ViewContext) { +// if let Some(shared_screen) = self.shared_screen_for_peer(peer_id, &self.active_pane, cx) { +// self.active_pane.update(cx, |pane, cx| { +// pane.add_item(Box::new(shared_screen), false, true, None, cx) +// }); +// } +// } + +// pub fn activate_item(&mut self, item: &dyn ItemHandle, cx: &mut ViewContext) -> bool { +// let result = self.panes.iter().find_map(|pane| { +// pane.read(cx) +// .index_for_item(item) +// .map(|ix| (pane.clone(), ix)) +// }); +// if let Some((pane, ix)) = result { +// pane.update(cx, |pane, cx| pane.activate_item(ix, true, true, cx)); +// true +// } else { +// false +// } +// } + +// fn activate_pane_at_index(&mut self, action: &ActivatePane, cx: &mut ViewContext) { +// let panes = self.center.panes(); +// if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) { +// cx.focus(&pane); +// } else { +// self.split_and_clone(self.active_pane.clone(), SplitDirection::Right, cx); +// } +// } + +// pub fn activate_next_pane(&mut self, cx: &mut ViewContext) { +// let panes = self.center.panes(); +// if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) { +// let next_ix = (ix + 1) % panes.len(); +// let next_pane = panes[next_ix].clone(); +// cx.focus(&next_pane); +// } +// } + +// pub fn activate_previous_pane(&mut self, cx: &mut ViewContext) { +// let panes = self.center.panes(); +// if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) { +// let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1); +// let prev_pane = panes[prev_ix].clone(); +// cx.focus(&prev_pane); +// } +// } + +// pub fn activate_pane_in_direction( +// &mut self, +// direction: SplitDirection, +// cx: &mut ViewContext, +// ) { +// if let Some(pane) = self.find_pane_in_direction(direction, cx) { +// cx.focus(pane); +// } +// } + +// pub fn swap_pane_in_direction( +// &mut self, +// direction: SplitDirection, +// cx: &mut ViewContext, +// ) { +// if let Some(to) = self +// .find_pane_in_direction(direction, cx) +// .map(|pane| pane.clone()) +// { +// self.center.swap(&self.active_pane.clone(), &to); +// cx.notify(); +// } +// } + +// fn find_pane_in_direction( +// &mut self, +// direction: SplitDirection, +// cx: &mut ViewContext, +// ) -> Option<&ViewHandle> { +// let Some(bounding_box) = self.center.bounding_box_for_pane(&self.active_pane) else { +// return None; +// }; +// let cursor = self.active_pane.read(cx).pixel_position_of_cursor(cx); +// let center = match cursor { +// Some(cursor) if bounding_box.contains_point(cursor) => cursor, +// _ => bounding_box.center(), +// }; + +// let distance_to_next = theme::current(cx).workspace.pane_divider.width + 1.; + +// let target = match direction { +// SplitDirection::Left => vec2f(bounding_box.origin_x() - distance_to_next, center.y()), +// SplitDirection::Right => vec2f(bounding_box.max_x() + distance_to_next, center.y()), +// SplitDirection::Up => vec2f(center.x(), bounding_box.origin_y() - distance_to_next), +// SplitDirection::Down => vec2f(center.x(), bounding_box.max_y() + distance_to_next), +// }; +// self.center.pane_at_pixel_position(target) +// } + +// fn handle_pane_focused(&mut self, pane: ViewHandle, cx: &mut ViewContext) { +// if self.active_pane != pane { +// self.active_pane = pane.clone(); +// self.status_bar.update(cx, |status_bar, cx| { +// status_bar.set_active_pane(&self.active_pane, cx); +// }); +// self.active_item_path_changed(cx); +// self.last_active_center_pane = Some(pane.downgrade()); +// } + +// self.dismiss_zoomed_items_to_reveal(None, cx); +// if pane.read(cx).is_zoomed() { +// self.zoomed = Some(pane.downgrade().into_any()); +// } else { +// self.zoomed = None; +// } +// self.zoomed_position = None; +// self.update_active_view_for_followers(cx); + +// cx.notify(); +// } + +// fn handle_pane_event( +// &mut self, +// pane: ViewHandle, +// event: &pane::Event, +// cx: &mut ViewContext, +// ) { +// match event { +// pane::Event::AddItem { item } => item.added_to_pane(self, pane, cx), +// pane::Event::Split(direction) => { +// self.split_and_clone(pane, *direction, cx); +// } +// pane::Event::Remove => self.remove_pane(pane, cx), +// pane::Event::ActivateItem { local } => { +// if *local { +// self.unfollow(&pane, cx); +// } +// if &pane == self.active_pane() { +// self.active_item_path_changed(cx); +// } +// } +// pane::Event::ChangeItemTitle => { +// if pane == self.active_pane { +// self.active_item_path_changed(cx); +// } +// self.update_window_edited(cx); +// } +// pane::Event::RemoveItem { item_id } => { +// self.update_window_edited(cx); +// if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) { +// if entry.get().id() == pane.id() { +// entry.remove(); +// } +// } +// } +// pane::Event::Focus => { +// self.handle_pane_focused(pane.clone(), cx); +// } +// pane::Event::ZoomIn => { +// if pane == self.active_pane { +// pane.update(cx, |pane, cx| pane.set_zoomed(true, cx)); +// if pane.read(cx).has_focus() { +// self.zoomed = Some(pane.downgrade().into_any()); +// self.zoomed_position = None; +// } +// cx.notify(); +// } +// } +// pane::Event::ZoomOut => { +// pane.update(cx, |pane, cx| pane.set_zoomed(false, cx)); +// if self.zoomed_position.is_none() { +// self.zoomed = None; +// } +// cx.notify(); +// } +// } + +// self.serialize_workspace(cx); +// } + +// pub fn split_pane( +// &mut self, +// pane_to_split: ViewHandle, +// split_direction: SplitDirection, +// cx: &mut ViewContext, +// ) -> ViewHandle { +// let new_pane = self.add_pane(cx); +// self.center +// .split(&pane_to_split, &new_pane, split_direction) +// .unwrap(); +// cx.notify(); +// new_pane +// } + +// pub fn split_and_clone( +// &mut self, +// pane: ViewHandle, +// direction: SplitDirection, +// cx: &mut ViewContext, +// ) -> Option> { +// let item = pane.read(cx).active_item()?; +// let maybe_pane_handle = if let Some(clone) = item.clone_on_split(self.database_id(), cx) { +// let new_pane = self.add_pane(cx); +// new_pane.update(cx, |pane, cx| pane.add_item(clone, true, true, None, cx)); +// self.center.split(&pane, &new_pane, direction).unwrap(); +// Some(new_pane) +// } else { +// None +// }; +// cx.notify(); +// maybe_pane_handle +// } + +// pub fn split_pane_with_item( +// &mut self, +// pane_to_split: WeakViewHandle, +// split_direction: SplitDirection, +// from: WeakViewHandle, +// item_id_to_move: usize, +// cx: &mut ViewContext, +// ) { +// let Some(pane_to_split) = pane_to_split.upgrade(cx) else { +// return; +// }; +// let Some(from) = from.upgrade(cx) else { +// return; +// }; + +// let new_pane = self.add_pane(cx); +// self.move_item(from.clone(), new_pane.clone(), item_id_to_move, 0, cx); +// self.center +// .split(&pane_to_split, &new_pane, split_direction) +// .unwrap(); +// cx.notify(); +// } + +// pub fn split_pane_with_project_entry( +// &mut self, +// pane_to_split: WeakViewHandle, +// split_direction: SplitDirection, +// project_entry: ProjectEntryId, +// cx: &mut ViewContext, +// ) -> Option>> { +// let pane_to_split = pane_to_split.upgrade(cx)?; +// let new_pane = self.add_pane(cx); +// self.center +// .split(&pane_to_split, &new_pane, split_direction) +// .unwrap(); + +// let path = self.project.read(cx).path_for_entry(project_entry, cx)?; +// let task = self.open_path(path, Some(new_pane.downgrade()), true, cx); +// Some(cx.foreground().spawn(async move { +// task.await?; +// Ok(()) +// })) +// } + +// pub fn move_item( +// &mut self, +// source: ViewHandle, +// destination: ViewHandle, +// item_id_to_move: usize, +// destination_index: usize, +// cx: &mut ViewContext, +// ) { +// let item_to_move = source +// .read(cx) +// .items() +// .enumerate() +// .find(|(_, item_handle)| item_handle.id() == item_id_to_move); + +// if item_to_move.is_none() { +// log::warn!("Tried to move item handle which was not in `from` pane. Maybe tab was closed during drop"); +// return; +// } +// let (item_ix, item_handle) = item_to_move.unwrap(); +// let item_handle = item_handle.clone(); + +// if source != destination { +// // Close item from previous pane +// source.update(cx, |source, cx| { +// source.remove_item(item_ix, false, cx); +// }); +// } + +// // This automatically removes duplicate items in the pane +// destination.update(cx, |destination, cx| { +// destination.add_item(item_handle, true, true, Some(destination_index), cx); +// cx.focus_self(); +// }); +// } + +// fn remove_pane(&mut self, pane: ViewHandle, cx: &mut ViewContext) { +// if self.center.remove(&pane).unwrap() { +// self.force_remove_pane(&pane, cx); +// self.unfollow(&pane, cx); +// self.last_leaders_by_pane.remove(&pane.downgrade()); +// for removed_item in pane.read(cx).items() { +// self.panes_by_item.remove(&removed_item.id()); +// } + +// cx.notify(); +// } else { +// self.active_item_path_changed(cx); +// } +// } + +// pub fn panes(&self) -> &[ViewHandle] { +// &self.panes +// } + +// pub fn active_pane(&self) -> &ViewHandle { +// &self.active_pane +// } + +// fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext) { +// self.follower_states.retain(|_, state| { +// if state.leader_id == peer_id { +// for item in state.items_by_leader_view_id.values() { +// item.set_leader_peer_id(None, cx); +// } +// false +// } else { +// true +// } +// }); +// cx.notify(); +// } + +// fn start_following( +// &mut self, +// leader_id: PeerId, +// cx: &mut ViewContext, +// ) -> Option>> { +// let pane = self.active_pane().clone(); + +// self.last_leaders_by_pane +// .insert(pane.downgrade(), leader_id); +// self.unfollow(&pane, cx); +// self.follower_states.insert( +// pane.clone(), +// FollowerState { +// leader_id, +// active_view_id: None, +// items_by_leader_view_id: Default::default(), +// }, +// ); +// cx.notify(); + +// let room_id = self.active_call()?.read(cx).room()?.read(cx).id(); +// let project_id = self.project.read(cx).remote_id(); +// let request = self.app_state.client.request(proto::Follow { +// room_id, +// project_id, +// leader_id: Some(leader_id), +// }); + +// Some(cx.spawn(|this, mut cx| async move { +// let response = request.await?; +// this.update(&mut cx, |this, _| { +// let state = this +// .follower_states +// .get_mut(&pane) +// .ok_or_else(|| anyhow!("following interrupted"))?; +// state.active_view_id = if let Some(active_view_id) = response.active_view_id { +// Some(ViewId::from_proto(active_view_id)?) +// } else { +// None +// }; +// Ok::<_, anyhow::Error>(()) +// })??; +// Self::add_views_from_leader( +// this.clone(), +// leader_id, +// vec![pane], +// response.views, +// &mut cx, +// ) +// .await?; +// this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx))?; +// Ok(()) +// })) +// } + +// pub fn follow_next_collaborator( +// &mut self, +// _: &FollowNextCollaborator, +// cx: &mut ViewContext, +// ) -> Option>> { +// let collaborators = self.project.read(cx).collaborators(); +// let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) { +// let mut collaborators = collaborators.keys().copied(); +// for peer_id in collaborators.by_ref() { +// if peer_id == leader_id { +// break; +// } +// } +// collaborators.next() +// } else if let Some(last_leader_id) = +// self.last_leaders_by_pane.get(&self.active_pane.downgrade()) +// { +// if collaborators.contains_key(last_leader_id) { +// Some(*last_leader_id) +// } else { +// None +// } +// } else { +// None +// }; + +// let pane = self.active_pane.clone(); +// let Some(leader_id) = next_leader_id.or_else(|| collaborators.keys().copied().next()) +// else { +// return None; +// }; +// if Some(leader_id) == self.unfollow(&pane, cx) { +// return None; +// } +// self.follow(leader_id, cx) +// } + +// pub fn follow( +// &mut self, +// leader_id: PeerId, +// cx: &mut ViewContext, +// ) -> Option>> { +// let room = ActiveCall::global(cx).read(cx).room()?.read(cx); +// let project = self.project.read(cx); + +// let Some(remote_participant) = room.remote_participant_for_peer_id(leader_id) else { +// return None; +// }; + +// let other_project_id = match remote_participant.location { +// call::ParticipantLocation::External => None, +// call::ParticipantLocation::UnsharedProject => None, +// call::ParticipantLocation::SharedProject { project_id } => { +// if Some(project_id) == project.remote_id() { +// None +// } else { +// Some(project_id) +// } +// } +// }; + +// // if they are active in another project, follow there. +// if let Some(project_id) = other_project_id { +// let app_state = self.app_state.clone(); +// return Some(crate::join_remote_project( +// project_id, +// remote_participant.user.id, +// app_state, +// cx, +// )); +// } + +// // if you're already following, find the right pane and focus it. +// for (pane, state) in &self.follower_states { +// if leader_id == state.leader_id { +// cx.focus(pane); +// return None; +// } +// } + +// // Otherwise, follow. +// self.start_following(leader_id, cx) +// } + +// pub fn unfollow( +// &mut self, +// pane: &ViewHandle, +// cx: &mut ViewContext, +// ) -> Option { +// let state = self.follower_states.remove(pane)?; +// let leader_id = state.leader_id; +// for (_, item) in state.items_by_leader_view_id { +// item.set_leader_peer_id(None, cx); +// } + +// if self +// .follower_states +// .values() +// .all(|state| state.leader_id != state.leader_id) +// { +// let project_id = self.project.read(cx).remote_id(); +// let room_id = self.active_call()?.read(cx).room()?.read(cx).id(); +// self.app_state +// .client +// .send(proto::Unfollow { +// room_id, +// project_id, +// leader_id: Some(leader_id), +// }) +// .log_err(); +// } + +// cx.notify(); +// Some(leader_id) +// } + +// pub fn is_being_followed(&self, peer_id: PeerId) -> bool { +// self.follower_states +// .values() +// .any(|state| state.leader_id == peer_id) +// } + +// fn render_titlebar(&self, theme: &Theme, cx: &mut ViewContext) -> AnyElement { +// // TODO: There should be a better system in place for this +// // (https://github.com/zed-industries/zed/issues/1290) +// let is_fullscreen = cx.window_is_fullscreen(); +// let container_theme = if is_fullscreen { +// let mut container_theme = theme.titlebar.container; +// container_theme.padding.left = container_theme.padding.right; +// container_theme +// } else { +// theme.titlebar.container +// }; + +// enum TitleBar {} +// MouseEventHandler::new::(0, cx, |_, cx| { +// Stack::new() +// .with_children( +// self.titlebar_item +// .as_ref() +// .map(|item| ChildView::new(item, cx)), +// ) +// .contained() +// .with_style(container_theme) +// }) +// .on_click(MouseButton::Left, |event, _, cx| { +// if event.click_count == 2 { +// cx.zoom_window(); +// } +// }) +// .constrained() +// .with_height(theme.titlebar.height) +// .into_any_named("titlebar") +// } + +// fn active_item_path_changed(&mut self, cx: &mut ViewContext) { +// let active_entry = self.active_project_path(cx); +// self.project +// .update(cx, |project, cx| project.set_active_path(active_entry, cx)); +// self.update_window_title(cx); +// } + +// fn update_window_title(&mut self, cx: &mut ViewContext) { +// let project = self.project().read(cx); +// let mut title = String::new(); + +// if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) { +// let filename = path +// .path +// .file_name() +// .map(|s| s.to_string_lossy()) +// .or_else(|| { +// Some(Cow::Borrowed( +// project +// .worktree_for_id(path.worktree_id, cx)? +// .read(cx) +// .root_name(), +// )) +// }); + +// if let Some(filename) = filename { +// title.push_str(filename.as_ref()); +// title.push_str(" — "); +// } +// } + +// for (i, name) in project.worktree_root_names(cx).enumerate() { +// if i > 0 { +// title.push_str(", "); +// } +// title.push_str(name); +// } + +// if title.is_empty() { +// title = "empty project".to_string(); +// } + +// if project.is_remote() { +// title.push_str(" ↙"); +// } else if project.is_shared() { +// title.push_str(" ↗"); +// } + +// cx.set_window_title(&title); +// } + +// fn update_window_edited(&mut self, cx: &mut ViewContext) { +// let is_edited = !self.project.read(cx).is_read_only() +// && self +// .items(cx) +// .any(|item| item.has_conflict(cx) || item.is_dirty(cx)); +// if is_edited != self.window_edited { +// self.window_edited = is_edited; +// cx.set_window_edited(self.window_edited) +// } +// } + +// fn render_disconnected_overlay( +// &self, +// cx: &mut ViewContext, +// ) -> Option> { +// if self.project.read(cx).is_read_only() { +// enum DisconnectedOverlay {} +// Some( +// MouseEventHandler::new::(0, cx, |_, cx| { +// let theme = &theme::current(cx); +// Label::new( +// "Your connection to the remote project has been lost.", +// theme.workspace.disconnected_overlay.text.clone(), +// ) +// .aligned() +// .contained() +// .with_style(theme.workspace.disconnected_overlay.container) +// }) +// .with_cursor_style(CursorStyle::Arrow) +// .capture_all() +// .into_any_named("disconnected overlay"), +// ) +// } else { +// None +// } +// } + +// fn render_notifications( +// &self, +// theme: &theme::Workspace, +// cx: &AppContext, +// ) -> Option> { +// if self.notifications.is_empty() { +// None +// } else { +// Some( +// Flex::column() +// .with_children(self.notifications.iter().map(|(_, _, notification)| { +// ChildView::new(notification.as_any(), cx) +// .contained() +// .with_style(theme.notification) +// })) +// .constrained() +// .with_width(theme.notifications.width) +// .contained() +// .with_style(theme.notifications.container) +// .aligned() +// .bottom() +// .right() +// .into_any(), +// ) +// } +// } + +// // RPC handlers + +// fn handle_follow( +// &mut self, +// follower_project_id: Option, +// cx: &mut ViewContext, +// ) -> proto::FollowResponse { +// let client = &self.app_state.client; +// let project_id = self.project.read(cx).remote_id(); + +// let active_view_id = self.active_item(cx).and_then(|i| { +// Some( +// i.to_followable_item_handle(cx)? +// .remote_id(client, cx)? +// .to_proto(), +// ) +// }); + +// cx.notify(); + +// self.last_active_view_id = active_view_id.clone(); +// proto::FollowResponse { +// active_view_id, +// views: self +// .panes() +// .iter() +// .flat_map(|pane| { +// let leader_id = self.leader_for_pane(pane); +// pane.read(cx).items().filter_map({ +// let cx = &cx; +// move |item| { +// let item = item.to_followable_item_handle(cx)?; +// if (project_id.is_none() || project_id != follower_project_id) +// && item.is_project_item(cx) +// { +// return None; +// } +// let id = item.remote_id(client, cx)?.to_proto(); +// let variant = item.to_state_proto(cx)?; +// Some(proto::View { +// id: Some(id), +// leader_id, +// variant: Some(variant), +// }) +// } +// }) +// }) +// .collect(), +// } +// } + +// fn handle_update_followers( +// &mut self, +// leader_id: PeerId, +// message: proto::UpdateFollowers, +// _cx: &mut ViewContext, +// ) { +// self.leader_updates_tx +// .unbounded_send((leader_id, message)) +// .ok(); +// } + +// async fn process_leader_update( +// this: &WeakViewHandle, +// leader_id: PeerId, +// update: proto::UpdateFollowers, +// cx: &mut AsyncAppContext, +// ) -> Result<()> { +// match update.variant.ok_or_else(|| anyhow!("invalid update"))? { +// proto::update_followers::Variant::UpdateActiveView(update_active_view) => { +// this.update(cx, |this, _| { +// for (_, state) in &mut this.follower_states { +// if state.leader_id == leader_id { +// state.active_view_id = +// if let Some(active_view_id) = update_active_view.id.clone() { +// Some(ViewId::from_proto(active_view_id)?) +// } else { +// None +// }; +// } +// } +// anyhow::Ok(()) +// })??; +// } +// proto::update_followers::Variant::UpdateView(update_view) => { +// let variant = update_view +// .variant +// .ok_or_else(|| anyhow!("missing update view variant"))?; +// let id = update_view +// .id +// .ok_or_else(|| anyhow!("missing update view id"))?; +// let mut tasks = Vec::new(); +// this.update(cx, |this, cx| { +// let project = this.project.clone(); +// for (_, state) in &mut this.follower_states { +// if state.leader_id == leader_id { +// let view_id = ViewId::from_proto(id.clone())?; +// if let Some(item) = state.items_by_leader_view_id.get(&view_id) { +// tasks.push(item.apply_update_proto(&project, variant.clone(), cx)); +// } +// } +// } +// anyhow::Ok(()) +// })??; +// try_join_all(tasks).await.log_err(); +// } +// proto::update_followers::Variant::CreateView(view) => { +// let panes = this.read_with(cx, |this, _| { +// this.follower_states +// .iter() +// .filter_map(|(pane, state)| (state.leader_id == leader_id).then_some(pane)) +// .cloned() +// .collect() +// })?; +// Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], cx).await?; +// } +// } +// this.update(cx, |this, cx| this.leader_updated(leader_id, cx))?; +// Ok(()) +// } + +// async fn add_views_from_leader( +// this: WeakViewHandle, +// leader_id: PeerId, +// panes: Vec>, +// views: Vec, +// cx: &mut AsyncAppContext, +// ) -> Result<()> { +// let this = this +// .upgrade(cx) +// .ok_or_else(|| anyhow!("workspace dropped"))?; + +// let item_builders = cx.update(|cx| { +// cx.default_global::() +// .values() +// .map(|b| b.0) +// .collect::>() +// }); + +// let mut item_tasks_by_pane = HashMap::default(); +// for pane in panes { +// let mut item_tasks = Vec::new(); +// let mut leader_view_ids = Vec::new(); +// for view in &views { +// let Some(id) = &view.id else { continue }; +// let id = ViewId::from_proto(id.clone())?; +// let mut variant = view.variant.clone(); +// if variant.is_none() { +// Err(anyhow!("missing view variant"))?; +// } +// for build_item in &item_builders { +// let task = cx +// .update(|cx| build_item(pane.clone(), this.clone(), id, &mut variant, cx)); +// if let Some(task) = task { +// item_tasks.push(task); +// leader_view_ids.push(id); +// break; +// } else { +// assert!(variant.is_some()); +// } +// } +// } + +// item_tasks_by_pane.insert(pane, (item_tasks, leader_view_ids)); +// } + +// for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane { +// let items = futures::future::try_join_all(item_tasks).await?; +// this.update(cx, |this, cx| { +// let state = this.follower_states.get_mut(&pane)?; +// for (id, item) in leader_view_ids.into_iter().zip(items) { +// item.set_leader_peer_id(Some(leader_id), cx); +// state.items_by_leader_view_id.insert(id, item); +// } + +// Some(()) +// }); +// } +// Ok(()) +// } + +// fn update_active_view_for_followers(&mut self, cx: &AppContext) { +// let mut is_project_item = true; +// let mut update = proto::UpdateActiveView::default(); +// if self.active_pane.read(cx).has_focus() { +// let item = self +// .active_item(cx) +// .and_then(|item| item.to_followable_item_handle(cx)); +// if let Some(item) = item { +// is_project_item = item.is_project_item(cx); +// update = proto::UpdateActiveView { +// id: item +// .remote_id(&self.app_state.client, cx) +// .map(|id| id.to_proto()), +// leader_id: self.leader_for_pane(&self.active_pane), +// }; +// } +// } + +// if update.id != self.last_active_view_id { +// self.last_active_view_id = update.id.clone(); +// self.update_followers( +// is_project_item, +// proto::update_followers::Variant::UpdateActiveView(update), +// cx, +// ); +// } +// } + +// fn update_followers( +// &self, +// project_only: bool, +// update: proto::update_followers::Variant, +// cx: &AppContext, +// ) -> Option<()> { +// let project_id = if project_only { +// self.project.read(cx).remote_id() +// } else { +// None +// }; +// self.app_state().workspace_store.read_with(cx, |store, cx| { +// store.update_followers(project_id, update, cx) +// }) +// } + +// pub fn leader_for_pane(&self, pane: &ViewHandle) -> Option { +// self.follower_states.get(pane).map(|state| state.leader_id) +// } + +// fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext) -> Option<()> { +// cx.notify(); + +// let call = self.active_call()?; +// let room = call.read(cx).room()?.read(cx); +// let participant = room.remote_participant_for_peer_id(leader_id)?; +// let mut items_to_activate = Vec::new(); + +// let leader_in_this_app; +// let leader_in_this_project; +// match participant.location { +// call::ParticipantLocation::SharedProject { project_id } => { +// leader_in_this_app = true; +// leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id(); +// } +// call::ParticipantLocation::UnsharedProject => { +// leader_in_this_app = true; +// leader_in_this_project = false; +// } +// call::ParticipantLocation::External => { +// leader_in_this_app = false; +// leader_in_this_project = false; +// } +// }; + +// for (pane, state) in &self.follower_states { +// if state.leader_id != leader_id { +// continue; +// } +// if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) { +// if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) { +// if leader_in_this_project || !item.is_project_item(cx) { +// items_to_activate.push((pane.clone(), item.boxed_clone())); +// } +// } else { +// log::warn!( +// "unknown view id {:?} for leader {:?}", +// active_view_id, +// leader_id +// ); +// } +// continue; +// } +// if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) { +// items_to_activate.push((pane.clone(), Box::new(shared_screen))); +// } +// } + +// for (pane, item) in items_to_activate { +// let pane_was_focused = pane.read(cx).has_focus(); +// if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) { +// pane.update(cx, |pane, cx| pane.activate_item(index, false, false, cx)); +// } else { +// pane.update(cx, |pane, cx| { +// pane.add_item(item.boxed_clone(), false, false, None, cx) +// }); +// } + +// if pane_was_focused { +// pane.update(cx, |pane, cx| pane.focus_active_item(cx)); +// } +// } + +// None +// } + +// fn shared_screen_for_peer( +// &self, +// peer_id: PeerId, +// pane: &ViewHandle, +// cx: &mut ViewContext, +// ) -> Option> { +// let call = self.active_call()?; +// let room = call.read(cx).room()?.read(cx); +// let participant = room.remote_participant_for_peer_id(peer_id)?; +// let track = participant.video_tracks.values().next()?.clone(); +// let user = participant.user.clone(); + +// for item in pane.read(cx).items_of_type::() { +// if item.read(cx).peer_id == peer_id { +// return Some(item); +// } +// } + +// Some(cx.add_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx))) +// } + +// pub fn on_window_activation_changed(&mut self, active: bool, cx: &mut ViewContext) { +// if active { +// self.update_active_view_for_followers(cx); +// cx.background() +// .spawn(persistence::DB.update_timestamp(self.database_id())) +// .detach(); +// } else { +// for pane in &self.panes { +// pane.update(cx, |pane, cx| { +// if let Some(item) = pane.active_item() { +// item.workspace_deactivated(cx); +// } +// if matches!( +// settings::get::(cx).autosave, +// AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange +// ) { +// for item in pane.items() { +// Pane::autosave_item(item.as_ref(), self.project.clone(), cx) +// .detach_and_log_err(cx); +// } +// } +// }); +// } +// } +// } + +// fn active_call(&self) -> Option<&ModelHandle> { +// self.active_call.as_ref().map(|(call, _)| call) +// } + +// fn on_active_call_event( +// &mut self, +// _: ModelHandle, +// event: &call::room::Event, +// cx: &mut ViewContext, +// ) { +// match event { +// call::room::Event::ParticipantLocationChanged { participant_id } +// | call::room::Event::RemoteVideoTracksChanged { participant_id } => { +// self.leader_updated(*participant_id, cx); +// } +// _ => {} +// } +// } + +// pub fn database_id(&self) -> WorkspaceId { +// self.database_id +// } + +// fn location(&self, cx: &AppContext) -> Option { +// let project = self.project().read(cx); + +// if project.is_local() { +// Some( +// project +// .visible_worktrees(cx) +// .map(|worktree| worktree.read(cx).abs_path()) +// .collect::>() +// .into(), +// ) +// } else { +// None +// } +// } + +// fn remove_panes(&mut self, member: Member, cx: &mut ViewContext) { +// match member { +// Member::Axis(PaneAxis { members, .. }) => { +// for child in members.iter() { +// self.remove_panes(child.clone(), cx) +// } +// } +// Member::Pane(pane) => { +// self.force_remove_pane(&pane, cx); +// } +// } +// } + +// fn force_remove_pane(&mut self, pane: &ViewHandle, cx: &mut ViewContext) { +// self.panes.retain(|p| p != pane); +// cx.focus(self.panes.last().unwrap()); +// if self.last_active_center_pane == Some(pane.downgrade()) { +// self.last_active_center_pane = None; +// } +// cx.notify(); +// } + +// fn schedule_serialize(&mut self, cx: &mut ViewContext) { +// self._schedule_serialize = Some(cx.spawn(|this, cx| async move { +// cx.background().timer(Duration::from_millis(100)).await; +// this.read_with(&cx, |this, cx| this.serialize_workspace(cx)) +// .ok(); +// })); +// } + +// fn serialize_workspace(&self, cx: &ViewContext) { +// fn serialize_pane_handle( +// pane_handle: &ViewHandle, +// cx: &AppContext, +// ) -> SerializedPane { +// let (items, active) = { +// let pane = pane_handle.read(cx); +// let active_item_id = pane.active_item().map(|item| item.id()); +// ( +// pane.items() +// .filter_map(|item_handle| { +// Some(SerializedItem { +// kind: Arc::from(item_handle.serialized_item_kind()?), +// item_id: item_handle.id(), +// active: Some(item_handle.id()) == active_item_id, +// }) +// }) +// .collect::>(), +// pane.has_focus(), +// ) +// }; + +// SerializedPane::new(items, active) +// } + +// fn build_serialized_pane_group( +// pane_group: &Member, +// cx: &AppContext, +// ) -> SerializedPaneGroup { +// match pane_group { +// Member::Axis(PaneAxis { +// axis, +// members, +// flexes, +// bounding_boxes: _, +// }) => SerializedPaneGroup::Group { +// axis: *axis, +// children: members +// .iter() +// .map(|member| build_serialized_pane_group(member, cx)) +// .collect::>(), +// flexes: Some(flexes.borrow().clone()), +// }, +// Member::Pane(pane_handle) => { +// SerializedPaneGroup::Pane(serialize_pane_handle(&pane_handle, cx)) +// } +// } +// } + +// fn build_serialized_docks(this: &Workspace, cx: &ViewContext) -> DockStructure { +// let left_dock = this.left_dock.read(cx); +// let left_visible = left_dock.is_open(); +// let left_active_panel = left_dock.visible_panel().and_then(|panel| { +// Some( +// cx.view_ui_name(panel.as_any().window(), panel.id())? +// .to_string(), +// ) +// }); +// let left_dock_zoom = left_dock +// .visible_panel() +// .map(|panel| panel.is_zoomed(cx)) +// .unwrap_or(false); + +// let right_dock = this.right_dock.read(cx); +// let right_visible = right_dock.is_open(); +// let right_active_panel = right_dock.visible_panel().and_then(|panel| { +// Some( +// cx.view_ui_name(panel.as_any().window(), panel.id())? +// .to_string(), +// ) +// }); +// let right_dock_zoom = right_dock +// .visible_panel() +// .map(|panel| panel.is_zoomed(cx)) +// .unwrap_or(false); + +// let bottom_dock = this.bottom_dock.read(cx); +// let bottom_visible = bottom_dock.is_open(); +// let bottom_active_panel = bottom_dock.visible_panel().and_then(|panel| { +// Some( +// cx.view_ui_name(panel.as_any().window(), panel.id())? +// .to_string(), +// ) +// }); +// let bottom_dock_zoom = bottom_dock +// .visible_panel() +// .map(|panel| panel.is_zoomed(cx)) +// .unwrap_or(false); + +// DockStructure { +// left: DockData { +// visible: left_visible, +// active_panel: left_active_panel, +// zoom: left_dock_zoom, +// }, +// right: DockData { +// visible: right_visible, +// active_panel: right_active_panel, +// zoom: right_dock_zoom, +// }, +// bottom: DockData { +// visible: bottom_visible, +// active_panel: bottom_active_panel, +// zoom: bottom_dock_zoom, +// }, +// } +// } + +// if let Some(location) = self.location(cx) { +// // Load bearing special case: +// // - with_local_workspace() relies on this to not have other stuff open +// // when you open your log +// if !location.paths().is_empty() { +// let center_group = build_serialized_pane_group(&self.center.root, cx); +// let docks = build_serialized_docks(self, cx); + +// let serialized_workspace = SerializedWorkspace { +// id: self.database_id, +// location, +// center_group, +// bounds: Default::default(), +// display: Default::default(), +// docks, +// }; + +// cx.background() +// .spawn(persistence::DB.save_workspace(serialized_workspace)) +// .detach(); +// } +// } +// } + +// pub(crate) fn load_workspace( +// workspace: WeakViewHandle, +// serialized_workspace: SerializedWorkspace, +// paths_to_open: Vec>, +// cx: &mut AppContext, +// ) -> Task>>>> { +// cx.spawn(|mut cx| async move { +// let (project, old_center_pane) = workspace.read_with(&cx, |workspace, _| { +// ( +// workspace.project().clone(), +// workspace.last_active_center_pane.clone(), +// ) +// })?; + +// let mut center_group = None; +// let mut center_items = None; +// // Traverse the splits tree and add to things +// if let Some((group, active_pane, items)) = serialized_workspace +// .center_group +// .deserialize(&project, serialized_workspace.id, &workspace, &mut cx) +// .await +// { +// center_items = Some(items); +// center_group = Some((group, active_pane)) +// } + +// let mut items_by_project_path = cx.read(|cx| { +// center_items +// .unwrap_or_default() +// .into_iter() +// .filter_map(|item| { +// let item = item?; +// let project_path = item.project_path(cx)?; +// Some((project_path, item)) +// }) +// .collect::>() +// }); + +// let opened_items = paths_to_open +// .into_iter() +// .map(|path_to_open| { +// path_to_open +// .and_then(|path_to_open| items_by_project_path.remove(&path_to_open)) +// }) +// .collect::>(); + +// // Remove old panes from workspace panes list +// workspace.update(&mut cx, |workspace, cx| { +// if let Some((center_group, active_pane)) = center_group { +// workspace.remove_panes(workspace.center.root.clone(), cx); + +// // Swap workspace center group +// workspace.center = PaneGroup::with_root(center_group); + +// // Change the focus to the workspace first so that we retrigger focus in on the pane. +// cx.focus_self(); + +// if let Some(active_pane) = active_pane { +// cx.focus(&active_pane); +// } else { +// cx.focus(workspace.panes.last().unwrap()); +// } +// } else { +// let old_center_handle = old_center_pane.and_then(|weak| weak.upgrade(cx)); +// if let Some(old_center_handle) = old_center_handle { +// cx.focus(&old_center_handle) +// } else { +// cx.focus_self() +// } +// } + +// let docks = serialized_workspace.docks; +// workspace.left_dock.update(cx, |dock, cx| { +// dock.set_open(docks.left.visible, cx); +// if let Some(active_panel) = docks.left.active_panel { +// if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) { +// dock.activate_panel(ix, cx); +// } +// } +// dock.active_panel() +// .map(|panel| panel.set_zoomed(docks.left.zoom, cx)); +// if docks.left.visible && docks.left.zoom { +// cx.focus_self() +// } +// }); +// // TODO: I think the bug is that setting zoom or active undoes the bottom zoom or something +// workspace.right_dock.update(cx, |dock, cx| { +// dock.set_open(docks.right.visible, cx); +// if let Some(active_panel) = docks.right.active_panel { +// if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) { +// dock.activate_panel(ix, cx); +// } +// } +// dock.active_panel() +// .map(|panel| panel.set_zoomed(docks.right.zoom, cx)); + +// if docks.right.visible && docks.right.zoom { +// cx.focus_self() +// } +// }); +// workspace.bottom_dock.update(cx, |dock, cx| { +// dock.set_open(docks.bottom.visible, cx); +// if let Some(active_panel) = docks.bottom.active_panel { +// if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) { +// dock.activate_panel(ix, cx); +// } +// } + +// dock.active_panel() +// .map(|panel| panel.set_zoomed(docks.bottom.zoom, cx)); + +// if docks.bottom.visible && docks.bottom.zoom { +// cx.focus_self() +// } +// }); + +// cx.notify(); +// })?; + +// // Serialize ourself to make sure our timestamps and any pane / item changes are replicated +// workspace.read_with(&cx, |workspace, cx| workspace.serialize_workspace(cx))?; + +// Ok(opened_items) +// }) +// } + +// #[cfg(any(test, feature = "test-support"))] +// pub fn test_new(project: ModelHandle, cx: &mut ViewContext) -> Self { +// use node_runtime::FakeNodeRuntime; + +// let client = project.read(cx).client(); +// let user_store = project.read(cx).user_store(); + +// let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx)); +// let app_state = Arc::new(AppState { +// languages: project.read(cx).languages().clone(), +// workspace_store, +// client, +// user_store, +// fs: project.read(cx).fs().clone(), +// build_window_options: |_, _, _| Default::default(), +// initialize_workspace: |_, _, _, _| Task::ready(Ok(())), +// background_actions: || &[], +// node_runtime: FakeNodeRuntime::new(), +// }); +// Self::new(0, project, app_state, cx) +// } + +// fn render_dock(&self, position: DockPosition, cx: &WindowContext) -> Option> { +// let dock = match position { +// DockPosition::Left => &self.left_dock, +// DockPosition::Right => &self.right_dock, +// DockPosition::Bottom => &self.bottom_dock, +// }; +// let active_panel = dock.read(cx).visible_panel()?; +// let element = if Some(active_panel.id()) == self.zoomed.as_ref().map(|zoomed| zoomed.id()) { +// dock.read(cx).render_placeholder(cx) +// } else { +// ChildView::new(dock, cx).into_any() +// }; + +// Some( +// element +// .constrained() +// .dynamically(move |constraint, _, cx| match position { +// DockPosition::Left | DockPosition::Right => SizeConstraint::new( +// Vector2F::new(20., constraint.min.y()), +// Vector2F::new(cx.window_size().x() * 0.8, constraint.max.y()), +// ), +// DockPosition::Bottom => SizeConstraint::new( +// Vector2F::new(constraint.min.x(), 20.), +// Vector2F::new(constraint.max.x(), cx.window_size().y() * 0.8), +// ), +// }) +// .into_any(), +// ) +// } +// } + +// fn window_bounds_env_override(cx: &AsyncAppContext) -> Option { +// ZED_WINDOW_POSITION +// .zip(*ZED_WINDOW_SIZE) +// .map(|(position, size)| { +// WindowBounds::Fixed(RectF::new( +// cx.platform().screens()[0].bounds().origin() + position, +// size, +// )) +// }) +// } + +// async fn open_items( +// serialized_workspace: Option, +// workspace: &WeakViewHandle, +// mut project_paths_to_open: Vec<(PathBuf, Option)>, +// app_state: Arc, +// mut cx: AsyncAppContext, +// ) -> Result>>>> { +// let mut opened_items = Vec::with_capacity(project_paths_to_open.len()); + +// if let Some(serialized_workspace) = serialized_workspace { +// let workspace = workspace.clone(); +// let restored_items = cx +// .update(|cx| { +// Workspace::load_workspace( +// workspace, +// serialized_workspace, +// project_paths_to_open +// .iter() +// .map(|(_, project_path)| project_path) +// .cloned() +// .collect(), +// cx, +// ) +// }) +// .await?; + +// let restored_project_paths = cx.read(|cx| { +// restored_items +// .iter() +// .filter_map(|item| item.as_ref()?.project_path(cx)) +// .collect::>() +// }); + +// for restored_item in restored_items { +// opened_items.push(restored_item.map(Ok)); +// } + +// project_paths_to_open +// .iter_mut() +// .for_each(|(_, project_path)| { +// if let Some(project_path_to_open) = project_path { +// if restored_project_paths.contains(project_path_to_open) { +// *project_path = None; +// } +// } +// }); +// } else { +// for _ in 0..project_paths_to_open.len() { +// opened_items.push(None); +// } +// } +// assert!(opened_items.len() == project_paths_to_open.len()); + +// let tasks = +// project_paths_to_open +// .into_iter() +// .enumerate() +// .map(|(i, (abs_path, project_path))| { +// let workspace = workspace.clone(); +// cx.spawn(|mut cx| { +// let fs = app_state.fs.clone(); +// async move { +// let file_project_path = project_path?; +// if fs.is_file(&abs_path).await { +// Some(( +// i, +// workspace +// .update(&mut cx, |workspace, cx| { +// workspace.open_path(file_project_path, None, true, cx) +// }) +// .log_err()? +// .await, +// )) +// } else { +// None +// } +// } +// }) +// }); + +// for maybe_opened_path in futures::future::join_all(tasks.into_iter()) +// .await +// .into_iter() +// { +// if let Some((i, path_open_result)) = maybe_opened_path { +// opened_items[i] = Some(path_open_result); +// } +// } + +// Ok(opened_items) +// } + +// fn notify_of_new_dock(workspace: &WeakViewHandle, cx: &mut AsyncAppContext) { +// const NEW_PANEL_BLOG_POST: &str = "https://zed.dev/blog/new-panel-system"; +// const NEW_DOCK_HINT_KEY: &str = "show_new_dock_key"; +// const MESSAGE_ID: usize = 2; + +// if workspace +// .read_with(cx, |workspace, cx| { +// workspace.has_shown_notification_once::(MESSAGE_ID, cx) +// }) +// .unwrap_or(false) +// { +// return; +// } + +// if db::kvp::KEY_VALUE_STORE +// .read_kvp(NEW_DOCK_HINT_KEY) +// .ok() +// .flatten() +// .is_some() +// { +// if !workspace +// .read_with(cx, |workspace, cx| { +// workspace.has_shown_notification_once::(MESSAGE_ID, cx) +// }) +// .unwrap_or(false) +// { +// cx.update(|cx| { +// cx.update_global::(|tracker, _| { +// let entry = tracker +// .entry(TypeId::of::()) +// .or_default(); +// if !entry.contains(&MESSAGE_ID) { +// entry.push(MESSAGE_ID); +// } +// }); +// }); +// } + +// return; +// } + +// cx.spawn(|_| async move { +// db::kvp::KEY_VALUE_STORE +// .write_kvp(NEW_DOCK_HINT_KEY.to_string(), "seen".to_string()) +// .await +// .ok(); +// }) +// .detach(); + +// workspace +// .update(cx, |workspace, cx| { +// workspace.show_notification_once(2, cx, |cx| { +// cx.add_view(|_| { +// MessageNotification::new_element(|text, _| { +// Text::new( +// "Looking for the dock? Try ctrl-`!\nshift-escape now zooms your pane.", +// text, +// ) +// .with_custom_runs(vec![26..32, 34..46], |_, bounds, cx| { +// let code_span_background_color = settings::get::(cx) +// .theme +// .editor +// .document_highlight_read_background; + +// cx.scene().push_quad(gpui::Quad { +// bounds, +// background: Some(code_span_background_color), +// border: Default::default(), +// corner_radii: (2.0).into(), +// }) +// }) +// .into_any() +// }) +// .with_click_message("Read more about the new panel system") +// .on_click(|cx| cx.platform().open_url(NEW_PANEL_BLOG_POST)) +// }) +// }) +// }) +// .ok(); +// } + +// fn notify_if_database_failed(workspace: &WeakViewHandle, cx: &mut AsyncAppContext) { +// const REPORT_ISSUE_URL: &str ="https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml"; + +// workspace +// .update(cx, |workspace, cx| { +// if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) { +// workspace.show_notification_once(0, cx, |cx| { +// cx.add_view(|_| { +// MessageNotification::new("Failed to load the database file.") +// .with_click_message("Click to let us know about this error") +// .on_click(|cx| cx.platform().open_url(REPORT_ISSUE_URL)) +// }) +// }); +// } +// }) +// .log_err(); +// } + +// impl Entity for Workspace { +// type Event = Event; + +// fn release(&mut self, cx: &mut AppContext) { +// self.app_state.workspace_store.update(cx, |store, _| { +// store.workspaces.remove(&self.weak_self); +// }) +// } +// } + +// impl View for Workspace { +// fn ui_name() -> &'static str { +// "Workspace" +// } + +// fn render(&mut self, cx: &mut ViewContext) -> AnyElement { +// let theme = theme::current(cx).clone(); +// Stack::new() +// .with_child( +// Flex::column() +// .with_child(self.render_titlebar(&theme, cx)) +// .with_child( +// Stack::new() +// .with_child({ +// let project = self.project.clone(); +// Flex::row() +// .with_children(self.render_dock(DockPosition::Left, cx)) +// .with_child( +// Flex::column() +// .with_child( +// FlexItem::new( +// self.center.render( +// &project, +// &theme, +// &self.follower_states, +// self.active_call(), +// self.active_pane(), +// self.zoomed +// .as_ref() +// .and_then(|zoomed| zoomed.upgrade(cx)) +// .as_ref(), +// &self.app_state, +// cx, +// ), +// ) +// .flex(1., true), +// ) +// .with_children( +// self.render_dock(DockPosition::Bottom, cx), +// ) +// .flex(1., true), +// ) +// .with_children(self.render_dock(DockPosition::Right, cx)) +// }) +// .with_child(Overlay::new( +// Stack::new() +// .with_children(self.zoomed.as_ref().and_then(|zoomed| { +// enum ZoomBackground {} +// let zoomed = zoomed.upgrade(cx)?; + +// let mut foreground_style = +// theme.workspace.zoomed_pane_foreground; +// if let Some(zoomed_dock_position) = self.zoomed_position { +// foreground_style = +// theme.workspace.zoomed_panel_foreground; +// let margin = foreground_style.margin.top; +// let border = foreground_style.border.top; + +// // Only include a margin and border on the opposite side. +// foreground_style.margin.top = 0.; +// foreground_style.margin.left = 0.; +// foreground_style.margin.bottom = 0.; +// foreground_style.margin.right = 0.; +// foreground_style.border.top = false; +// foreground_style.border.left = false; +// foreground_style.border.bottom = false; +// foreground_style.border.right = false; +// match zoomed_dock_position { +// DockPosition::Left => { +// foreground_style.margin.right = margin; +// foreground_style.border.right = border; +// } +// DockPosition::Right => { +// foreground_style.margin.left = margin; +// foreground_style.border.left = border; +// } +// DockPosition::Bottom => { +// foreground_style.margin.top = margin; +// foreground_style.border.top = border; +// } +// } +// } + +// Some( +// ChildView::new(&zoomed, cx) +// .contained() +// .with_style(foreground_style) +// .aligned() +// .contained() +// .with_style(theme.workspace.zoomed_background) +// .mouse::(0) +// .capture_all() +// .on_down( +// MouseButton::Left, +// |_, this: &mut Self, cx| { +// this.zoom_out(cx); +// }, +// ), +// ) +// })) +// .with_children(self.modal.as_ref().map(|modal| { +// // Prevent clicks within the modal from falling +// // through to the rest of the workspace. +// enum ModalBackground {} +// MouseEventHandler::new::( +// 0, +// cx, +// |_, cx| ChildView::new(modal.view.as_any(), cx), +// ) +// .on_click(MouseButton::Left, |_, _, _| {}) +// .contained() +// .with_style(theme.workspace.modal) +// .aligned() +// .top() +// })) +// .with_children(self.render_notifications(&theme.workspace, cx)), +// )) +// .provide_resize_bounds::() +// .flex(1.0, true), +// ) +// .with_child(ChildView::new(&self.status_bar, cx)) +// .contained() +// .with_background_color(theme.workspace.background), +// ) +// .with_children(DragAndDrop::render(cx)) +// .with_children(self.render_disconnected_overlay(cx)) +// .into_any_named("workspace") +// } + +// fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { +// if cx.is_self_focused() { +// cx.focus(&self.active_pane); +// } +// } + +// fn modifiers_changed(&mut self, e: &ModifiersChangedEvent, cx: &mut ViewContext) -> bool { +// DragAndDrop::::update_modifiers(e.modifiers, cx) +// } +// } + +// impl WorkspaceStore { +// pub fn new(client: Arc, cx: &mut ModelContext) -> Self { +// Self { +// workspaces: Default::default(), +// followers: Default::default(), +// _subscriptions: vec![ +// client.add_request_handler(cx.handle(), Self::handle_follow), +// client.add_message_handler(cx.handle(), Self::handle_unfollow), +// client.add_message_handler(cx.handle(), Self::handle_update_followers), +// ], +// client, +// } +// } + +// pub fn update_followers( +// &self, +// project_id: Option, +// update: proto::update_followers::Variant, +// cx: &AppContext, +// ) -> Option<()> { +// if !cx.has_global::>() { +// return None; +// } + +// let room_id = ActiveCall::global(cx).read(cx).room()?.read(cx).id(); +// let follower_ids: Vec<_> = self +// .followers +// .iter() +// .filter_map(|follower| { +// if follower.project_id == project_id || project_id.is_none() { +// Some(follower.peer_id.into()) +// } else { +// None +// } +// }) +// .collect(); +// if follower_ids.is_empty() { +// return None; +// } +// self.client +// .send(proto::UpdateFollowers { +// room_id, +// project_id, +// follower_ids, +// variant: Some(update), +// }) +// .log_err() +// } + +// async fn handle_follow( +// this: ModelHandle, +// envelope: TypedEnvelope, +// _: Arc, +// mut cx: AsyncAppContext, +// ) -> Result { +// this.update(&mut cx, |this, cx| { +// let follower = Follower { +// project_id: envelope.payload.project_id, +// peer_id: envelope.original_sender_id()?, +// }; +// let active_project = ActiveCall::global(cx) +// .read(cx) +// .location() +// .map(|project| project.id()); + +// let mut response = proto::FollowResponse::default(); +// for workspace in &this.workspaces { +// let Some(workspace) = workspace.upgrade(cx) else { +// continue; +// }; + +// workspace.update(cx.as_mut(), |workspace, cx| { +// let handler_response = workspace.handle_follow(follower.project_id, cx); +// if response.views.is_empty() { +// response.views = handler_response.views; +// } else { +// response.views.extend_from_slice(&handler_response.views); +// } + +// if let Some(active_view_id) = handler_response.active_view_id.clone() { +// if response.active_view_id.is_none() +// || Some(workspace.project.id()) == active_project +// { +// response.active_view_id = Some(active_view_id); +// } +// } +// }); +// } + +// if let Err(ix) = this.followers.binary_search(&follower) { +// this.followers.insert(ix, follower); +// } + +// Ok(response) +// }) +// } + +// async fn handle_unfollow( +// this: ModelHandle, +// envelope: TypedEnvelope, +// _: Arc, +// mut cx: AsyncAppContext, +// ) -> Result<()> { +// this.update(&mut cx, |this, _| { +// let follower = Follower { +// project_id: envelope.payload.project_id, +// peer_id: envelope.original_sender_id()?, +// }; +// if let Ok(ix) = this.followers.binary_search(&follower) { +// this.followers.remove(ix); +// } +// Ok(()) +// }) +// } + +// async fn handle_update_followers( +// this: ModelHandle, +// envelope: TypedEnvelope, +// _: Arc, +// mut cx: AsyncAppContext, +// ) -> Result<()> { +// let leader_id = envelope.original_sender_id()?; +// let update = envelope.payload; +// this.update(&mut cx, |this, cx| { +// for workspace in &this.workspaces { +// let Some(workspace) = workspace.upgrade(cx) else { +// continue; +// }; +// workspace.update(cx.as_mut(), |workspace, cx| { +// let project_id = workspace.project.read(cx).remote_id(); +// if update.project_id != project_id && update.project_id.is_some() { +// return; +// } +// workspace.handle_update_followers(leader_id, update.clone(), cx); +// }); +// } +// Ok(()) +// }) +// } +// } + +// impl Entity for WorkspaceStore { +// type Event = (); +// } + +// impl ViewId { +// pub(crate) fn from_proto(message: proto::ViewId) -> Result { +// Ok(Self { +// creator: message +// .creator +// .ok_or_else(|| anyhow!("creator is missing"))?, +// id: message.id, +// }) +// } + +// pub(crate) fn to_proto(&self) -> proto::ViewId { +// proto::ViewId { +// creator: Some(self.creator), +// id: self.id, +// } +// } +// } + +// pub trait WorkspaceHandle { +// fn file_project_paths(&self, cx: &AppContext) -> Vec; +// } + +// impl WorkspaceHandle for ViewHandle { +// fn file_project_paths(&self, cx: &AppContext) -> Vec { +// self.read(cx) +// .worktrees(cx) +// .flat_map(|worktree| { +// let worktree_id = worktree.read(cx).id(); +// worktree.read(cx).files(true, 0).map(move |f| ProjectPath { +// worktree_id, +// path: f.path.clone(), +// }) +// }) +// .collect::>() +// } +// } + +// impl std::fmt::Debug for OpenPaths { +// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +// f.debug_struct("OpenPaths") +// .field("paths", &self.paths) +// .finish() +// } +// } + +// pub struct WorkspaceCreated(pub WeakViewHandle); + +// pub fn activate_workspace_for_project( +// cx: &mut AsyncAppContext, +// predicate: impl Fn(&mut Project, &mut ModelContext) -> bool, +// ) -> Option> { +// for window in cx.windows() { +// let handle = window +// .update(cx, |cx| { +// if let Some(workspace_handle) = cx.root_view().clone().downcast::() { +// let project = workspace_handle.read(cx).project.clone(); +// if project.update(cx, &predicate) { +// cx.activate_window(); +// return Some(workspace_handle.clone()); +// } +// } +// None +// }) +// .flatten(); + +// if let Some(handle) = handle { +// return Some(handle.downgrade()); +// } +// } +// None +// } + +// pub async fn last_opened_workspace_paths() -> Option { +// DB.last_workspace().await.log_err().flatten() +// } + +// async fn join_channel_internal( +// channel_id: u64, +// app_state: &Arc, +// requesting_window: Option>, +// active_call: &ModelHandle, +// cx: &mut AsyncAppContext, +// ) -> Result { +// let (should_prompt, open_room) = active_call.read_with(cx, |active_call, cx| { +// let Some(room) = active_call.room().map(|room| room.read(cx)) else { +// return (false, None); +// }; + +// let already_in_channel = room.channel_id() == Some(channel_id); +// let should_prompt = room.is_sharing_project() +// && room.remote_participants().len() > 0 +// && !already_in_channel; +// let open_room = if already_in_channel { +// active_call.room().cloned() +// } else { +// None +// }; +// (should_prompt, open_room) +// }); + +// if let Some(room) = open_room { +// let task = room.update(cx, |room, cx| { +// if let Some((project, host)) = room.most_active_project(cx) { +// return Some(join_remote_project(project, host, app_state.clone(), cx)); +// } + +// None +// }); +// if let Some(task) = task { +// task.await?; +// } +// return anyhow::Ok(true); +// } + +// if should_prompt { +// if let Some(workspace) = requesting_window { +// if let Some(window) = workspace.update(cx, |cx| cx.window()) { +// let answer = window.prompt( +// PromptLevel::Warning, +// "Leaving this call will unshare your current project.\nDo you want to switch channels?", +// &["Yes, Join Channel", "Cancel"], +// cx, +// ); + +// if let Some(mut answer) = answer { +// if answer.next().await == Some(1) { +// return Ok(false); +// } +// } +// } else { +// return Ok(false); // unreachable!() hopefully +// } +// } else { +// return Ok(false); // unreachable!() hopefully +// } +// } + +// let client = cx.read(|cx| active_call.read(cx).client()); + +// let mut client_status = client.status(); + +// // this loop will terminate within client::CONNECTION_TIMEOUT seconds. +// 'outer: loop { +// let Some(status) = client_status.recv().await else { +// return Err(anyhow!("error connecting")); +// }; + +// match status { +// Status::Connecting +// | Status::Authenticating +// | Status::Reconnecting +// | Status::Reauthenticating => continue, +// Status::Connected { .. } => break 'outer, +// Status::SignedOut => return Err(anyhow!("not signed in")), +// Status::UpgradeRequired => return Err(anyhow!("zed is out of date")), +// Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => { +// return Err(anyhow!("zed is offline")) +// } +// } +// } + +// let room = active_call +// .update(cx, |active_call, cx| { +// active_call.join_channel(channel_id, cx) +// }) +// .await?; + +// room.update(cx, |room, _| room.room_update_completed()) +// .await; + +// let task = room.update(cx, |room, cx| { +// if let Some((project, host)) = room.most_active_project(cx) { +// return Some(join_remote_project(project, host, app_state.clone(), cx)); +// } + +// None +// }); +// if let Some(task) = task { +// task.await?; +// return anyhow::Ok(true); +// } +// anyhow::Ok(false) +// } + +// pub fn join_channel( +// channel_id: u64, +// app_state: Arc, +// requesting_window: Option>, +// cx: &mut AppContext, +// ) -> Task> { +// let active_call = ActiveCall::global(cx); +// cx.spawn(|mut cx| async move { +// let result = join_channel_internal( +// channel_id, +// &app_state, +// requesting_window, +// &active_call, +// &mut cx, +// ) +// .await; + +// // join channel succeeded, and opened a window +// if matches!(result, Ok(true)) { +// return anyhow::Ok(()); +// } + +// if requesting_window.is_some() { +// return anyhow::Ok(()); +// } + +// // find an existing workspace to focus and show call controls +// let mut active_window = activate_any_workspace_window(&mut cx); +// if active_window.is_none() { +// // no open workspaces, make one to show the error in (blergh) +// cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), requesting_window, cx)) +// .await; +// } + +// active_window = activate_any_workspace_window(&mut cx); +// if active_window.is_none() { +// return result.map(|_| ()); // unreachable!() assuming new_local always opens a window +// } + +// if let Err(err) = result { +// let prompt = active_window.unwrap().prompt( +// PromptLevel::Critical, +// &format!("Failed to join channel: {}", err), +// &["Ok"], +// &mut cx, +// ); +// if let Some(mut prompt) = prompt { +// prompt.next().await; +// } else { +// return Err(err); +// } +// } + +// // return ok, we showed the error to the user. +// return anyhow::Ok(()); +// }) +// } + +// pub fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option { +// for window in cx.windows() { +// let found = window.update(cx, |cx| { +// let is_workspace = cx.root_view().clone().downcast::().is_some(); +// if is_workspace { +// cx.activate_window(); +// } +// is_workspace +// }); +// if found == Some(true) { +// return Some(window); +// } +// } +// None +// } + +// #[allow(clippy::type_complexity)] +// pub fn open_paths( +// abs_paths: &[PathBuf], +// app_state: &Arc, +// requesting_window: Option>, +// cx: &mut AppContext, +// ) -> Task< +// Result<( +// WeakViewHandle, +// Vec, anyhow::Error>>>, +// )>, +// > { +// let app_state = app_state.clone(); +// let abs_paths = abs_paths.to_vec(); +// cx.spawn(|mut cx| async move { +// // Open paths in existing workspace if possible +// let existing = activate_workspace_for_project(&mut cx, |project, cx| { +// project.contains_paths(&abs_paths, cx) +// }); + +// if let Some(existing) = existing { +// Ok(( +// existing.clone(), +// existing +// .update(&mut cx, |workspace, cx| { +// workspace.open_paths(abs_paths, true, cx) +// })? +// .await, +// )) +// } else { +// Ok(cx +// .update(|cx| { +// Workspace::new_local(abs_paths, app_state.clone(), requesting_window, cx) +// }) +// .await) +// } +// }) +// } + +// pub fn open_new( +// app_state: &Arc, +// cx: &mut AppContext, +// init: impl FnOnce(&mut Workspace, &mut ViewContext) + 'static, +// ) -> Task<()> { +// let task = Workspace::new_local(Vec::new(), app_state.clone(), None, cx); +// cx.spawn(|mut cx| async move { +// let (workspace, opened_paths) = task.await; + +// workspace +// .update(&mut cx, |workspace, cx| { +// if opened_paths.is_empty() { +// init(workspace, cx) +// } +// }) +// .log_err(); +// }) +// } + +// pub fn create_and_open_local_file( +// path: &'static Path, +// cx: &mut ViewContext, +// default_content: impl 'static + Send + FnOnce() -> Rope, +// ) -> Task>> { +// cx.spawn(|workspace, mut cx| async move { +// let fs = workspace.read_with(&cx, |workspace, _| workspace.app_state().fs.clone())?; +// if !fs.is_file(path).await { +// fs.create_file(path, Default::default()).await?; +// fs.save(path, &default_content(), Default::default()) +// .await?; +// } + +// let mut items = workspace +// .update(&mut cx, |workspace, cx| { +// workspace.with_local_workspace(cx, |workspace, cx| { +// workspace.open_paths(vec![path.to_path_buf()], false, cx) +// }) +// })? +// .await? +// .await; + +// let item = items.pop().flatten(); +// item.ok_or_else(|| anyhow!("path {path:?} is not a file"))? +// }) +// } + +// pub fn join_remote_project( +// project_id: u64, +// follow_user_id: u64, +// app_state: Arc, +// cx: &mut AppContext, +// ) -> Task> { +// cx.spawn(|mut cx| async move { +// let windows = cx.windows(); +// let existing_workspace = windows.into_iter().find_map(|window| { +// window.downcast::().and_then(|window| { +// window +// .read_root_with(&cx, |workspace, cx| { +// if workspace.project().read(cx).remote_id() == Some(project_id) { +// Some(cx.handle().downgrade()) +// } else { +// None +// } +// }) +// .unwrap_or(None) +// }) +// }); + +// let workspace = if let Some(existing_workspace) = existing_workspace { +// existing_workspace +// } else { +// let active_call = cx.read(ActiveCall::global); +// let room = active_call +// .read_with(&cx, |call, _| call.room().cloned()) +// .ok_or_else(|| anyhow!("not in a call"))?; +// let project = room +// .update(&mut cx, |room, cx| { +// room.join_project( +// project_id, +// app_state.languages.clone(), +// app_state.fs.clone(), +// cx, +// ) +// }) +// .await?; + +// let window_bounds_override = window_bounds_env_override(&cx); +// let window = cx.add_window( +// (app_state.build_window_options)( +// window_bounds_override, +// None, +// cx.platform().as_ref(), +// ), +// |cx| Workspace::new(0, project, app_state.clone(), cx), +// ); +// let workspace = window.root(&cx).unwrap(); +// (app_state.initialize_workspace)( +// workspace.downgrade(), +// false, +// app_state.clone(), +// cx.clone(), +// ) +// .await +// .log_err(); + +// workspace.downgrade() +// }; + +// workspace.window().activate(&mut cx); +// cx.platform().activate(true); + +// workspace.update(&mut cx, |workspace, cx| { +// if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() { +// let follow_peer_id = room +// .read(cx) +// .remote_participants() +// .iter() +// .find(|(_, participant)| participant.user.id == follow_user_id) +// .map(|(_, p)| p.peer_id) +// .or_else(|| { +// // If we couldn't follow the given user, follow the host instead. +// let collaborator = workspace +// .project() +// .read(cx) +// .collaborators() +// .values() +// .find(|collaborator| collaborator.replica_id == 0)?; +// Some(collaborator.peer_id) +// }); + +// if let Some(follow_peer_id) = follow_peer_id { +// workspace +// .follow(follow_peer_id, cx) +// .map(|follow| follow.detach_and_log_err(cx)); +// } +// } +// })?; + +// anyhow::Ok(()) +// }) +// } + +// pub fn restart(_: &Restart, cx: &mut AppContext) { +// let should_confirm = settings::get::(cx).confirm_quit; +// cx.spawn(|mut cx| async move { +// let mut workspace_windows = cx +// .windows() +// .into_iter() +// .filter_map(|window| window.downcast::()) +// .collect::>(); + +// // If multiple windows have unsaved changes, and need a save prompt, +// // prompt in the active window before switching to a different window. +// workspace_windows.sort_by_key(|window| window.is_active(&cx) == Some(false)); + +// if let (true, Some(window)) = (should_confirm, workspace_windows.first()) { +// let answer = window.prompt( +// PromptLevel::Info, +// "Are you sure you want to restart?", +// &["Restart", "Cancel"], +// &mut cx, +// ); + +// if let Some(mut answer) = answer { +// let answer = answer.next().await; +// if answer != Some(0) { +// return Ok(()); +// } +// } +// } + +// // If the user cancels any save prompt, then keep the app open. +// for window in workspace_windows { +// if let Some(should_close) = window.update_root(&mut cx, |workspace, cx| { +// workspace.prepare_to_close(true, cx) +// }) { +// if !should_close.await? { +// return Ok(()); +// } +// } +// } +// cx.platform().restart(); +// anyhow::Ok(()) +// }) +// .detach_and_log_err(cx); +// } + +// fn parse_pixel_position_env_var(value: &str) -> Option { +// let mut parts = value.split(','); +// let width: usize = parts.next()?.parse().ok()?; +// let height: usize = parts.next()?.parse().ok()?; +// Some(vec2f(width as f32, height as f32)) +// } + +// #[cfg(test)] +// mod tests { +// use super::*; +// use crate::{ +// dock::test::{TestPanel, TestPanelEvent}, +// item::test::{TestItem, TestItemEvent, TestProjectItem}, +// }; +// use fs::FakeFs; +// use gpui::{executor::Deterministic, test::EmptyView, TestAppContext}; +// use project::{Project, ProjectEntryId}; +// use serde_json::json; +// use settings::SettingsStore; +// use std::{cell::RefCell, rc::Rc}; + +// #[gpui::test] +// async fn test_tab_disambiguation(cx: &mut TestAppContext) { +// init_test(cx); + +// let fs = FakeFs::new(cx.background()); +// let project = Project::test(fs, [], cx).await; +// let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); +// let workspace = window.root(cx); + +// // Adding an item with no ambiguity renders the tab without detail. +// let item1 = window.add_view(cx, |_| { +// let mut item = TestItem::new(); +// item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]); +// item +// }); +// workspace.update(cx, |workspace, cx| { +// workspace.add_item(Box::new(item1.clone()), cx); +// }); +// item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), None)); + +// // Adding an item that creates ambiguity increases the level of detail on +// // both tabs. +// let item2 = window.add_view(cx, |_| { +// let mut item = TestItem::new(); +// item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]); +// item +// }); +// workspace.update(cx, |workspace, cx| { +// workspace.add_item(Box::new(item2.clone()), cx); +// }); +// item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1))); +// item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1))); + +// // Adding an item that creates ambiguity increases the level of detail only +// // on the ambiguous tabs. In this case, the ambiguity can't be resolved so +// // we stop at the highest detail available. +// let item3 = window.add_view(cx, |_| { +// let mut item = TestItem::new(); +// item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]); +// item +// }); +// workspace.update(cx, |workspace, cx| { +// workspace.add_item(Box::new(item3.clone()), cx); +// }); +// item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1))); +// item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3))); +// item3.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3))); +// } + +// #[gpui::test] +// async fn test_tracking_active_path(cx: &mut TestAppContext) { +// init_test(cx); + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/root1", +// json!({ +// "one.txt": "", +// "two.txt": "", +// }), +// ) +// .await; +// fs.insert_tree( +// "/root2", +// json!({ +// "three.txt": "", +// }), +// ) +// .await; + +// let project = Project::test(fs, ["root1".as_ref()], cx).await; +// let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); +// let workspace = window.root(cx); +// let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); +// let worktree_id = project.read_with(cx, |project, cx| { +// project.worktrees(cx).next().unwrap().read(cx).id() +// }); + +// let item1 = window.add_view(cx, |cx| { +// TestItem::new().with_project_items(&[TestProjectItem::new(1, "one.txt", cx)]) +// }); +// let item2 = window.add_view(cx, |cx| { +// TestItem::new().with_project_items(&[TestProjectItem::new(2, "two.txt", cx)]) +// }); + +// // Add an item to an empty pane +// workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx)); +// project.read_with(cx, |project, cx| { +// assert_eq!( +// project.active_entry(), +// project +// .entry_for_path(&(worktree_id, "one.txt").into(), cx) +// .map(|e| e.id) +// ); +// }); +// assert_eq!(window.current_title(cx).as_deref(), Some("one.txt — root1")); + +// // Add a second item to a non-empty pane +// workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx)); +// assert_eq!(window.current_title(cx).as_deref(), Some("two.txt — root1")); +// project.read_with(cx, |project, cx| { +// assert_eq!( +// project.active_entry(), +// project +// .entry_for_path(&(worktree_id, "two.txt").into(), cx) +// .map(|e| e.id) +// ); +// }); + +// // Close the active item +// pane.update(cx, |pane, cx| { +// pane.close_active_item(&Default::default(), cx).unwrap() +// }) +// .await +// .unwrap(); +// assert_eq!(window.current_title(cx).as_deref(), Some("one.txt — root1")); +// project.read_with(cx, |project, cx| { +// assert_eq!( +// project.active_entry(), +// project +// .entry_for_path(&(worktree_id, "one.txt").into(), cx) +// .map(|e| e.id) +// ); +// }); + +// // Add a project folder +// project +// .update(cx, |project, cx| { +// project.find_or_create_local_worktree("/root2", true, cx) +// }) +// .await +// .unwrap(); +// assert_eq!( +// window.current_title(cx).as_deref(), +// Some("one.txt — root1, root2") +// ); + +// // Remove a project folder +// project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx)); +// assert_eq!(window.current_title(cx).as_deref(), Some("one.txt — root2")); +// } + +// #[gpui::test] +// async fn test_close_window(cx: &mut TestAppContext) { +// init_test(cx); + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree("/root", json!({ "one": "" })).await; + +// let project = Project::test(fs, ["root".as_ref()], cx).await; +// let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); +// let workspace = window.root(cx); + +// // When there are no dirty items, there's nothing to do. +// let item1 = window.add_view(cx, |_| TestItem::new()); +// workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx)); +// let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx)); +// assert!(task.await.unwrap()); + +// // When there are dirty untitled items, prompt to save each one. If the user +// // cancels any prompt, then abort. +// let item2 = window.add_view(cx, |_| TestItem::new().with_dirty(true)); +// let item3 = window.add_view(cx, |cx| { +// TestItem::new() +// .with_dirty(true) +// .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]) +// }); +// workspace.update(cx, |w, cx| { +// w.add_item(Box::new(item2.clone()), cx); +// w.add_item(Box::new(item3.clone()), cx); +// }); +// let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx)); +// cx.foreground().run_until_parked(); +// window.simulate_prompt_answer(2, cx); // cancel save all +// cx.foreground().run_until_parked(); +// window.simulate_prompt_answer(2, cx); // cancel save all +// cx.foreground().run_until_parked(); +// assert!(!window.has_pending_prompt(cx)); +// assert!(!task.await.unwrap()); +// } + +// #[gpui::test] +// async fn test_close_pane_items(cx: &mut TestAppContext) { +// init_test(cx); + +// let fs = FakeFs::new(cx.background()); + +// let project = Project::test(fs, None, cx).await; +// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); +// let workspace = window.root(cx); + +// let item1 = window.add_view(cx, |cx| { +// TestItem::new() +// .with_dirty(true) +// .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]) +// }); +// let item2 = window.add_view(cx, |cx| { +// TestItem::new() +// .with_dirty(true) +// .with_conflict(true) +// .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)]) +// }); +// let item3 = window.add_view(cx, |cx| { +// TestItem::new() +// .with_dirty(true) +// .with_conflict(true) +// .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)]) +// }); +// let item4 = window.add_view(cx, |cx| { +// TestItem::new() +// .with_dirty(true) +// .with_project_items(&[TestProjectItem::new_untitled(cx)]) +// }); +// let pane = workspace.update(cx, |workspace, cx| { +// workspace.add_item(Box::new(item1.clone()), cx); +// workspace.add_item(Box::new(item2.clone()), cx); +// workspace.add_item(Box::new(item3.clone()), cx); +// workspace.add_item(Box::new(item4.clone()), cx); +// workspace.active_pane().clone() +// }); + +// let close_items = pane.update(cx, |pane, cx| { +// pane.activate_item(1, true, true, cx); +// assert_eq!(pane.active_item().unwrap().id(), item2.id()); +// let item1_id = item1.id(); +// let item3_id = item3.id(); +// let item4_id = item4.id(); +// pane.close_items(cx, SaveIntent::Close, move |id| { +// [item1_id, item3_id, item4_id].contains(&id) +// }) +// }); +// cx.foreground().run_until_parked(); + +// assert!(window.has_pending_prompt(cx)); +// // Ignore "Save all" prompt +// window.simulate_prompt_answer(2, cx); +// cx.foreground().run_until_parked(); +// // There's a prompt to save item 1. +// pane.read_with(cx, |pane, _| { +// assert_eq!(pane.items_len(), 4); +// assert_eq!(pane.active_item().unwrap().id(), item1.id()); +// }); +// // Confirm saving item 1. +// window.simulate_prompt_answer(0, cx); +// cx.foreground().run_until_parked(); + +// // Item 1 is saved. There's a prompt to save item 3. +// pane.read_with(cx, |pane, cx| { +// assert_eq!(item1.read(cx).save_count, 1); +// assert_eq!(item1.read(cx).save_as_count, 0); +// assert_eq!(item1.read(cx).reload_count, 0); +// assert_eq!(pane.items_len(), 3); +// assert_eq!(pane.active_item().unwrap().id(), item3.id()); +// }); +// assert!(window.has_pending_prompt(cx)); + +// // Cancel saving item 3. +// window.simulate_prompt_answer(1, cx); +// cx.foreground().run_until_parked(); + +// // Item 3 is reloaded. There's a prompt to save item 4. +// pane.read_with(cx, |pane, cx| { +// assert_eq!(item3.read(cx).save_count, 0); +// assert_eq!(item3.read(cx).save_as_count, 0); +// assert_eq!(item3.read(cx).reload_count, 1); +// assert_eq!(pane.items_len(), 2); +// assert_eq!(pane.active_item().unwrap().id(), item4.id()); +// }); +// assert!(window.has_pending_prompt(cx)); + +// // Confirm saving item 4. +// window.simulate_prompt_answer(0, cx); +// cx.foreground().run_until_parked(); + +// // There's a prompt for a path for item 4. +// cx.simulate_new_path_selection(|_| Some(Default::default())); +// close_items.await.unwrap(); + +// // The requested items are closed. +// pane.read_with(cx, |pane, cx| { +// assert_eq!(item4.read(cx).save_count, 0); +// assert_eq!(item4.read(cx).save_as_count, 1); +// assert_eq!(item4.read(cx).reload_count, 0); +// assert_eq!(pane.items_len(), 1); +// assert_eq!(pane.active_item().unwrap().id(), item2.id()); +// }); +// } + +// #[gpui::test] +// async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) { +// init_test(cx); + +// let fs = FakeFs::new(cx.background()); + +// let project = Project::test(fs, [], cx).await; +// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); +// let workspace = window.root(cx); + +// // Create several workspace items with single project entries, and two +// // workspace items with multiple project entries. +// let single_entry_items = (0..=4) +// .map(|project_entry_id| { +// window.add_view(cx, |cx| { +// TestItem::new() +// .with_dirty(true) +// .with_project_items(&[TestProjectItem::new( +// project_entry_id, +// &format!("{project_entry_id}.txt"), +// cx, +// )]) +// }) +// }) +// .collect::>(); +// let item_2_3 = window.add_view(cx, |cx| { +// TestItem::new() +// .with_dirty(true) +// .with_singleton(false) +// .with_project_items(&[ +// single_entry_items[2].read(cx).project_items[0].clone(), +// single_entry_items[3].read(cx).project_items[0].clone(), +// ]) +// }); +// let item_3_4 = window.add_view(cx, |cx| { +// TestItem::new() +// .with_dirty(true) +// .with_singleton(false) +// .with_project_items(&[ +// single_entry_items[3].read(cx).project_items[0].clone(), +// single_entry_items[4].read(cx).project_items[0].clone(), +// ]) +// }); + +// // Create two panes that contain the following project entries: +// // left pane: +// // multi-entry items: (2, 3) +// // single-entry items: 0, 1, 2, 3, 4 +// // right pane: +// // single-entry items: 1 +// // multi-entry items: (3, 4) +// let left_pane = workspace.update(cx, |workspace, cx| { +// let left_pane = workspace.active_pane().clone(); +// workspace.add_item(Box::new(item_2_3.clone()), cx); +// for item in single_entry_items { +// workspace.add_item(Box::new(item), cx); +// } +// left_pane.update(cx, |pane, cx| { +// pane.activate_item(2, true, true, cx); +// }); + +// workspace +// .split_and_clone(left_pane.clone(), SplitDirection::Right, cx) +// .unwrap(); + +// left_pane +// }); + +// //Need to cause an effect flush in order to respect new focus +// workspace.update(cx, |workspace, cx| { +// workspace.add_item(Box::new(item_3_4.clone()), cx); +// cx.focus(&left_pane); +// }); + +// // When closing all of the items in the left pane, we should be prompted twice: +// // once for project entry 0, and once for project entry 2. After those two +// // prompts, the task should complete. + +// let close = left_pane.update(cx, |pane, cx| { +// pane.close_items(cx, SaveIntent::Close, move |_| true) +// }); +// cx.foreground().run_until_parked(); +// // Discard "Save all" prompt +// window.simulate_prompt_answer(2, cx); + +// cx.foreground().run_until_parked(); +// left_pane.read_with(cx, |pane, cx| { +// assert_eq!( +// pane.active_item().unwrap().project_entry_ids(cx).as_slice(), +// &[ProjectEntryId::from_proto(0)] +// ); +// }); +// window.simulate_prompt_answer(0, cx); + +// cx.foreground().run_until_parked(); +// left_pane.read_with(cx, |pane, cx| { +// assert_eq!( +// pane.active_item().unwrap().project_entry_ids(cx).as_slice(), +// &[ProjectEntryId::from_proto(2)] +// ); +// }); +// window.simulate_prompt_answer(0, cx); + +// cx.foreground().run_until_parked(); +// close.await.unwrap(); +// left_pane.read_with(cx, |pane, _| { +// assert_eq!(pane.items_len(), 0); +// }); +// } + +// #[gpui::test] +// async fn test_autosave(deterministic: Arc, cx: &mut gpui::TestAppContext) { +// init_test(cx); + +// let fs = FakeFs::new(cx.background()); + +// let project = Project::test(fs, [], cx).await; +// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); +// let workspace = window.root(cx); +// let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + +// let item = window.add_view(cx, |cx| { +// TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]) +// }); +// let item_id = item.id(); +// workspace.update(cx, |workspace, cx| { +// workspace.add_item(Box::new(item.clone()), cx); +// }); + +// // Autosave on window change. +// item.update(cx, |item, cx| { +// cx.update_global(|settings: &mut SettingsStore, cx| { +// settings.update_user_settings::(cx, |settings| { +// settings.autosave = Some(AutosaveSetting::OnWindowChange); +// }) +// }); +// item.is_dirty = true; +// }); + +// // Deactivating the window saves the file. +// window.simulate_deactivation(cx); +// deterministic.run_until_parked(); +// item.read_with(cx, |item, _| assert_eq!(item.save_count, 1)); + +// // Autosave on focus change. +// item.update(cx, |item, cx| { +// cx.focus_self(); +// cx.update_global(|settings: &mut SettingsStore, cx| { +// settings.update_user_settings::(cx, |settings| { +// settings.autosave = Some(AutosaveSetting::OnFocusChange); +// }) +// }); +// item.is_dirty = true; +// }); + +// // Blurring the item saves the file. +// item.update(cx, |_, cx| cx.blur()); +// deterministic.run_until_parked(); +// item.read_with(cx, |item, _| assert_eq!(item.save_count, 2)); + +// // Deactivating the window still saves the file. +// window.simulate_activation(cx); +// item.update(cx, |item, cx| { +// cx.focus_self(); +// item.is_dirty = true; +// }); +// window.simulate_deactivation(cx); + +// 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 SettingsStore, cx| { +// settings.update_user_settings::(cx, |settings| { +// settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 }); +// }) +// }); +// item.is_dirty = true; +// cx.emit(TestItemEvent::Edit); +// }); + +// // 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, 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, 4)); + +// // Autosave on focus change, ensuring closing the tab counts as such. +// item.update(cx, |item, cx| { +// cx.update_global(|settings: &mut SettingsStore, cx| { +// settings.update_user_settings::(cx, |settings| { +// settings.autosave = Some(AutosaveSetting::OnFocusChange); +// }) +// }); +// item.is_dirty = true; +// }); + +// pane.update(cx, |pane, cx| { +// pane.close_items(cx, SaveIntent::Close, move |id| id == item_id) +// }) +// .await +// .unwrap(); +// assert!(!window.has_pending_prompt(cx)); +// 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| { +// workspace.add_item(Box::new(item.clone()), cx); +// }); +// item.update(cx, |item, cx| { +// item.project_items[0].update(cx, |item, _| { +// item.entry_id = None; +// }); +// item.is_dirty = true; +// cx.blur(); +// }); +// deterministic.run_until_parked(); +// 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 = pane.update(cx, |pane, cx| { +// pane.close_items(cx, SaveIntent::Close, move |id| id == item_id) +// }); +// deterministic.run_until_parked(); +// assert!(window.has_pending_prompt(cx)); +// item.read_with(cx, |item, _| assert_eq!(item.save_count, 5)); +// } + +// #[gpui::test] +// async fn test_pane_navigation(cx: &mut gpui::TestAppContext) { +// init_test(cx); + +// let fs = FakeFs::new(cx.background()); + +// let project = Project::test(fs, [], cx).await; +// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); +// let workspace = window.root(cx); + +// let item = window.add_view(cx, |cx| { +// TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]) +// }); +// 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| workspace.go_back(pane.downgrade(), cx)) +// .await +// .unwrap(); + +// assert_eq!(*toolbar_notify_count.borrow(), 3); +// pane.read_with(cx, |pane, _| { +// assert!(!pane.can_navigate_backward()); +// assert!(pane.can_navigate_forward()); +// }); +// } + +// #[gpui::test] +// async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) { +// init_test(cx); +// let fs = FakeFs::new(cx.background()); + +// let project = Project::test(fs, [], cx).await; +// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); +// let workspace = window.root(cx); + +// let panel = workspace.update(cx, |workspace, cx| { +// let panel = cx.add_view(|_| TestPanel::new(DockPosition::Right)); +// workspace.add_panel(panel.clone(), cx); + +// workspace +// .right_dock() +// .update(cx, |right_dock, cx| right_dock.set_open(true, cx)); + +// panel +// }); + +// let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); +// pane.update(cx, |pane, cx| { +// let item = cx.add_view(|_| TestItem::new()); +// pane.add_item(Box::new(item), true, true, None, cx); +// }); + +// // Transfer focus from center to panel +// workspace.update(cx, |workspace, cx| { +// workspace.toggle_panel_focus::(cx); +// }); + +// workspace.read_with(cx, |workspace, cx| { +// assert!(workspace.right_dock().read(cx).is_open()); +// assert!(!panel.is_zoomed(cx)); +// assert!(panel.has_focus(cx)); +// }); + +// // Transfer focus from panel to center +// workspace.update(cx, |workspace, cx| { +// workspace.toggle_panel_focus::(cx); +// }); + +// workspace.read_with(cx, |workspace, cx| { +// assert!(workspace.right_dock().read(cx).is_open()); +// assert!(!panel.is_zoomed(cx)); +// assert!(!panel.has_focus(cx)); +// }); + +// // Close the dock +// workspace.update(cx, |workspace, cx| { +// workspace.toggle_dock(DockPosition::Right, cx); +// }); + +// workspace.read_with(cx, |workspace, cx| { +// assert!(!workspace.right_dock().read(cx).is_open()); +// assert!(!panel.is_zoomed(cx)); +// assert!(!panel.has_focus(cx)); +// }); + +// // Open the dock +// workspace.update(cx, |workspace, cx| { +// workspace.toggle_dock(DockPosition::Right, cx); +// }); + +// workspace.read_with(cx, |workspace, cx| { +// assert!(workspace.right_dock().read(cx).is_open()); +// assert!(!panel.is_zoomed(cx)); +// assert!(panel.has_focus(cx)); +// }); + +// // Focus and zoom panel +// panel.update(cx, |panel, cx| { +// cx.focus_self(); +// panel.set_zoomed(true, cx) +// }); + +// workspace.read_with(cx, |workspace, cx| { +// assert!(workspace.right_dock().read(cx).is_open()); +// assert!(panel.is_zoomed(cx)); +// assert!(panel.has_focus(cx)); +// }); + +// // Transfer focus to the center closes the dock +// workspace.update(cx, |workspace, cx| { +// workspace.toggle_panel_focus::(cx); +// }); + +// workspace.read_with(cx, |workspace, cx| { +// assert!(!workspace.right_dock().read(cx).is_open()); +// assert!(panel.is_zoomed(cx)); +// assert!(!panel.has_focus(cx)); +// }); + +// // Transferring focus back to the panel keeps it zoomed +// workspace.update(cx, |workspace, cx| { +// workspace.toggle_panel_focus::(cx); +// }); + +// workspace.read_with(cx, |workspace, cx| { +// assert!(workspace.right_dock().read(cx).is_open()); +// assert!(panel.is_zoomed(cx)); +// assert!(panel.has_focus(cx)); +// }); + +// // Close the dock while it is zoomed +// workspace.update(cx, |workspace, cx| { +// workspace.toggle_dock(DockPosition::Right, cx) +// }); + +// workspace.read_with(cx, |workspace, cx| { +// assert!(!workspace.right_dock().read(cx).is_open()); +// assert!(panel.is_zoomed(cx)); +// assert!(workspace.zoomed.is_none()); +// assert!(!panel.has_focus(cx)); +// }); + +// // Opening the dock, when it's zoomed, retains focus +// workspace.update(cx, |workspace, cx| { +// workspace.toggle_dock(DockPosition::Right, cx) +// }); + +// workspace.read_with(cx, |workspace, cx| { +// assert!(workspace.right_dock().read(cx).is_open()); +// assert!(panel.is_zoomed(cx)); +// assert!(workspace.zoomed.is_some()); +// assert!(panel.has_focus(cx)); +// }); + +// // Unzoom and close the panel, zoom the active pane. +// panel.update(cx, |panel, cx| panel.set_zoomed(false, cx)); +// workspace.update(cx, |workspace, cx| { +// workspace.toggle_dock(DockPosition::Right, cx) +// }); +// pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx)); + +// // Opening a dock unzooms the pane. +// workspace.update(cx, |workspace, cx| { +// workspace.toggle_dock(DockPosition::Right, cx) +// }); +// workspace.read_with(cx, |workspace, cx| { +// let pane = pane.read(cx); +// assert!(!pane.is_zoomed()); +// assert!(!pane.has_focus()); +// assert!(workspace.right_dock().read(cx).is_open()); +// assert!(workspace.zoomed.is_none()); +// }); +// } + +// #[gpui::test] +// async fn test_panels(cx: &mut gpui::TestAppContext) { +// init_test(cx); +// let fs = FakeFs::new(cx.background()); + +// let project = Project::test(fs, [], cx).await; +// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); +// let workspace = window.root(cx); + +// let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| { +// // Add panel_1 on the left, panel_2 on the right. +// let panel_1 = cx.add_view(|_| TestPanel::new(DockPosition::Left)); +// workspace.add_panel(panel_1.clone(), cx); +// workspace +// .left_dock() +// .update(cx, |left_dock, cx| left_dock.set_open(true, cx)); +// let panel_2 = cx.add_view(|_| TestPanel::new(DockPosition::Right)); +// workspace.add_panel(panel_2.clone(), cx); +// workspace +// .right_dock() +// .update(cx, |right_dock, cx| right_dock.set_open(true, cx)); + +// let left_dock = workspace.left_dock(); +// assert_eq!( +// left_dock.read(cx).visible_panel().unwrap().id(), +// panel_1.id() +// ); +// assert_eq!( +// left_dock.read(cx).active_panel_size(cx).unwrap(), +// panel_1.size(cx) +// ); + +// left_dock.update(cx, |left_dock, cx| { +// left_dock.resize_active_panel(Some(1337.), cx) +// }); +// assert_eq!( +// workspace +// .right_dock() +// .read(cx) +// .visible_panel() +// .unwrap() +// .id(), +// panel_2.id() +// ); + +// (panel_1, panel_2) +// }); + +// // Move panel_1 to the right +// panel_1.update(cx, |panel_1, cx| { +// panel_1.set_position(DockPosition::Right, cx) +// }); + +// workspace.update(cx, |workspace, cx| { +// // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right. +// // Since it was the only panel on the left, the left dock should now be closed. +// assert!(!workspace.left_dock().read(cx).is_open()); +// assert!(workspace.left_dock().read(cx).visible_panel().is_none()); +// let right_dock = workspace.right_dock(); +// assert_eq!( +// right_dock.read(cx).visible_panel().unwrap().id(), +// panel_1.id() +// ); +// assert_eq!(right_dock.read(cx).active_panel_size(cx).unwrap(), 1337.); + +// // Now we move panel_2 to the left +// panel_2.set_position(DockPosition::Left, cx); +// }); + +// workspace.update(cx, |workspace, cx| { +// // Since panel_2 was not visible on the right, we don't open the left dock. +// assert!(!workspace.left_dock().read(cx).is_open()); +// // And the right dock is unaffected in it's displaying of panel_1 +// assert!(workspace.right_dock().read(cx).is_open()); +// assert_eq!( +// workspace +// .right_dock() +// .read(cx) +// .visible_panel() +// .unwrap() +// .id(), +// panel_1.id() +// ); +// }); + +// // Move panel_1 back to the left +// panel_1.update(cx, |panel_1, cx| { +// panel_1.set_position(DockPosition::Left, cx) +// }); + +// workspace.update(cx, |workspace, cx| { +// // Since panel_1 was visible on the right, we open the left dock and make panel_1 active. +// let left_dock = workspace.left_dock(); +// assert!(left_dock.read(cx).is_open()); +// assert_eq!( +// left_dock.read(cx).visible_panel().unwrap().id(), +// panel_1.id() +// ); +// assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), 1337.); +// // And right the dock should be closed as it no longer has any panels. +// assert!(!workspace.right_dock().read(cx).is_open()); + +// // Now we move panel_1 to the bottom +// panel_1.set_position(DockPosition::Bottom, cx); +// }); + +// workspace.update(cx, |workspace, cx| { +// // Since panel_1 was visible on the left, we close the left dock. +// assert!(!workspace.left_dock().read(cx).is_open()); +// // The bottom dock is sized based on the panel's default size, +// // since the panel orientation changed from vertical to horizontal. +// let bottom_dock = workspace.bottom_dock(); +// assert_eq!( +// bottom_dock.read(cx).active_panel_size(cx).unwrap(), +// panel_1.size(cx), +// ); +// // Close bottom dock and move panel_1 back to the left. +// bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx)); +// panel_1.set_position(DockPosition::Left, cx); +// }); + +// // Emit activated event on panel 1 +// panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Activated)); + +// // Now the left dock is open and panel_1 is active and focused. +// workspace.read_with(cx, |workspace, cx| { +// let left_dock = workspace.left_dock(); +// assert!(left_dock.read(cx).is_open()); +// assert_eq!( +// left_dock.read(cx).visible_panel().unwrap().id(), +// panel_1.id() +// ); +// assert!(panel_1.is_focused(cx)); +// }); + +// // Emit closed event on panel 2, which is not active +// panel_2.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed)); + +// // Wo don't close the left dock, because panel_2 wasn't the active panel +// workspace.read_with(cx, |workspace, cx| { +// let left_dock = workspace.left_dock(); +// assert!(left_dock.read(cx).is_open()); +// assert_eq!( +// left_dock.read(cx).visible_panel().unwrap().id(), +// panel_1.id() +// ); +// }); + +// // Emitting a ZoomIn event shows the panel as zoomed. +// panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomIn)); +// workspace.read_with(cx, |workspace, _| { +// assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any())); +// assert_eq!(workspace.zoomed_position, Some(DockPosition::Left)); +// }); + +// // Move panel to another dock while it is zoomed +// panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx)); +// workspace.read_with(cx, |workspace, _| { +// assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any())); +// assert_eq!(workspace.zoomed_position, Some(DockPosition::Right)); +// }); + +// // If focus is transferred to another view that's not a panel or another pane, we still show +// // the panel as zoomed. +// let focus_receiver = window.add_view(cx, |_| EmptyView); +// focus_receiver.update(cx, |_, cx| cx.focus_self()); +// workspace.read_with(cx, |workspace, _| { +// assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any())); +// assert_eq!(workspace.zoomed_position, Some(DockPosition::Right)); +// }); + +// // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed. +// workspace.update(cx, |_, cx| cx.focus_self()); +// workspace.read_with(cx, |workspace, _| { +// assert_eq!(workspace.zoomed, None); +// assert_eq!(workspace.zoomed_position, None); +// }); + +// // If focus is transferred again to another view that's not a panel or a pane, we won't +// // show the panel as zoomed because it wasn't zoomed before. +// focus_receiver.update(cx, |_, cx| cx.focus_self()); +// workspace.read_with(cx, |workspace, _| { +// assert_eq!(workspace.zoomed, None); +// assert_eq!(workspace.zoomed_position, None); +// }); + +// // When focus is transferred back to the panel, it is zoomed again. +// panel_1.update(cx, |_, cx| cx.focus_self()); +// workspace.read_with(cx, |workspace, _| { +// assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any())); +// assert_eq!(workspace.zoomed_position, Some(DockPosition::Right)); +// }); + +// // Emitting a ZoomOut event unzooms the panel. +// panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomOut)); +// workspace.read_with(cx, |workspace, _| { +// assert_eq!(workspace.zoomed, None); +// assert_eq!(workspace.zoomed_position, None); +// }); + +// // Emit closed event on panel 1, which is active +// panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed)); + +// // Now the left dock is closed, because panel_1 was the active panel +// workspace.read_with(cx, |workspace, cx| { +// let right_dock = workspace.right_dock(); +// assert!(!right_dock.read(cx).is_open()); +// }); +// } + +// pub fn init_test(cx: &mut TestAppContext) { +// cx.foreground().forbid_parking(); +// cx.update(|cx| { +// cx.set_global(SettingsStore::test(cx)); +// theme::init((), cx); +// language::init(cx); +// crate::init_settings(cx); +// Project::init_settings(cx); +// }); +// } +// } diff --git a/crates/workspace2/src/workspace_settings.rs b/crates/workspace2/src/workspace_settings.rs new file mode 100644 index 0000000000..6483167018 --- /dev/null +++ b/crates/workspace2/src/workspace_settings.rs @@ -0,0 +1,56 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use settings::Setting; + +#[derive(Deserialize)] +pub struct WorkspaceSettings { + pub active_pane_magnification: f32, + pub confirm_quit: bool, + pub show_call_status_icon: bool, + pub autosave: AutosaveSetting, +} + +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] +pub struct WorkspaceSettingsContent { + pub active_pane_magnification: Option, + pub confirm_quit: Option, + pub show_call_status_icon: Option, + pub autosave: Option, +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum AutosaveSetting { + Off, + AfterDelay { milliseconds: u64 }, + OnFocusChange, + OnWindowChange, +} + +#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] +pub struct GitSettings { + pub git_gutter: Option, + pub gutter_debounce: Option, +} + +#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum GitGutterSetting { + #[default] + TrackedFiles, + Hide, +} + +impl Setting for WorkspaceSettings { + const KEY: Option<&'static str> = None; + + type FileContent = WorkspaceSettingsContent; + + fn load( + default_value: &Self::FileContent, + user_values: &[&Self::FileContent], + _: &gpui::AppContext, + ) -> anyhow::Result { + Self::load_via_json_merge(default_value, user_values) + } +} diff --git a/crates/zed2/Cargo.toml b/crates/zed2/Cargo.toml index bc56e31457..08aedce714 100644 --- a/crates/zed2/Cargo.toml +++ b/crates/zed2/Cargo.toml @@ -69,7 +69,7 @@ theme2 = { path = "../theme2" } util = { path = "../util" } # semantic_index = { path = "../semantic_index" } # vim = { path = "../vim" } -# workspace = { path = "../workspace" } +workspace2 = { path = "../workspace2" } # welcome = { path = "../welcome" } # zed-actions = {path = "../zed-actions"} anyhow.workspace = true diff --git a/crates/zed2/src/zed2.rs b/crates/zed2/src/zed2.rs index d78908dfb5..9782c6dd05 100644 --- a/crates/zed2/src/zed2.rs +++ b/crates/zed2/src/zed2.rs @@ -4,7 +4,8 @@ mod open_listener; pub use assets::*; use client2::{Client, UserStore}; -use gpui2::{AsyncAppContext, Handle}; +use collections::HashMap; +use gpui2::{AsyncAppContext, Handle, Point}; pub use only_instance::*; pub use open_listener::*; @@ -13,8 +14,12 @@ use cli::{ ipc::{self, IpcSender}, CliRequest, CliResponse, IpcHandshake, }; -use futures::{channel::mpsc, SinkExt, StreamExt}; -use std::{sync::Arc, thread}; +use futures::{ + channel::{mpsc, oneshot}, + FutureExt, SinkExt, StreamExt, +}; +use std::{path::Path, sync::Arc, thread, time::Duration}; +use util::{paths::PathLikeWithPosition, ResultExt}; pub fn connect_to_cli( server_name: &str, @@ -51,156 +56,157 @@ pub struct AppState { } pub async fn handle_cli_connection( - (mut requests, _responses): (mpsc::Receiver, IpcSender), - _app_state: Arc, - mut _cx: AsyncAppContext, + (mut requests, responses): (mpsc::Receiver, IpcSender), + app_state: Arc, + mut cx: AsyncAppContext, ) { if let Some(request) = requests.next().await { match request { - CliRequest::Open { paths: _, wait: _ } => { - // let mut caret_positions = HashMap::new(); + CliRequest::Open { paths, wait } => { + let mut caret_positions = HashMap::default(); - // let paths = if paths.is_empty() { - // todo!() - // workspace::last_opened_workspace_paths() - // .await - // .map(|location| location.paths().to_vec()) - // .unwrap_or_default() - // } else { - // paths - // .into_iter() - // .filter_map(|path_with_position_string| { - // let path_with_position = PathLikeWithPosition::parse_str( - // &path_with_position_string, - // |path_str| { - // Ok::<_, std::convert::Infallible>( - // Path::new(path_str).to_path_buf(), - // ) - // }, - // ) - // .expect("Infallible"); - // let path = path_with_position.path_like; - // if let Some(row) = path_with_position.row { - // if path.is_file() { - // let row = row.saturating_sub(1); - // let col = - // path_with_position.column.unwrap_or(0).saturating_sub(1); - // caret_positions.insert(path.clone(), Point::new(row, col)); - // } - // } - // Some(path) - // }) - // .collect() - // }; + let paths = if paths.is_empty() { + todo!() + // workspace::last_opened_workspace_paths() + // .await + // .map(|location| location.paths().to_vec()) + // .unwrap_or_default() + } else { + paths + .into_iter() + .filter_map(|path_with_position_string| { + let path_with_position = PathLikeWithPosition::parse_str( + &path_with_position_string, + |path_str| { + Ok::<_, std::convert::Infallible>( + Path::new(path_str).to_path_buf(), + ) + }, + ) + .expect("Infallible"); + let path = path_with_position.path_like; + if let Some(row) = path_with_position.row { + if path.is_file() { + let row = row.saturating_sub(1); + let col = + path_with_position.column.unwrap_or(0).saturating_sub(1); + caret_positions.insert(path.clone(), Point::new(row, col)); + } + } + Some(path) + }) + .collect() + }; - // let mut errored = false; - // todo!("workspace") - // match cx - // .update(|cx| workspace::open_paths(&paths, &app_state, None, cx)) - // .await - // { - // Ok((workspace, items)) => { - // let mut item_release_futures = Vec::new(); + let mut errored = false; - // for (item, path) in items.into_iter().zip(&paths) { - // match item { - // Some(Ok(item)) => { - // if let Some(point) = caret_positions.remove(path) { - // if let Some(active_editor) = item.downcast::() { - // active_editor - // .downgrade() - // .update(&mut cx, |editor, cx| { - // let snapshot = - // editor.snapshot(cx).display_snapshot; - // let point = snapshot - // .buffer_snapshot - // .clip_point(point, Bias::Left); - // editor.change_selections( - // Some(Autoscroll::center()), - // cx, - // |s| s.select_ranges([point..point]), - // ); - // }) - // .log_err(); - // } - // } + match cx + .update(|cx| workspace2::open_paths(&paths, &app_state, None, cx)) + .await + { + Ok((workspace, items)) => { + let mut item_release_futures = Vec::new(); - // let released = oneshot::channel(); - // cx.update(|cx| { - // item.on_release( - // cx, - // Box::new(move |_| { - // let _ = released.0.send(()); - // }), - // ) - // .detach(); - // }); - // item_release_futures.push(released.1); - // } - // Some(Err(err)) => { - // responses - // .send(CliResponse::Stderr { - // message: format!("error opening {:?}: {}", path, err), - // }) - // .log_err(); - // errored = true; - // } - // None => {} - // } - // } + for (item, path) in items.into_iter().zip(&paths) { + match item { + Some(Ok(item)) => { + if let Some(point) = caret_positions.remove(path) { + todo!() + // if let Some(active_editor) = item.downcast::() { + // active_editor + // .downgrade() + // .update(&mut cx, |editor, cx| { + // let snapshot = + // editor.snapshot(cx).display_snapshot; + // let point = snapshot + // .buffer_snapshot + // .clip_point(point, Bias::Left); + // editor.change_selections( + // Some(Autoscroll::center()), + // cx, + // |s| s.select_ranges([point..point]), + // ); + // }) + // .log_err(); + // } + } - // if wait { - // let background = cx.background(); - // let wait = async move { - // if paths.is_empty() { - // let (done_tx, done_rx) = oneshot::channel(); - // if let Some(workspace) = workspace.upgrade(&cx) { - // let _subscription = cx.update(|cx| { - // cx.observe_release(&workspace, move |_, _| { - // let _ = done_tx.send(()); - // }) - // }); - // drop(workspace); - // let _ = done_rx.await; - // } - // } else { - // let _ = - // futures::future::try_join_all(item_release_futures).await; - // }; - // } - // .fuse(); - // futures::pin_mut!(wait); + let released = oneshot::channel(); + cx.update(|cx| { + item.on_release( + cx, + Box::new(move |_| { + let _ = released.0.send(()); + }), + ) + .detach(); + }); + item_release_futures.push(released.1); + } + Some(Err(err)) => { + responses + .send(CliResponse::Stderr { + message: format!("error opening {:?}: {}", path, err), + }) + .log_err(); + errored = true; + } + None => {} + } + } - // loop { - // // Repeatedly check if CLI is still open to avoid wasting resources - // // waiting for files or workspaces to close. - // let mut timer = background.timer(Duration::from_secs(1)).fuse(); - // futures::select_biased! { - // _ = wait => break, - // _ = timer => { - // if responses.send(CliResponse::Ping).is_err() { - // break; - // } - // } - // } - // } - // } - // } - // Err(error) => { - // errored = true; - // responses - // .send(CliResponse::Stderr { - // message: format!("error opening {:?}: {}", paths, error), - // }) - // .log_err(); - // } - // } + if wait { + let executor = cx.executor(); + let wait = async move { + if paths.is_empty() { + let (done_tx, done_rx) = oneshot::channel(); + if let Some(workspace) = workspace.upgrade(&cx) { + let _subscription = cx.update(|cx| { + cx.observe_release(&workspace, move |_, _| { + let _ = done_tx.send(()); + }) + }); + drop(workspace); + let _ = done_rx.await; + } + } else { + let _ = + futures::future::try_join_all(item_release_futures).await; + }; + } + .fuse(); + futures::pin_mut!(wait); - // responses - // .send(CliResponse::Exit { - // status: i32::from(errored), - // }) - // .log_err(); + loop { + // Repeatedly check if CLI is still open to avoid wasting resources + // waiting for files or workspaces to close. + let mut timer = executor.timer(Duration::from_secs(1)).fuse(); + futures::select_biased! { + _ = wait => break, + _ = timer => { + if responses.send(CliResponse::Ping).is_err() { + break; + } + } + } + } + } + } + Err(error) => { + errored = true; + responses + .send(CliResponse::Stderr { + message: format!("error opening {:?}: {}", paths, error), + }) + .log_err(); + } + } + + responses + .send(CliResponse::Exit { + status: i32::from(errored), + }) + .log_err(); } } }