Detailed changes
@@ -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]]
@@ -94,7 +94,7 @@ members = [
"crates/semantic_index",
"crates/vim",
"crates/vcs_menu",
- "crates/workspace",
+ "crates/workspace2",
"crates/welcome",
"crates/xtask",
"crates/zed",
@@ -607,6 +607,20 @@ impl AppContext {
self.globals_by_type.insert(global_type, lease.global);
}
+ pub fn observe_release<E: 'static>(
+ &mut self,
+ handle: &Handle<E>,
+ 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);
}
@@ -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<G: 'static>(
@@ -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<T>,
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) {
@@ -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
@@ -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<Self>);
+ fn size(&self, cx: &WindowContext) -> f32;
+ fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>);
+ fn icon_path(&self, cx: &WindowContext) -> Option<&'static str>;
+ fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>);
+ fn icon_label(&self, _: &WindowContext) -> Option<String> {
+ 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<Self>) {}
+ fn set_active(&mut self, _active: bool, _cx: &mut ViewContext<Self>) {}
+ 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<f32>, cx: &mut WindowContext);
+ fn icon_path(&self, cx: &WindowContext) -> Option<&'static str>;
+ fn icon_tooltip(&self, cx: &WindowContext) -> (String, Option<Box<dyn Action>>);
+ fn icon_label(&self, cx: &WindowContext) -> Option<String>;
+ fn has_focus(&self, cx: &WindowContext) -> bool;
+ fn as_any(&self) -> &AnyViewHandle;
+}
+
+impl<T> PanelHandle for ViewHandle<T>
+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<f32>, 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<Box<dyn Action>>) {
+ self.read(cx).icon_tooltip()
+ }
+
+ fn icon_label(&self, cx: &WindowContext) -> Option<String> {
+ 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<PanelEntry>,
+ 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<dyn PanelHandle>,
+ context_menu: ViewHandle<ContextMenu>,
+ _subscriptions: [Subscription; 2],
+}
+
+pub struct PanelButtons {
+ dock: ViewHandle<Dock>,
+ workspace: WeakViewHandle<Workspace>,
+}
+
+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<T: Panel>(&self) -> Option<ViewHandle<T>> {
+ self.panel_entries
+ .iter()
+ .find_map(|entry| entry.panel.as_any().clone().downcast())
+ }
+
+ pub fn panel_index_for_type<T: Panel>(&self) -> Option<usize> {
+ self.panel_entries
+ .iter()
+ .position(|entry| entry.panel.as_any().is::<T>())
+ }
+
+ pub fn panel_index_for_ui_name(&self, ui_name: &str, cx: &AppContext) -> Option<usize> {
+ 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<Self>) {
+ 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<Self>,
+ ) {
+ 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<Self>) {
+ for entry in &mut self.panel_entries {
+ if entry.panel.is_zoomed(cx) {
+ entry.panel.set_zoomed(false, cx);
+ }
+ }
+ }
+
+ pub(crate) fn add_panel<T: Panel>(&mut self, panel: ViewHandle<T>, cx: &mut ViewContext<Self>) {
+ 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<T: Panel>(&mut self, panel: &ViewHandle<T>, cx: &mut ViewContext<Self>) {
+ 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<Self>) {
+ 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<dyn PanelHandle>> {
+ let entry = self.visible_entry()?;
+ Some(&entry.panel)
+ }
+
+ pub fn active_panel(&self) -> Option<&Rc<dyn PanelHandle>> {
+ 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<Rc<dyn PanelHandle>> {
+ 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<f32> {
+ 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<f32> {
+ 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<f32>, cx: &mut ViewContext<Self>) {
+ 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<Workspace> {
+ if let Some(active_entry) = self.visible_entry() {
+ Empty::new()
+ .into_any()
+ .contained()
+ .with_style(self.style(cx))
+ .resizable::<WorkspaceBounds>(
+ 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::<ThemeSettings>(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<Self>) -> AnyElement<Self> {
+ 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::<WorkspaceBounds>(
+ 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<Self>) {
+ 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<Dock>,
+ workspace: WeakViewHandle<Workspace>,
+ cx: &mut ViewContext<Self>,
+ ) -> 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<Self>) -> AnyElement<Self> {
+ let theme = &settings::get::<ThemeSettings>(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::<Vec<_>>();
+ 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::<Self, _>(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::<Self>(
+ 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<Self>,
+ ) {
+ }
+}
+
+#[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<Self> {
+ Empty::new().into_any()
+ }
+
+ fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
+ self.has_focus = true;
+ cx.emit(TestPanelEvent::Focus);
+ }
+
+ fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
+ 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>) {
+ 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>) {
+ self.zoomed = zoomed;
+ }
+
+ fn set_active(&mut self, active: bool, _cx: &mut ViewContext<Self>) {
+ self.active = active;
+ }
+
+ fn size(&self, _: &WindowContext) -> f32 {
+ self.size
+ }
+
+ fn set_size(&mut self, size: Option<f32>, _: &mut ViewContext<Self>) {
+ 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<Box<dyn Action>>) {
+ ("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)
+ }
+ }
+}
@@ -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<bool>,
+ close_position: Option<ClosePosition>,
+}
+
+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> {
+ 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<Vec<(Range<usize>, HighlightStyle)>>,
+}
+
+pub trait Item: View {
+ fn deactivated(&mut self, _: &mut ViewContext<Self>) {}
+ fn workspace_deactivated(&mut self, _: &mut ViewContext<Self>) {}
+ fn navigate(&mut self, _: Box<dyn Any>, _: &mut ViewContext<Self>) -> bool {
+ false
+ }
+ fn tab_tooltip_text(&self, _: &AppContext) -> Option<Cow<str>> {
+ None
+ }
+ fn tab_description<'a>(&'a self, _: usize, _: &'a AppContext) -> Option<Cow<str>> {
+ None
+ }
+ fn tab_content<V: 'static>(
+ &self,
+ detail: Option<usize>,
+ style: &theme2::Tab,
+ cx: &AppContext,
+ ) -> AnyElement<V>;
+ 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<Self>) {}
+ fn clone_on_split(&self, _workspace_id: WorkspaceId, _: &mut ViewContext<Self>) -> Option<Self>
+ 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<Project>,
+ _cx: &mut ViewContext<Self>,
+ ) -> Task<Result<()>> {
+ unimplemented!("save() must be implemented if can_save() returns true")
+ }
+ fn save_as(
+ &mut self,
+ _project: ModelHandle<Project>,
+ _abs_path: PathBuf,
+ _cx: &mut ViewContext<Self>,
+ ) -> Task<Result<()>> {
+ unimplemented!("save_as() must be implemented if can_save() returns true")
+ }
+ fn reload(
+ &mut self,
+ _project: ModelHandle<Project>,
+ _cx: &mut ViewContext<Self>,
+ ) -> Task<Result<()>> {
+ 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<Self>,
+ _: &'a AppContext,
+ ) -> Option<&AnyViewHandle> {
+ if TypeId::of::<Self>() == type_id {
+ Some(self_handle)
+ } else {
+ None
+ }
+ }
+
+ fn as_searchable(&self, _: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
+ None
+ }
+
+ fn breadcrumb_location(&self) -> ToolbarItemLocation {
+ ToolbarItemLocation::Hidden
+ }
+
+ fn breadcrumbs(&self, _theme: &Theme, _cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
+ None
+ }
+
+ fn added_to_workspace(&mut self, _workspace: &mut Workspace, _cx: &mut ViewContext<Self>) {}
+
+ fn serialized_item_kind() -> Option<&'static str> {
+ None
+ }
+
+ fn deserialize(
+ _project: ModelHandle<Project>,
+ _workspace: WeakViewHandle<Workspace>,
+ _workspace_id: WorkspaceId,
+ _item_id: ItemId,
+ _cx: &mut ViewContext<Pane>,
+ ) -> Task<Result<ViewHandle<Self>>> {
+ 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<Vector2F> {
+ None
+ }
+}
+
+pub trait ItemHandle: 'static + fmt::Debug {
+ fn subscribe_to_item_events(
+ &self,
+ cx: &mut WindowContext,
+ handler: Box<dyn Fn(ItemEvent, &mut WindowContext)>,
+ ) -> gpui2::Subscription;
+ fn tab_tooltip_text<'a>(&self, cx: &'a AppContext) -> Option<Cow<'a, str>>;
+ fn tab_description<'a>(&'a self, detail: usize, cx: &'a AppContext) -> Option<Cow<'a, str>>;
+ fn tab_content(
+ &self,
+ detail: Option<usize>,
+ style: &theme2::Tab,
+ cx: &AppContext,
+ ) -> AnyElement<Pane>;
+ fn dragged_tab_content(
+ &self,
+ detail: Option<usize>,
+ style: &theme2::Tab,
+ cx: &AppContext,
+ ) -> AnyElement<Workspace>;
+ fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
+ 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<dyn ItemHandle>;
+ fn clone_on_split(
+ &self,
+ workspace_id: WorkspaceId,
+ cx: &mut WindowContext,
+ ) -> Option<Box<dyn ItemHandle>>;
+ fn added_to_pane(
+ &self,
+ workspace: &mut Workspace,
+ pane: ViewHandle<Pane>,
+ cx: &mut ViewContext<Workspace>,
+ );
+ fn deactivated(&self, cx: &mut WindowContext);
+ fn workspace_deactivated(&self, cx: &mut WindowContext);
+ fn navigate(&self, data: Box<dyn Any>, 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<Project>, cx: &mut WindowContext) -> Task<Result<()>>;
+ fn save_as(
+ &self,
+ project: ModelHandle<Project>,
+ abs_path: PathBuf,
+ cx: &mut WindowContext,
+ ) -> Task<Result<()>>;
+ fn reload(&self, project: ModelHandle<Project>, cx: &mut WindowContext) -> Task<Result<()>>;
+ 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<Box<dyn FollowableItemHandle>>;
+ fn on_release(
+ &self,
+ cx: &mut AppContext,
+ callback: Box<dyn FnOnce(&mut AppContext)>,
+ ) -> gpui2::Subscription;
+ fn to_searchable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn SearchableItemHandle>>;
+ fn breadcrumb_location(&self, cx: &AppContext) -> ToolbarItemLocation;
+ fn breadcrumbs(&self, theme: &Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>>;
+ fn serialized_item_kind(&self) -> Option<&'static str>;
+ fn show_toolbar(&self, cx: &AppContext) -> bool;
+ fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Vector2F>;
+}
+
+pub trait WeakItemHandle {
+ fn id(&self) -> usize;
+ fn window(&self) -> AnyWindowHandle;
+ fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>>;
+}
+
+impl dyn ItemHandle {
+ pub fn downcast<T: View>(&self) -> Option<ViewHandle<T>> {
+ self.as_any().clone().downcast()
+ }
+
+ pub fn act_as<T: View>(&self, cx: &AppContext) -> Option<ViewHandle<T>> {
+ self.act_as_type(TypeId::of::<T>(), cx)
+ .and_then(|t| t.clone().downcast())
+ }
+}
+
+impl<T: Item> ItemHandle for ViewHandle<T> {
+ fn subscribe_to_item_events(
+ &self,
+ cx: &mut WindowContext,
+ handler: Box<dyn Fn(ItemEvent, &mut WindowContext)>,
+ ) -> 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<Cow<'a, str>> {
+ self.read(cx).tab_tooltip_text(cx)
+ }
+
+ fn tab_description<'a>(&'a self, detail: usize, cx: &'a AppContext) -> Option<Cow<'a, str>> {
+ self.read(cx).tab_description(detail, cx)
+ }
+
+ fn tab_content(
+ &self,
+ detail: Option<usize>,
+ style: &theme2::Tab,
+ cx: &AppContext,
+ ) -> AnyElement<Pane> {
+ self.read(cx).tab_content(detail, style, cx)
+ }
+
+ fn dragged_tab_content(
+ &self,
+ detail: Option<usize>,
+ style: &theme2::Tab,
+ cx: &AppContext,
+ ) -> AnyElement<Workspace> {
+ self.read(cx).tab_content(detail, style, cx)
+ }
+
+ fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
+ 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<dyn ItemHandle> {
+ Box::new(self.clone())
+ }
+
+ fn clone_on_split(
+ &self,
+ workspace_id: WorkspaceId,
+ cx: &mut WindowContext,
+ ) -> Option<Box<dyn ItemHandle>> {
+ self.update(cx, |item, cx| {
+ cx.add_option_view(|cx| item.clone_on_split(workspace_id, cx))
+ })
+ .map(|handle| Box::new(handle) as Box<dyn ItemHandle>)
+ }
+
+ fn added_to_pane(
+ &self,
+ workspace: &mut Workspace,
+ pane: ViewHandle<Pane>,
+ cx: &mut ViewContext<Workspace>,
+ ) {
+ let history = pane.read(cx).nav_history_for_item(self);
+ self.update(cx, |this, cx| {
+ this.set_nav_history(history, cx);
+ 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::<WorkspaceSettings>(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::<WorkspaceSettings>(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<dyn Any>, 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<Project>, cx: &mut WindowContext) -> Task<Result<()>> {
+ self.update(cx, |item, cx| item.save(project, cx))
+ }
+
+ fn save_as(
+ &self,
+ project: ModelHandle<Project>,
+ abs_path: PathBuf,
+ cx: &mut WindowContext,
+ ) -> Task<anyhow::Result<()>> {
+ self.update(cx, |item, cx| item.save_as(project, abs_path, cx))
+ }
+
+ fn reload(&self, project: ModelHandle<Project>, cx: &mut WindowContext) -> Task<Result<()>> {
+ 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<Box<dyn FollowableItemHandle>> {
+ if cx.has_global::<FollowableItemBuilders>() {
+ let builders = cx.global::<FollowableItemBuilders>();
+ let item = self.as_any();
+ Some(builders.get(&item.view_type())?.1(item))
+ } else {
+ None
+ }
+ }
+
+ fn on_release(
+ &self,
+ cx: &mut AppContext,
+ callback: Box<dyn FnOnce(&mut AppContext)>,
+ ) -> gpui2::Subscription {
+ cx.observe_release(self, move |_, cx| callback(cx))
+ }
+
+ fn to_searchable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn SearchableItemHandle>> {
+ 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<Vec<BreadcrumbText>> {
+ 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<Vector2F> {
+ self.read(cx).pixel_position_of_cursor(cx)
+ }
+}
+
+impl From<Box<dyn ItemHandle>> for AnyViewHandle {
+ fn from(val: Box<dyn ItemHandle>) -> Self {
+ val.as_any().clone()
+ }
+}
+
+impl From<&Box<dyn ItemHandle>> for AnyViewHandle {
+ fn from(val: &Box<dyn ItemHandle>) -> Self {
+ val.as_any().clone()
+ }
+}
+
+impl Clone for Box<dyn ItemHandle> {
+ fn clone(&self) -> Box<dyn ItemHandle> {
+ self.boxed_clone()
+ }
+}
+
+impl<T: Item> WeakItemHandle for WeakViewHandle<T> {
+ fn id(&self) -> usize {
+ self.id()
+ }
+
+ fn window(&self) -> AnyWindowHandle {
+ self.window()
+ }
+
+ fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
+ self.upgrade(cx).map(|v| Box::new(v) as Box<dyn ItemHandle>)
+ }
+}
+
+pub trait ProjectItem: Item {
+ type Item: project2::Item + gpui2::Entity;
+
+ fn for_project_item(
+ project: ModelHandle<Project>,
+ item: ModelHandle<Self::Item>,
+ cx: &mut ViewContext<Self>,
+ ) -> Self;
+}
+
+pub trait FollowableItem: Item {
+ fn remote_id(&self) -> Option<ViewId>;
+ fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant>;
+ fn from_state_proto(
+ pane: ViewHandle<Pane>,
+ project: ViewHandle<Workspace>,
+ id: ViewId,
+ state: &mut Option<proto::view::Variant>,
+ cx: &mut AppContext,
+ ) -> Option<Task<Result<ViewHandle<Self>>>>;
+ fn add_event_to_update_proto(
+ &self,
+ event: &Self::Event,
+ update: &mut Option<proto::update_view::Variant>,
+ cx: &AppContext,
+ ) -> bool;
+ fn apply_update_proto(
+ &mut self,
+ project: &ModelHandle<Project>,
+ message: proto::update_view::Variant,
+ cx: &mut ViewContext<Self>,
+ ) -> Task<Result<()>>;
+ fn is_project_item(&self, cx: &AppContext) -> bool;
+
+ fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>);
+ fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool;
+}
+
+pub trait FollowableItemHandle: ItemHandle {
+ fn remote_id(&self, client: &Arc<Client>, cx: &AppContext) -> Option<ViewId>;
+ fn set_leader_peer_id(&self, leader_peer_id: Option<PeerId>, cx: &mut WindowContext);
+ fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant>;
+ fn add_event_to_update_proto(
+ &self,
+ event: &dyn Any,
+ update: &mut Option<proto::update_view::Variant>,
+ cx: &AppContext,
+ ) -> bool;
+ fn apply_update_proto(
+ &self,
+ project: &ModelHandle<Project>,
+ message: proto::update_view::Variant,
+ cx: &mut WindowContext,
+ ) -> Task<Result<()>>;
+ fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool;
+ fn is_project_item(&self, cx: &AppContext) -> bool;
+}
+
+impl<T: FollowableItem> FollowableItemHandle for ViewHandle<T> {
+ fn remote_id(&self, client: &Arc<Client>, cx: &AppContext) -> Option<ViewId> {
+ 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<PeerId>, 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<proto::view::Variant> {
+ self.read(cx).to_state_proto(cx)
+ }
+
+ fn add_event_to_update_proto(
+ &self,
+ event: &dyn Any,
+ update: &mut Option<proto::update_view::Variant>,
+ 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<Project>,
+ message: proto::update_view::Variant,
+ cx: &mut WindowContext,
+ ) -> Task<Result<()>> {
+ 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<ProjectEntryId>,
+ pub project_path: Option<ProjectPath>,
+ }
+
+ 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<ModelHandle<TestProjectItem>>,
+ pub nav_history: Option<ItemNavHistory>,
+ pub tab_descriptions: Option<Vec<&'static str>>,
+ pub tab_detail: Cell<Option<usize>>,
+ }
+
+ impl Entity for TestProjectItem {
+ type Event = ();
+ }
+
+ impl project2::Item for TestProjectItem {
+ fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
+ self.entry_id
+ }
+
+ fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
+ 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<Self> {
+ 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<Self> {
+ 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<TestProjectItem>]) -> 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>) {
+ self.push_to_nav_history(cx);
+ self.state = state;
+ }
+
+ fn push_to_nav_history(&mut self, cx: &mut ViewContext<Self>) {
+ if let Some(history) = &mut self.nav_history {
+ history.push(Some(Box::new(self.state.clone())), cx);
+ }
+ }
+ }
+
+ impl Entity for TestItem {
+ type Event = TestItemEvent;
+ }
+
+ impl View for TestItem {
+ fn ui_name() -> &'static str {
+ "TestItem"
+ }
+
+ fn render(&mut self, _: &mut ViewContext<Self>) -> AnyElement<Self> {
+ Empty::new().into_any()
+ }
+ }
+
+ impl Item for TestItem {
+ fn tab_description(&self, detail: usize, _: &AppContext) -> Option<Cow<str>> {
+ self.tab_descriptions.as_ref().and_then(|descriptions| {
+ let description = *descriptions.get(detail).or_else(|| descriptions.last())?;
+ Some(description.into())
+ })
+ }
+
+ fn tab_content<V: 'static>(
+ &self,
+ detail: Option<usize>,
+ _: &theme2::Tab,
+ _: &AppContext,
+ ) -> AnyElement<V> {
+ 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>) {
+ self.nav_history = Some(history);
+ }
+
+ fn navigate(&mut self, state: Box<dyn Any>, _: &mut ViewContext<Self>) -> bool {
+ let state = *state.downcast::<String>().unwrap_or_default();
+ if state != self.state {
+ self.state = state;
+ true
+ } else {
+ false
+ }
+ }
+
+ fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
+ self.push_to_nav_history(cx);
+ }
+
+ fn clone_on_split(
+ &self,
+ _workspace_id: WorkspaceId,
+ _: &mut ViewContext<Self>,
+ ) -> Option<Self>
+ 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<Project>,
+ _: &mut ViewContext<Self>,
+ ) -> Task<anyhow::Result<()>> {
+ self.save_count += 1;
+ self.is_dirty = false;
+ Task::ready(Ok(()))
+ }
+
+ fn save_as(
+ &mut self,
+ _: ModelHandle<Project>,
+ _: std::path::PathBuf,
+ _: &mut ViewContext<Self>,
+ ) -> Task<anyhow::Result<()>> {
+ self.save_as_count += 1;
+ self.is_dirty = false;
+ Task::ready(Ok(()))
+ }
+
+ fn reload(
+ &mut self,
+ _: ModelHandle<Project>,
+ _: &mut ViewContext<Self>,
+ ) -> Task<anyhow::Result<()>> {
+ 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<Project>,
+ _workspace: WeakViewHandle<Workspace>,
+ workspace_id: WorkspaceId,
+ _item_id: ItemId,
+ cx: &mut ViewContext<Pane>,
+ ) -> Task<anyhow::Result<ViewHandle<Self>>> {
+ let view = cx.add_view(|_cx| Self::new_deserialized(workspace_id));
+ Task::Ready(Some(anyhow::Ok(view)))
+ }
+ }
+}
@@ -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: &<Self as Entity>::Event) -> bool;
+}
+
+pub trait NotificationHandle {
+ fn id(&self) -> usize;
+ fn as_any(&self) -> &AnyViewHandle;
+}
+
+impl<T: Notification> NotificationHandle for ViewHandle<T> {
+ 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<TypeId, Vec<usize>>,
+}
+
+impl std::ops::Deref for NotificationTracker {
+ type Target = HashMap<TypeId, Vec<usize>>;
+
+ 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<V: Notification>(
+ &self,
+ id: usize,
+ cx: &ViewContext<Self>,
+ ) -> bool {
+ cx.global::<NotificationTracker>()
+ .get(&TypeId::of::<V>())
+ .map(|ids| ids.contains(&id))
+ .unwrap_or(false)
+ }
+
+ pub fn show_notification_once<V: Notification>(
+ &mut self,
+ id: usize,
+ cx: &mut ViewContext<Self>,
+ build_notification: impl FnOnce(&mut ViewContext<Self>) -> ViewHandle<V>,
+ ) {
+ if !self.has_shown_notification_once::<V>(id, cx) {
+ cx.update_global::<NotificationTracker, _, _>(|tracker, _| {
+ let entry = tracker.entry(TypeId::of::<V>()).or_default();
+ entry.push(id);
+ });
+
+ self.show_notification::<V>(id, cx, build_notification)
+ }
+ }
+
+ pub fn show_notification<V: Notification>(
+ &mut self,
+ id: usize,
+ cx: &mut ViewContext<Self>,
+ build_notification: impl FnOnce(&mut ViewContext<Self>) -> ViewHandle<V>,
+ ) {
+ let type_id = TypeId::of::<V>();
+ 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<V: Notification>(&mut self, id: usize, cx: &mut ViewContext<Self>) {
+ let type_id = TypeId::of::<V>();
+
+ self.dismiss_notification_internal(type_id, id, cx)
+ }
+
+ pub fn show_toast(&mut self, toast: Toast, cx: &mut ViewContext<Self>) {
+ self.dismiss_notification::<simple_message_notification::MessageNotification>(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>) {
+ self.dismiss_notification::<simple_message_notification::MessageNotification>(id, cx);
+ }
+
+ fn dismiss_notification_internal(
+ &mut self,
+ type_id: TypeId,
+ id: usize,
+ cx: &mut ViewContext<Self>,
+ ) {
+ 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<I: Into<Cow<'static, str>>>(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<Workspace>| {
+ cx.platform().open_url(open_action.0.as_ref());
+ },
+ )
+ }
+
+ enum NotificationMessage {
+ Text(Cow<'static, str>),
+ Element(fn(TextStyle, &AppContext) -> AnyElement<MessageNotification>),
+ }
+
+ pub struct MessageNotification {
+ message: NotificationMessage,
+ on_click: Option<Arc<dyn Fn(&mut ViewContext<Self>)>>,
+ click_message: Option<Cow<'static, str>>,
+ }
+
+ pub enum MessageNotificationEvent {
+ Dismiss,
+ }
+
+ impl Entity for MessageNotification {
+ type Event = MessageNotificationEvent;
+ }
+
+ impl MessageNotification {
+ pub fn new<S>(message: S) -> MessageNotification
+ where
+ S: Into<Cow<'static, str>>,
+ {
+ Self {
+ message: NotificationMessage::Text(message.into()),
+ on_click: None,
+ click_message: None,
+ }
+ }
+
+ pub fn new_element(
+ message: fn(TextStyle, &AppContext) -> AnyElement<MessageNotification>,
+ ) -> MessageNotification {
+ Self {
+ message: NotificationMessage::Element(message),
+ on_click: None,
+ click_message: None,
+ }
+ }
+
+ pub fn with_click_message<S>(mut self, message: S) -> Self
+ where
+ S: Into<Cow<'static, str>>,
+ {
+ self.click_message = Some(message.into());
+ self
+ }
+
+ pub fn on_click<F>(mut self, on_click: F) -> Self
+ where
+ F: 'static + Fn(&mut ViewContext<Self>),
+ {
+ self.on_click = Some(Arc::new(on_click));
+ self
+ }
+
+ pub fn dismiss(&mut self, _: &CancelMessageNotification, cx: &mut ViewContext<Self>) {
+ cx.emit(MessageNotificationEvent::Dismiss);
+ }
+ }
+
+ impl View for MessageNotification {
+ fn ui_name() -> &'static str {
+ "MessageNotification"
+ }
+
+ fn render(&mut self, cx: &mut gpui2::ViewContext<Self>) -> gpui::AnyElement<Self> {
+ 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::<Cancel, _>(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::<MessageNotificationTag, _>(
+ 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: &<Self as Entity>::Event) -> bool {
+ match event {
+ MessageNotificationEvent::Dismiss => true,
+ }
+ }
+ }
+}
+
+pub trait NotifyResultExt {
+ type Ok;
+
+ fn notify_err(
+ self,
+ workspace: &mut Workspace,
+ cx: &mut ViewContext<Workspace>,
+ ) -> Option<Self::Ok>;
+}
+
+impl<T, E> NotifyResultExt for Result<T, E>
+where
+ E: std::fmt::Debug,
+{
+ type Ok = T;
+
+ fn notify_err(self, workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Option<T> {
+ 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
+ }
+ }
+ }
+}
@@ -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<Pane>,
+}
+
+#[derive(Clone, PartialEq)]
+pub struct CloseItemsToTheLeftById {
+ pub item_id: usize,
+ pub pane: WeakViewHandle<Pane>,
+}
+
+#[derive(Clone, PartialEq)]
+pub struct CloseItemsToTheRightById {
+ pub item_id: usize,
+ pub pane: WeakViewHandle<Pane>,
+}
+
+#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
+#[serde(rename_all = "camelCase")]
+pub struct CloseActiveItem {
+ pub save_intent: Option<SaveIntent>,
+}
+
+#[derive(Clone, PartialEq, Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct CloseAllItems {
+ pub save_intent: Option<SaveIntent>,
+}
+
+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<dyn ItemHandle> },
+ ActivateItem { local: bool },
+ Remove,
+ RemoveItem { item_id: usize },
+ Split(SplitDirection),
+ ChangeItemTitle,
+ Focus,
+ ZoomIn,
+ ZoomOut,
+}
+
+pub struct Pane {
+ items: Vec<Box<dyn ItemHandle>>,
+ activation_history: Vec<usize>,
+ zoomed: bool,
+ active_item_index: usize,
+ last_focused_view_by_item: HashMap<usize, AnyWeakViewHandle>,
+ autoscroll: bool,
+ nav_history: NavHistory,
+ toolbar: ViewHandle<Toolbar>,
+ tab_bar_context_menu: TabBarContextMenu,
+ tab_context_menu: ViewHandle<ContextMenu>,
+ _background_actions: BackgroundActions,
+ workspace: WeakViewHandle<Workspace>,
+ project: ModelHandle<Project>,
+ has_focus: bool,
+ can_drop: Rc<dyn Fn(&DragAndDrop<Workspace>, &WindowContext) -> bool>,
+ can_split: bool,
+ render_tab_bar_buttons: Rc<dyn Fn(&mut Pane, &mut ViewContext<Pane>) -> AnyElement<Pane>>,
+}
+
+pub struct ItemNavHistory {
+ history: NavHistory,
+ item: Rc<dyn WeakItemHandle>,
+}
+
+#[derive(Clone)]
+pub struct NavHistory(Rc<RefCell<NavHistoryState>>);
+
+struct NavHistoryState {
+ mode: NavigationMode,
+ backward_stack: VecDeque<NavigationEntry>,
+ forward_stack: VecDeque<NavigationEntry>,
+ closed_stack: VecDeque<NavigationEntry>,
+ paths_by_item: HashMap<usize, (ProjectPath, Option<PathBuf>)>,
+ pane: WeakViewHandle<Pane>,
+ next_timestamp: Arc<AtomicUsize>,
+}
+
+#[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<dyn WeakItemHandle>,
+ pub data: Option<Box<dyn Any>>,
+ pub timestamp: usize,
+}
+
+pub struct DraggedItem {
+ pub handle: Box<dyn ItemHandle>,
+ pub pane: WeakViewHandle<Pane>,
+}
+
+pub enum ReorderBehavior {
+ None,
+ MoveAfterActive,
+ MoveToIndex(usize),
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+enum TabBarContextMenuKind {
+ New,
+ Split,
+}
+
+struct TabBarContextMenu {
+ kind: TabBarContextMenuKind,
+ handle: ViewHandle<ContextMenu>,
+}
+
+impl TabBarContextMenu {
+ fn handle_if_kind(&self, kind: TabBarContextMenuKind) -> Option<ViewHandle<ContextMenu>> {
+ if self.kind == kind {
+ return Some(self.handle.clone());
+ }
+ None
+ }
+}
+
+#[allow(clippy::too_many_arguments)]
+fn nav_button<A: Action, F: 'static + Fn(&mut Pane, &mut ViewContext<Pane>)>(
+ svg_path: &'static str,
+ style: theme2::Interactive<theme2::IconButton>,
+ nav_button_height: f32,
+ tooltip_style: TooltipStyle,
+ enabled: bool,
+ on_click: F,
+ tooltip_action: A,
+ action_name: &str,
+ cx: &mut ViewContext<Pane>,
+) -> AnyElement<Pane> {
+ MouseEventHandler::new::<A, _>(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::<A>(
+ 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<Workspace>,
+ project: ModelHandle<Project>,
+ background_actions: BackgroundActions,
+ next_timestamp: Arc<AtomicUsize>,
+ cx: &mut ViewContext<Self>,
+ ) -> 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<Workspace> {
+ &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<F>(&mut self, can_drop: F)
+ where
+ F: 'static + Fn(&DragAndDrop<Workspace>, &WindowContext) -> bool,
+ {
+ self.can_drop = Rc::new(can_drop);
+ }
+
+ pub fn set_can_split(&mut self, can_split: bool, cx: &mut ViewContext<Self>) {
+ self.can_split = can_split;
+ cx.notify();
+ }
+
+ pub fn set_can_navigate(&mut self, can_navigate: bool, cx: &mut ViewContext<Self>) {
+ self.toolbar.update(cx, |toolbar, cx| {
+ toolbar.set_can_navigate(can_navigate, cx);
+ });
+ cx.notify();
+ }
+
+ pub fn set_render_tab_bar_buttons<F>(&mut self, cx: &mut ViewContext<Self>, render: F)
+ where
+ F: 'static + Fn(&mut Pane, &mut ViewContext<Pane>) -> AnyElement<Pane>,
+ {
+ self.render_tab_bar_buttons = Rc::new(render);
+ cx.notify();
+ }
+
+ pub fn nav_history_for_item<T: Item>(&self, item: &ViewHandle<T>) -> 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>) {
+ self.toolbar.update(cx, |_, cx| cx.notify());
+ }
+
+ pub(crate) fn open_item(
+ &mut self,
+ project_entry_id: ProjectEntryId,
+ focus_item: bool,
+ cx: &mut ViewContext<Self>,
+ build_item: impl FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
+ ) -> Box<dyn ItemHandle> {
+ 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<dyn ItemHandle>,
+ activate_pane: bool,
+ focus_item: bool,
+ destination_index: Option<usize>,
+ cx: &mut ViewContext<Self>,
+ ) {
+ 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<Item = &Box<dyn ItemHandle>> + DoubleEndedIterator {
+ self.items.iter()
+ }
+
+ pub fn items_of_type<T: View>(&self) -> impl '_ + Iterator<Item = ViewHandle<T>> {
+ self.items
+ .iter()
+ .filter_map(|item| item.as_any().clone().downcast())
+ }
+
+ pub fn active_item(&self) -> Option<Box<dyn ItemHandle>> {
+ self.items.get(self.active_item_index).cloned()
+ }
+
+ pub fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Vector2F> {
+ self.items
+ .get(self.active_item_index)?
+ .pixel_position_of_cursor(cx)
+ }
+
+ pub fn item_for_entry(
+ &self,
+ entry_id: ProjectEntryId,
+ cx: &AppContext,
+ ) -> Option<Box<dyn ItemHandle>> {
+ 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<usize> {
+ self.items.iter().position(|i| i.id() == item.id())
+ }
+
+ pub fn toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext<Self>) {
+ // 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<Self>,
+ ) {
+ 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<Self>) {
+ 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<Self>) {
+ 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<Self>,
+ ) -> Option<Task<Result<()>>> {
+ 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<Self>,
+ ) -> Task<Result<()>> {
+ 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<Self>,
+ ) -> Option<Task<Result<()>>> {
+ 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<Self>,
+ ) -> Option<Task<Result<()>>> {
+ 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<Self>,
+ ) -> Option<Task<Result<()>>> {
+ 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<Self>,
+ ) -> Task<Result<()>> {
+ 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<Self>,
+ ) -> Option<Task<Result<()>>> {
+ 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<Self>,
+ ) -> Task<Result<()>> {
+ 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<Self>,
+ ) -> Option<Task<Result<()>>> {
+ 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<Item = &Box<dyn ItemHandle>>,
+ 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<Pane>,
+ mut save_intent: SaveIntent,
+ should_close: impl 'static + Fn(usize) -> bool,
+ ) -> Task<Result<()>> {
+ // 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>,
+ ) {
+ 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<Project>,
+ pane: &WeakViewHandle<Pane>,
+ item_ix: usize,
+ item: &dyn ItemHandle,
+ save_intent: SaveIntent,
+ cx: &mut AsyncAppContext,
+ ) -> Result<bool> {
+ 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::<WorkspaceSettings>(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<Project>,
+ cx: &mut WindowContext,
+ ) -> Task<Result<()>> {
+ 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<Self>) {
+ if let Some(active_item) = self.active_item() {
+ cx.focus(active_item.as_any());
+ }
+ }
+
+ pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext<Self>) {
+ cx.emit(Event::Split(direction));
+ }
+
+ fn deploy_split_menu(&mut self, cx: &mut ViewContext<Self>) {
+ 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>) {
+ 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<Self>,
+ ) {
+ 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<Toolbar> {
+ &self.toolbar
+ }
+
+ pub fn handle_deleted_project_item(
+ &mut self,
+ entry_id: ProjectEntryId,
+ cx: &mut ViewContext<Pane>,
+ ) -> 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<Self>) {
+ 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<Self>) -> impl Element<Self> {
+ 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::<Tabs>(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::<TabDragReceiver, _, _>(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::<ItemSettings>(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::<Tab, _>(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::<Self>(
+ 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<Workspace>| {
+ 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::<Filler, _, _>(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<usize> {
+ let mut tab_details = (0..self.items.len()).map(|_| 0).collect::<Vec<_>>();
+
+ 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<dyn ItemHandle>,
+ pane: WeakViewHandle<Pane>,
+ first: bool,
+ detail: Option<usize>,
+ hovered: bool,
+ tab_style: &theme::Tab,
+ cx: &mut ViewContext<Self>,
+ ) -> AnyElement<Self> {
+ 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<dyn ItemHandle>,
+ pane: WeakViewHandle<Pane>,
+ first: bool,
+ detail: Option<usize>,
+ hovered: bool,
+ tab_style: &theme::Tab,
+ cx: &mut ViewContext<Workspace>,
+ ) -> AnyElement<Workspace> {
+ 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<T: View>(
+ title: AnyElement<T>,
+ item: &Box<dyn ItemHandle>,
+ pane: WeakViewHandle<Pane>,
+ first: bool,
+ hovered: bool,
+ tab_style: &theme::Tab,
+ cx: &mut ViewContext<T>,
+ ) -> AnyElement<T> {
+ 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::<TabCloseButton, _>(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::<ItemSettings>(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<Pane>),
+ F2: 'static + Fn(&mut Pane, &mut EventContext<Pane>),
+ >(
+ index: usize,
+ icon: &'static str,
+ is_active: bool,
+ tooltip: Option<(&'static str, Option<Box<dyn Action>>)>,
+ cx: &mut ViewContext<Pane>,
+ on_click: F1,
+ on_down: F2,
+ context_menu: Option<ViewHandle<ContextMenu>>,
+ ) -> AnyElement<Pane> {
+ enum TabBarButton {}
+
+ let mut button = MouseEventHandler::new::<TabBarButton, _>(index, cx, |mouse_state, cx| {
+ let theme = &settings::get::<ThemeSettings>(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::<ThemeSettings>(cx).theme.tooltip.clone();
+ button = button
+ .with_tooltip::<TabBarButton>(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<Self>) -> AnyElement<Self> {
+ 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>) {
+ 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<Self>) -> AnyElement<Self> {
+ enum MouseNavigationHandler {}
+
+ MouseEventHandler::new::<MouseNavigationHandler, _>(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::<TabBarEventHandler, _>(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::<PaneContentTabDropTarget, _, _>(
+ 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::<EmptyPane, _, _>(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<Self>) {
+ 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>) {
+ 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<D: 'static + Any>(&mut self, data: Option<D>, cx: &mut WindowContext) {
+ self.history.push(data, self.item.clone(), cx);
+ }
+
+ pub fn pop_backward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
+ self.history.pop(NavigationMode::GoingBack, cx)
+ }
+
+ pub fn pop_forward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
+ self.history.pop(NavigationMode::GoingForward, cx)
+ }
+}
+
+impl NavHistory {
+ pub fn for_each_entry(
+ &self,
+ cx: &AppContext,
+ mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
+ ) {
+ 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<NavigationEntry> {
+ 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<D: 'static + Any>(
+ &mut self,
+ data: Option<D>,
+ item: Rc<dyn WeakItemHandle>,
+ 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<dyn Any>),
+ 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<dyn Any>),
+ 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<dyn Any>),
+ 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<dyn Any>),
+ 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<PathBuf>)> {
+ 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<V> {
+ child_view: usize,
+ child: AnyElement<V>,
+}
+
+impl<V> PaneBackdrop<V> {
+ pub fn new(pane_item_view: usize, child: AnyElement<V>) -> Self {
+ PaneBackdrop {
+ child,
+ child_view: pane_item_view,
+ }
+ }
+}
+
+impl<V: 'static> Element<V> for PaneBackdrop<V> {
+ type LayoutState = ();
+
+ type PaintState = ();
+
+ fn layout(
+ &mut self,
+ constraint: gpui::SizeConstraint,
+ view: &mut V,
+ cx: &mut ViewContext<V>,
+ ) -> (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<V>,
+ ) -> 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::<Self>(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<usize>,
+ _bounds: RectF,
+ _visible_bounds: RectF,
+ _layout: &Self::LayoutState,
+ _paint: &Self::PaintState,
+ view: &V,
+ cx: &gpui::ViewContext<V>,
+ ) -> Option<RectF> {
+ 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<V>,
+ ) -> 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<ProjectPath>) -> 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<Pane>,
+ label: &str,
+ is_dirty: bool,
+ cx: &mut TestAppContext,
+ ) -> Box<ViewHandle<TestItem>> {
+ 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<const COUNT: usize>(
+ pane: &ViewHandle<Pane>,
+ labels: [&str; COUNT],
+ cx: &mut TestAppContext,
+ ) -> [Box<ViewHandle<TestItem>>; 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<const COUNT: usize>(
+ pane: &ViewHandle<Pane>,
+ 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::<TestItem>()
+ .unwrap()
+ .read(cx)
+ .label
+ .clone();
+ if ix == pane.active_item_index {
+ state.push('*');
+ }
+ if item.is_dirty(cx) {
+ state.push('^');
+ }
+ state
+ })
+ .collect::<Vec<_>>();
+
+ assert_eq!(
+ actual_states, expected_states,
+ "pane items do not match expectation"
+ );
+ })
+ }
+}
@@ -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<Tag, D, F>(
+ pane: &Pane,
+ region_id: usize,
+ drop_index: usize,
+ allow_same_pane: bool,
+ split_margin: Option<f32>,
+ cx: &mut ViewContext<Pane>,
+ render_child: F,
+) -> MouseEventHandler<Pane>
+where
+ Tag: 'static,
+ D: Element<Pane>,
+ F: FnOnce(&mut MouseState, &mut ViewContext<Pane>) -> D,
+{
+ let drag_and_drop = cx.global::<DragAndDrop<Workspace>>();
+ let drag_position = if (pane.can_drop)(drag_and_drop, cx) {
+ drag_and_drop
+ .currently_dragged::<DraggedItem>(cx.window())
+ .map(|(drag_position, _)| drag_position)
+ .or_else(|| {
+ drag_and_drop
+ .currently_dragged::<ProjectEntryId>(cx.window())
+ .map(|(drag_position, _)| drag_position)
+ })
+ } else {
+ None
+ };
+
+ let mut handler = MouseEventHandler::above::<Tag, _>(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::<DragAndDrop<Workspace>>();
+
+ if drag_and_drop
+ .currently_dragged::<DraggedItem>(cx.window())
+ .is_some()
+ || drag_and_drop
+ .currently_dragged::<ProjectEntryId>(cx.window())
+ .is_some()
+ {
+ cx.notify();
+ } else {
+ cx.propagate_event();
+ }
+ })
+ }
+
+ handler
+}
+
+pub fn handle_dropped_item<V: 'static>(
+ event: MouseUp,
+ workspace: WeakViewHandle<Workspace>,
+ pane: &WeakViewHandle<Pane>,
+ index: usize,
+ allow_same_pane: bool,
+ split_margin: Option<f32>,
+ cx: &mut EventContext<V>,
+) {
+ enum Action {
+ Move(WeakViewHandle<Pane>, usize),
+ Open(ProjectEntryId),
+ }
+ let drag_and_drop = cx.global::<DragAndDrop<Workspace>>();
+ let action = if let Some((_, dragged_item)) =
+ drag_and_drop.currently_dragged::<DraggedItem>(cx.window())
+ {
+ Action::Move(dragged_item.pane.clone(), dragged_item.handle.id())
+ } else if let Some((_, project_entry)) =
+ drag_and_drop.currently_dragged::<ProjectEntryId>(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<SplitDirection> {
+ 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
+}
@@ -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<Pane>) -> Self {
+ Self {
+ root: Member::Pane(pane),
+ }
+ }
+
+ pub fn split(
+ &mut self,
+ old_pane: &ViewHandle<Pane>,
+ new_pane: &ViewHandle<Pane>,
+ 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<Pane>) -> Option<RectF> {
+ 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<Pane>> {
+ 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<Pane>) -> Result<bool> {
+ 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<Pane>, to: &ViewHandle<Pane>) {
+ match &mut self.root {
+ Member::Pane(_) => {}
+ Member::Axis(axis) => axis.swap(from, to),
+ };
+ }
+
+ pub(crate) fn render(
+ &self,
+ project: &ModelHandle<Project>,
+ theme: &Theme,
+ follower_states: &HashMap<ViewHandle<Pane>, FollowerState>,
+ active_call: Option<&ModelHandle<ActiveCall>>,
+ active_pane: &ViewHandle<Pane>,
+ zoomed: Option<&AnyViewHandle>,
+ app_state: &Arc<AppState>,
+ cx: &mut ViewContext<Workspace>,
+ ) -> AnyElement<Workspace> {
+ self.root.render(
+ project,
+ 0,
+ theme,
+ follower_states,
+ active_call,
+ active_pane,
+ zoomed,
+ app_state,
+ cx,
+ )
+ }
+
+ pub(crate) fn panes(&self) -> Vec<&ViewHandle<Pane>> {
+ let mut panes = Vec::new();
+ self.root.collect_panes(&mut panes);
+ panes
+ }
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub(crate) enum Member {
+ Axis(PaneAxis),
+ Pane(ViewHandle<Pane>),
+}
+
+impl Member {
+ fn new_axis(
+ old_pane: ViewHandle<Pane>,
+ new_pane: ViewHandle<Pane>,
+ 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<Pane>) -> 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<Project>,
+ basis: usize,
+ theme: &Theme,
+ follower_states: &HashMap<ViewHandle<Pane>, FollowerState>,
+ active_call: Option<&ModelHandle<ActiveCall>>,
+ active_pane: &ViewHandle<Pane>,
+ zoomed: Option<&AnyViewHandle>,
+ app_state: &Arc<AppState>,
+ cx: &mut ViewContext<Workspace>,
+ ) -> AnyElement<Workspace> {
+ 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::<FollowIntoExternalProject, _>(
+ 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<Pane>>) {
+ 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<Member>,
+ pub flexes: Rc<RefCell<Vec<f32>>>,
+ pub bounding_boxes: Rc<RefCell<Vec<Option<RectF>>>>,
+}
+
+impl PaneAxis {
+ pub fn new(axis: Axis, members: Vec<Member>) -> 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<Member>, flexes: Option<Vec<f32>>) -> 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<Pane>,
+ new_pane: &ViewHandle<Pane>,
+ 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<Pane>) -> Result<Option<Member>> {
+ 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<Pane>, to: &ViewHandle<Pane>) {
+ 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<Pane>) -> Option<RectF> {
+ 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<Pane>> {
+ 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<Project>,
+ basis: usize,
+ theme: &Theme,
+ follower_states: &HashMap<ViewHandle<Pane>, FollowerState>,
+ active_call: Option<&ModelHandle<ActiveCall>>,
+ active_pane: &ViewHandle<Pane>,
+ zoomed: Option<&AnyViewHandle>,
+ app_state: &Arc<AppState>,
+ cx: &mut ViewContext<Workspace>,
+ ) -> AnyElement<Workspace> {
+ 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<usize>,
+ flexes: Rc<RefCell<Vec<f32>>>,
+ children: Vec<AnyElement<Workspace>>,
+ bounding_boxes: Rc<RefCell<Vec<Option<RectF>>>>,
+ }
+
+ impl PaneAxisElement {
+ pub fn new(
+ axis: Axis,
+ basis: usize,
+ flexes: Rc<RefCell<Vec<f32>>>,
+ bounding_boxes: Rc<RefCell<Vec<Option<RectF>>>>,
+ ) -> 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<usize>) {
+ 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<Workspace>,
+ ) {
+ 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<RefCell<Vec<f32>>>,
+ axis: Axis,
+ preceding_ix: usize,
+ child_start: Vector2F,
+ drag_bounds: RectF,
+ ) -> impl Fn(MouseDrag, &mut Workspace, &mut EventContext<Workspace>) {
+ 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<AnyElement<Workspace>> for PaneAxisElement {
+ fn extend<T: IntoIterator<Item = AnyElement<Workspace>>>(&mut self, children: T) {
+ self.children.extend(children);
+ }
+ }
+
+ impl Element<Workspace> for PaneAxisElement {
+ type LayoutState = f32;
+ type PaintState = ();
+
+ fn layout(
+ &mut self,
+ constraint: SizeConstraint,
+ view: &mut Workspace,
+ cx: &mut ViewContext<Workspace>,
+ ) -> (Vector2F, Self::LayoutState) {
+ debug_assert!(self.children.len() == self.flexes.borrow().len());
+
+ let active_pane_magnification =
+ settings::get::<WorkspaceSettings>(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<Workspace>,
+ ) -> Self::PaintState {
+ let can_resize = settings::get::<WorkspaceSettings>(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::<ResizeHandle>(
+ 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<usize>,
+ _: RectF,
+ _: RectF,
+ _: &Self::LayoutState,
+ _: &Self::PaintState,
+ view: &Workspace,
+ cx: &ViewContext<Workspace>,
+ ) -> Option<RectF> {
+ 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<Workspace>,
+ ) -> 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::<Vec<json::Value>>()
+ })
+ }
+ }
+}
@@ -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<Vec<PathBuf>>,
+ // dock_visible: bool, // Deprecated
+ // dock_anchor: DockAnchor, // Deprecated
+ // dock_pane: Option<usize>, // Deprecated
+ // left_sidebar_open: boolean,
+ // timestamp: String, // UTC YYYY-MM-DD HH:MM:SS
+ // window_state: String, // WindowBounds Discriminant
+ // window_x: Option<f32>, // WindowBounds::Fixed RectF x
+ // window_y: Option<f32>, // WindowBounds::Fixed RectF y
+ // window_width: Option<f32>, // WindowBounds::Fixed RectF width
+ // window_height: Option<f32>, // WindowBounds::Fixed RectF height
+ // display: Option<Uuid>, // Display id
+ // )
+ //
+ // pane_groups(
+ // group_id: usize, // Primary key for pane_groups
+ // workspace_id: usize, // References workspaces table
+ // parent_group_id: Option<usize>, // None indicates that this is the root node
+ // position: Optiopn<usize>, // None indicates that this is the root node
+ // axis: Option<Axis>, // 'Vertical', 'Horizontal'
+ // flexes: Option<Vec<f32>>, // 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<usize>, // References pane_groups. If none, this is the root
+ // position: Option<usize>, // 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<P: AsRef<Path>>(
+ &self,
+ worktree_roots: &[P],
+ ) -> Option<SerializedWorkspace> {
+ 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<WindowBounds>,
+ Option<Uuid>,
+ 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<WorkspaceId> {
+ INSERT INTO workspaces DEFAULT VALUES RETURNING workspace_id
+ }
+ }
+
+ query! {
+ fn recent_workspaces() -> Result<Vec<(WorkspaceId, WorkspaceLocation)>> {
+ 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<Vec<(WorkspaceId, WorkspaceLocation)>> {
+ 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<Option<WorkspaceLocation>> {
+ Ok(self
+ .recent_workspaces_on_disk()
+ .await?
+ .into_iter()
+ .next()
+ .map(|(_, location)| location))
+ }
+
+ fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result<SerializedPaneGroup> {
+ 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<GroupId>,
+ ) -> Result<Vec<SerializedPaneGroup>> {
+ type GroupKey = (Option<GroupId>, WorkspaceId);
+ type GroupOrPane = (
+ Option<GroupId>,
+ Option<Axis>,
+ Option<PaneId>,
+ Option<bool>,
+ Option<String>,
+ );
+ self.select_bound::<GroupKey, GroupOrPane>(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::<Vec<f32>>(&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::<Result<_>>()
+ }
+
+ 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<PaneId> {
+ 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<Vec<SerializedItem>> {
+ 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::<WorkspaceId, WorkspaceId>(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 {
+ 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<P: AsRef<Path>>(
+ 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);
+ }
+}
@@ -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<Vec<PathBuf>>);
+
+impl WorkspaceLocation {
+ pub fn paths(&self) -> Arc<Vec<PathBuf>> {
+ self.0.clone()
+ }
+}
+
+impl<P: AsRef<Path>, T: IntoIterator<Item = P>> From<T> for WorkspaceLocation {
+ fn from(iterator: T) -> Self {
+ let mut roots = iterator
+ .into_iter()
+ .map(|p| p.as_ref().to_path_buf())
+ .collect::<Vec<_>>();
+ roots.sort();
+ Self(Arc::new(roots))
+ }
+}
+
+impl StaticColumnCount for WorkspaceLocation {}
+impl Bind for &WorkspaceLocation {
+ fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
+ 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<WindowBounds>,
+ pub display: Option<Uuid>,
+ 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<i32> {
+ 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<String>,
+ pub(crate) zoom: bool,
+}
+
+impl Column for DockData {
+ fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
+ let (visible, next_index) = Option::<bool>::column(statement, start_index)?;
+ let (active_panel, next_index) = Option::<String>::column(statement, next_index)?;
+ let (zoom, next_index) = Option::<bool>::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<i32> {
+ 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<Vec<f32>>,
+ children: Vec<SerializedPaneGroup>,
+ },
+ 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<Project>,
+ workspace_id: WorkspaceId,
+ workspace: &WeakViewHandle<Workspace>,
+ cx: &mut AsyncAppContext,
+ ) -> Option<(
+ Member,
+ Option<ViewHandle<Pane>>,
+ Vec<Option<Box<dyn ItemHandle>>>,
+ )> {
+ 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<SerializedItem>,
+}
+
+impl SerializedPane {
+ pub fn new(children: Vec<SerializedItem>, active: bool) -> Self {
+ SerializedPane { children, active }
+ }
+
+ pub async fn deserialize_to(
+ &self,
+ project: &ModelHandle<Project>,
+ pane: &WeakViewHandle<Pane>,
+ workspace_id: WorkspaceId,
+ workspace: &WeakViewHandle<Workspace>,
+ cx: &mut AsyncAppContext,
+ ) -> Result<Vec<Option<Box<dyn ItemHandle>>>> {
+ 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::<ItemDeserializers>().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<str>,
+ pub item_id: ItemId,
+ pub active: bool,
+}
+
+impl SerializedItem {
+ pub fn new(kind: impl AsRef<str>, 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<i32> {
+ 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::<str>::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,
+ ))
+ }
+}
@@ -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<Self>,
+ ) -> Option<SearchEvent>;
+ fn clear_matches(&mut self, cx: &mut ViewContext<Self>);
+ fn update_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>);
+ fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String;
+ fn activate_match(
+ &mut self,
+ index: usize,
+ matches: Vec<Self::Match>,
+ cx: &mut ViewContext<Self>,
+ );
+ fn select_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>);
+ fn replace(&mut self, _: &Self::Match, _: &SearchQuery, _: &mut ViewContext<Self>);
+ fn match_index_for_direction(
+ &mut self,
+ matches: &Vec<Self::Match>,
+ current_index: usize,
+ direction: Direction,
+ count: usize,
+ _: &mut ViewContext<Self>,
+ ) -> 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<SearchQuery>,
+ cx: &mut ViewContext<Self>,
+ ) -> Task<Vec<Self::Match>>;
+ fn active_match_index(
+ &mut self,
+ matches: Vec<Self::Match>,
+ cx: &mut ViewContext<Self>,
+ ) -> Option<usize>;
+}
+
+pub trait SearchableItemHandle: ItemHandle {
+ fn downgrade(&self) -> Box<dyn WeakSearchableItemHandle>;
+ fn boxed_clone(&self) -> Box<dyn SearchableItemHandle>;
+ fn supported_options(&self) -> SearchOptions;
+ fn subscribe_to_search_events(
+ &self,
+ cx: &mut WindowContext,
+ handler: Box<dyn Fn(SearchEvent, &mut WindowContext)>,
+ ) -> Subscription;
+ fn clear_matches(&self, cx: &mut WindowContext);
+ fn update_matches(&self, matches: &Vec<Box<dyn Any + Send>>, cx: &mut WindowContext);
+ fn query_suggestion(&self, cx: &mut WindowContext) -> String;
+ fn activate_match(
+ &self,
+ index: usize,
+ matches: &Vec<Box<dyn Any + Send>>,
+ cx: &mut WindowContext,
+ );
+ fn select_matches(&self, matches: &Vec<Box<dyn Any + Send>>, cx: &mut WindowContext);
+ fn replace(&self, _: &Box<dyn Any + Send>, _: &SearchQuery, _: &mut WindowContext);
+ fn match_index_for_direction(
+ &self,
+ matches: &Vec<Box<dyn Any + Send>>,
+ current_index: usize,
+ direction: Direction,
+ count: usize,
+ cx: &mut WindowContext,
+ ) -> usize;
+ fn find_matches(
+ &self,
+ query: Arc<SearchQuery>,
+ cx: &mut WindowContext,
+ ) -> Task<Vec<Box<dyn Any + Send>>>;
+ fn active_match_index(
+ &self,
+ matches: &Vec<Box<dyn Any + Send>>,
+ cx: &mut WindowContext,
+ ) -> Option<usize>;
+}
+
+impl<T: SearchableItem> SearchableItemHandle for ViewHandle<T> {
+ fn downgrade(&self) -> Box<dyn WeakSearchableItemHandle> {
+ Box::new(self.downgrade())
+ }
+
+ fn boxed_clone(&self) -> Box<dyn SearchableItemHandle> {
+ Box::new(self.clone())
+ }
+
+ fn supported_options(&self) -> SearchOptions {
+ T::supported_options()
+ }
+
+ fn subscribe_to_search_events(
+ &self,
+ cx: &mut WindowContext,
+ handler: Box<dyn Fn(SearchEvent, &mut WindowContext)>,
+ ) -> 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<Box<dyn Any + Send>>, 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<Box<dyn Any + Send>>,
+ 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<Box<dyn Any + Send>>, 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<Box<dyn Any + Send>>,
+ 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<SearchQuery>,
+ cx: &mut WindowContext,
+ ) -> Task<Vec<Box<dyn Any + Send>>> {
+ let matches = self.update(cx, |this, cx| this.find_matches(query, cx));
+ cx.foreground().spawn(async {
+ let matches = matches.await;
+ matches
+ .into_iter()
+ .map::<Box<dyn Any + Send>, _>(|range| Box::new(range))
+ .collect()
+ })
+ }
+ fn active_match_index(
+ &self,
+ matches: &Vec<Box<dyn Any + Send>>,
+ cx: &mut WindowContext,
+ ) -> Option<usize> {
+ let matches = downcast_matches(matches);
+ self.update(cx, |this, cx| this.active_match_index(matches, cx))
+ }
+
+ fn replace(&self, matches: &Box<dyn Any + Send>, query: &SearchQuery, cx: &mut WindowContext) {
+ let matches = matches.downcast_ref().unwrap();
+ self.update(cx, |this, cx| this.replace(matches, query, cx))
+ }
+}
+
+fn downcast_matches<T: Any + Clone>(matches: &Vec<Box<dyn Any + Send>>) -> Vec<T> {
+ matches
+ .iter()
+ .map(|range| range.downcast_ref::<T>().cloned())
+ .collect::<Option<Vec<_>>>()
+ .expect(
+ "SearchableItemHandle function called with vec of matches of a different type than expected",
+ )
+}
+
+impl From<Box<dyn SearchableItemHandle>> for AnyViewHandle {
+ fn from(this: Box<dyn SearchableItemHandle>) -> Self {
+ this.as_any().clone()
+ }
+}
+
+impl From<&Box<dyn SearchableItemHandle>> for AnyViewHandle {
+ fn from(this: &Box<dyn SearchableItemHandle>) -> Self {
+ this.as_any().clone()
+ }
+}
+
+impl PartialEq for Box<dyn SearchableItemHandle> {
+ fn eq(&self, other: &Self) -> bool {
+ self.id() == other.id() && self.window() == other.window()
+ }
+}
+
+impl Eq for Box<dyn SearchableItemHandle> {}
+
+pub trait WeakSearchableItemHandle: WeakItemHandle {
+ fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn SearchableItemHandle>>;
+
+ fn into_any(self) -> AnyWeakViewHandle;
+}
+
+impl<T: SearchableItem> WeakSearchableItemHandle for WeakViewHandle<T> {
+ fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn SearchableItemHandle>> {
+ Some(Box::new(self.upgrade(cx)?))
+ }
+
+ fn into_any(self) -> AnyWeakViewHandle {
+ self.into_any()
+ }
+}
+
+impl PartialEq for Box<dyn WeakSearchableItemHandle> {
+ fn eq(&self, other: &Self) -> bool {
+ self.id() == other.id() && self.window() == other.window()
+ }
+}
+
+impl Eq for Box<dyn WeakSearchableItemHandle> {}
+
+impl std::hash::Hash for Box<dyn WeakSearchableItemHandle> {
+ fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+ (self.id(), self.window().id()).hash(state)
+ }
+}
@@ -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<RemoteVideoTrack>,
+ frame: Option<Frame>,
+ pub peer_id: PeerId,
+ user: Arc<User>,
+ nav_history: Option<ItemNavHistory>,
+ _maintain_frame: Task<Result<()>>,
+}
+
+impl SharedScreen {
+ pub fn new(
+ track: &Arc<RemoteVideoTrack>,
+ peer_id: PeerId,
+ user: Arc<User>,
+ cx: &mut ViewContext<Self>,
+ ) -> 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<Self>) -> AnyElement<Self> {
+ enum Focus {}
+
+ let frame = self.frame.clone();
+ MouseEventHandler::new::<Focus, _>(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<Cow<str>> {
+ Some(format!("{}'s screen", self.user.github_login).into())
+ }
+ fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
+ if let Some(nav_history) = self.nav_history.as_mut() {
+ nav_history.push::<()>(None, cx);
+ }
+ }
+
+ fn tab_content<V: 'static>(
+ &self,
+ _: Option<usize>,
+ style: &theme::Tab,
+ _: &AppContext,
+ ) -> gpui::AnyElement<V> {
+ 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>) {
+ self.nav_history = Some(history);
+ }
+
+ fn clone_on_split(
+ &self,
+ _workspace_id: WorkspaceId,
+ cx: &mut ViewContext<Self>,
+ ) -> Option<Self> {
+ 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),
+ }
+ }
+}
@@ -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<Self>,
+ );
+}
+
+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<Box<dyn StatusItemViewHandle>>,
+ right_items: Vec<Box<dyn StatusItemViewHandle>>,
+ active_pane: ViewHandle<Pane>,
+ _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<Self>) -> AnyElement<Self> {
+ 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<Pane>, cx: &mut ViewContext<Self>) -> 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<T>(&mut self, item: ViewHandle<T>, cx: &mut ViewContext<Self>)
+ where
+ T: 'static + StatusItemView,
+ {
+ self.left_items.push(Box::new(item));
+ cx.notify();
+ }
+
+ pub fn item_of_type<T: StatusItemView>(&self) -> Option<ViewHandle<T>> {
+ self.left_items
+ .iter()
+ .chain(self.right_items.iter())
+ .find_map(|item| item.as_any().clone().downcast())
+ }
+
+ pub fn position_of_item<T>(&self) -> Option<usize>
+ 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<T>(
+ &mut self,
+ position: usize,
+ item: ViewHandle<T>,
+ cx: &mut ViewContext<Self>,
+ ) 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<Self>) {
+ 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<T>(&mut self, item: ViewHandle<T>, cx: &mut ViewContext<Self>)
+ where
+ T: 'static + StatusItemView,
+ {
+ self.right_items.push(Box::new(item));
+ cx.notify();
+ }
+
+ pub fn set_active_pane(&mut self, active_pane: &ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
+ 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<Self>) {
+ 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<T: StatusItemView> StatusItemViewHandle for ViewHandle<T> {
+ 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<StatusBar>,
+ right: AnyElement<StatusBar>,
+}
+
+impl Element<StatusBar> for StatusBarElement {
+ type LayoutState = ();
+ type PaintState = ();
+
+ fn layout(
+ &mut self,
+ mut constraint: SizeConstraint,
+ view: &mut StatusBar,
+ cx: &mut ViewContext<StatusBar>,
+ ) -> (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<StatusBar>,
+ ) -> 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<usize>,
+ _: RectF,
+ _: RectF,
+ _: &Self::LayoutState,
+ _: &Self::PaintState,
+ _: &StatusBar,
+ _: &ViewContext<StatusBar>,
+ ) -> Option<RectF> {
+ None
+ }
+
+ fn debug(
+ &self,
+ bounds: RectF,
+ _: &Self::LayoutState,
+ _: &Self::PaintState,
+ _: &StatusBar,
+ _: &ViewContext<StatusBar>,
+ ) -> serde_json::Value {
+ json!({
+ "type": "StatusBarElement",
+ "bounds": bounds.to_json()
+ })
+ }
+}
@@ -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<Self>,
+ ) -> 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<Self>) {}
+
+ /// 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<Self>) -> 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<Box<dyn ItemHandle>>,
+ hidden: bool,
+ can_navigate: bool,
+ items: Vec<(Box<dyn ToolbarItemViewHandle>, ToolbarItemLocation)>,
+}
+
+impl Entity for Toolbar {
+ type Event = ();
+}
+
+impl View for Toolbar {
+ fn ui_name() -> &'static str {
+ "Toolbar"
+ }
+
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+ 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<A: Action, F: 'static + Fn(&mut Toolbar, &mut ViewContext<Toolbar>)>(
+// svg_path: &'static str,
+// style: theme::Interactive<theme::IconButton>,
+// nav_button_height: f32,
+// tooltip_style: TooltipStyle,
+// enabled: bool,
+// spacing: f32,
+// on_click: F,
+// tooltip_action: A,
+// action_name: &'static str,
+// cx: &mut ViewContext<Toolbar>,
+// ) -> AnyElement<Toolbar> {
+// MouseEventHandler::new::<A, _>(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::<A>(
+// 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>) {
+ self.can_navigate = can_navigate;
+ cx.notify();
+ }
+
+ pub fn add_item<T>(&mut self, item: ViewHandle<T>, cx: &mut ViewContext<Self>)
+ 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>) {
+ 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<Self>) {
+ for (toolbar_item, _) in self.items.iter_mut() {
+ toolbar_item.focus_changed(focused, cx);
+ }
+ }
+
+ pub fn item_of_type<T: ToolbarItemView>(&self) -> Option<ViewHandle<T>> {
+ self.items
+ .iter()
+ .find_map(|(item, _)| item.as_any().clone().downcast())
+ }
+
+ pub fn hidden(&self) -> bool {
+ self.hidden
+ }
+}
+
+impl<T: ToolbarItemView> ToolbarItemViewHandle for ViewHandle<T> {
+ 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()
+ }
+}
@@ -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<Vector2F> = env::var("ZED_WINDOW_SIZE")
+// .ok()
+// .as_deref()
+// .and_then(parse_pixel_position_env_var);
+// static ref ZED_WINDOW_POSITION: Option<Vector2F> = 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<T: Modal> ModalHandle for ViewHandle<T> {
+// 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<PathBuf>,
+// }
+
+// #[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<SaveIntent>,
+// }
+
+// #[derive(Clone, PartialEq, Debug, Deserialize)]
+// #[serde(rename_all = "camelCase")]
+// pub struct Save {
+// pub save_intent: Option<SaveIntent>,
+// }
+
+// #[derive(Clone, PartialEq, Debug, Deserialize, Default)]
+// #[serde(rename_all = "camelCase")]
+// pub struct CloseAllItemsAndPanes {
+// pub save_intent: Option<SaveIntent>,
+// }
+
+// #[derive(Deserialize)]
+// pub struct Toast {
+// id: usize,
+// msg: Cow<'static, str>,
+// #[serde(skip)]
+// on_click: Option<(Cow<'static, str>, Arc<dyn Fn(&mut WindowContext)>)>,
+// }
+
+// impl Toast {
+// pub fn new<I: Into<Cow<'static, str>>>(id: usize, msg: I) -> Self {
+// Toast {
+// id,
+// msg: msg.into(),
+// on_click: None,
+// }
+// }
+
+// pub fn on_click<F, M>(mut self, message: M, on_click: F) -> Self
+// where
+// M: Into<Cow<'static, str>>,
+// 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::<WorkspaceSettings>(cx);
+// settings::register::<item::ItemSettings>(cx);
+// }
+
+// pub fn init(app_state: Arc<AppState>, 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<Workspace>| {
+// let pane = workspace.active_pane().clone();
+// workspace.unfollow(&pane, cx);
+// },
+// );
+// cx.add_action(
+// |workspace: &mut Workspace, action: &Save, cx: &mut ViewContext<Workspace>| {
+// 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>| {
+// 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<Project>, AnyModelHandle, &mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
+// >;
+// pub fn register_project_item<I: ProjectItem>(cx: &mut AppContext) {
+// cx.update_default_global(|builders: &mut ProjectItemBuilders, _| {
+// builders.insert(TypeId::of::<I::Item>(), |project, model, cx| {
+// let item = model.downcast::<I::Item>().unwrap();
+// Box::new(cx.add_view(|cx| I::for_project_item(project, item, cx)))
+// });
+// });
+// }
+
+// type FollowableItemBuilder = fn(
+// ViewHandle<Pane>,
+// ViewHandle<Workspace>,
+// ViewId,
+// &mut Option<proto::view::Variant>,
+// &mut AppContext,
+// ) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>;
+// type FollowableItemBuilders = HashMap<
+// TypeId,
+// (
+// FollowableItemBuilder,
+// fn(&AnyViewHandle) -> Box<dyn FollowableItemHandle>,
+// ),
+// >;
+// pub fn register_followable_item<I: FollowableItem>(cx: &mut AppContext) {
+// cx.update_default_global(|builders: &mut FollowableItemBuilders, _| {
+// builders.insert(
+// TypeId::of::<I>(),
+// (
+// |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::<I>().unwrap()),
+// ),
+// );
+// });
+// }
+
+// type ItemDeserializers = HashMap<
+// Arc<str>,
+// fn(
+// ModelHandle<Project>,
+// WeakViewHandle<Workspace>,
+// WorkspaceId,
+// ItemId,
+// &mut ViewContext<Pane>,
+// ) -> Task<Result<Box<dyn ItemHandle>>>,
+// >;
+// pub fn register_deserializable_item<I: 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<LanguageRegistry>,
+// pub client: Arc<Client>,
+// pub user_store: ModelHandle<UserStore>,
+// pub workspace_store: ModelHandle<WorkspaceStore>,
+// pub fs: Arc<dyn fs::Fs>,
+// pub build_window_options:
+// fn(Option<WindowBounds>, Option<uuid::Uuid>, &dyn Platform) -> WindowOptions<'static>,
+// pub initialize_workspace:
+// fn(WeakViewHandle<Workspace>, bool, Arc<AppState>, AsyncAppContext) -> Task<Result<()>>,
+// pub background_actions: BackgroundActions,
+// pub node_runtime: Arc<dyn NodeRuntime>,
+// }
+
+// pub struct WorkspaceStore {
+// workspaces: HashSet<WeakViewHandle<Workspace>>,
+// followers: Vec<Follower>,
+// client: Arc<Client>,
+// _subscriptions: Vec<client::Subscription>,
+// }
+
+// #[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
+// struct Follower {
+// project_id: Option<u64>,
+// peer_id: PeerId,
+// }
+
+// impl AppState {
+// #[cfg(any(test, feature = "test-support"))]
+// pub fn test(cx: &mut AppContext) -> Arc<Self> {
+// use node_runtime::FakeNodeRuntime;
+// use settings::SettingsStore;
+
+// if !cx.has_global::<SettingsStore>() {
+// 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<Task<()>>,
+// cancel_channel: Option<oneshot::Sender<()>>,
+// }
+
+// impl DelayedDebouncedEditAction {
+// fn new() -> DelayedDebouncedEditAction {
+// DelayedDebouncedEditAction {
+// task: None,
+// cancel_channel: None,
+// }
+// }
+
+// fn fire_new<F>(&mut self, delay: Duration, cx: &mut ViewContext<Workspace>, func: F)
+// where
+// F: 'static + FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> Task<Result<()>>,
+// {
+// 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<Pane>),
+// ContactRequestedJoin(u64),
+// }
+
+// pub struct Workspace {
+// weak_self: WeakViewHandle<Self>,
+// modal: Option<ActiveModal>,
+// zoomed: Option<AnyWeakViewHandle>,
+// zoomed_position: Option<DockPosition>,
+// center: PaneGroup,
+// left_dock: ViewHandle<Dock>,
+// bottom_dock: ViewHandle<Dock>,
+// right_dock: ViewHandle<Dock>,
+// panes: Vec<ViewHandle<Pane>>,
+// panes_by_item: HashMap<usize, WeakViewHandle<Pane>>,
+// active_pane: ViewHandle<Pane>,
+// last_active_center_pane: Option<WeakViewHandle<Pane>>,
+// last_active_view_id: Option<proto::ViewId>,
+// status_bar: ViewHandle<StatusBar>,
+// titlebar_item: Option<AnyViewHandle>,
+// notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
+// project: ModelHandle<Project>,
+// follower_states: HashMap<ViewHandle<Pane>, FollowerState>,
+// last_leaders_by_pane: HashMap<WeakViewHandle<Pane>, PeerId>,
+// window_edited: bool,
+// active_call: Option<(ModelHandle<ActiveCall>, Vec<Subscription>)>,
+// leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
+// database_id: WorkspaceId,
+// app_state: Arc<AppState>,
+// subscriptions: Vec<Subscription>,
+// _apply_leader_updates: Task<Result<()>>,
+// _observe_current_user: Task<Result<()>>,
+// _schedule_serialize: Option<Task<()>>,
+// pane_history_timestamp: Arc<AtomicUsize>,
+// }
+
+// struct ActiveModal {
+// view: Box<dyn ModalHandle>,
+// previously_focused_view_id: Option<usize>,
+// }
+
+// #[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<ViewId>,
+// items_by_leader_view_id: HashMap<ViewId, Box<dyn FollowableItemHandle>>,
+// }
+
+// enum WorkspaceBounds {}
+
+// impl Workspace {
+// pub fn new(
+// workspace_id: WorkspaceId,
+// project: ModelHandle<Project>,
+// app_state: Arc<AppState>,
+// cx: &mut ViewContext<Self>,
+// ) -> 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::<DragAndDrop<Workspace>, _, _>(|drag_and_drop, _| {
+// drag_and_drop.register_container(weak_handle.clone());
+// });
+
+// let mut active_call = None;
+// if cx.has_global::<ModelHandle<ActiveCall>>() {
+// let call = cx.global::<ModelHandle<ActiveCall>>().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<PathBuf>,
+// app_state: Arc<AppState>,
+// requesting_window: Option<WindowHandle<Workspace>>,
+// cx: &mut AppContext,
+// ) -> Task<(
+// WeakViewHandle<Workspace>,
+// Vec<Option<Result<Box<dyn ItemHandle>, 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<Arc<Path>> = Default::default();
+// let mut project_paths: Vec<(PathBuf, Option<ProjectPath>)> =
+// 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> {
+// self.weak_self.clone()
+// }
+
+// pub fn left_dock(&self) -> &ViewHandle<Dock> {
+// &self.left_dock
+// }
+
+// pub fn bottom_dock(&self) -> &ViewHandle<Dock> {
+// &self.bottom_dock
+// }
+
+// pub fn right_dock(&self) -> &ViewHandle<Dock> {
+// &self.right_dock
+// }
+
+// pub fn add_panel<T: Panel>(&mut self, panel: ViewHandle<T>, cx: &mut ViewContext<Self>)
+// where
+// T::Event: std::fmt::Debug,
+// {
+// self.add_panel_with_extra_event_handler(panel, cx, |_, _, _, _| {})
+// }
+
+// pub fn add_panel_with_extra_event_handler<T: Panel, F>(
+// &mut self,
+// panel: ViewHandle<T>,
+// cx: &mut ViewContext<Self>,
+// handler: F,
+// ) where
+// T::Event: std::fmt::Debug,
+// F: Fn(&mut Self, &ViewHandle<T>, &T::Event, &mut ViewContext<Self>) + '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<StatusBar> {
+// &self.status_bar
+// }
+
+// pub fn app_state(&self) -> &Arc<AppState> {
+// &self.app_state
+// }
+
+// pub fn user_store(&self) -> &ModelHandle<UserStore> {
+// &self.app_state.user_store
+// }
+
+// pub fn project(&self) -> &ModelHandle<Project> {
+// &self.project
+// }
+
+// pub fn recent_navigation_history(
+// &self,
+// limit: Option<usize>,
+// cx: &AppContext,
+// ) -> Vec<(ProjectPath, Option<PathBuf>)> {
+// let mut abs_paths_opened: HashMap<PathBuf, HashSet<ProjectPath>> = HashMap::default();
+// let mut history: HashMap<ProjectPath, (Option<PathBuf>, 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<Pane>,
+// mode: NavigationMode,
+// cx: &mut ViewContext<Workspace>,
+// ) -> Task<Result<()>> {
+// 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<Pane>,
+// cx: &mut ViewContext<Workspace>,
+// ) -> Task<Result<()>> {
+// self.navigate_history(pane, NavigationMode::GoingBack, cx)
+// }
+
+// pub fn go_forward(
+// &mut self,
+// pane: WeakViewHandle<Pane>,
+// cx: &mut ViewContext<Workspace>,
+// ) -> Task<Result<()>> {
+// self.navigate_history(pane, NavigationMode::GoingForward, cx)
+// }
+
+// pub fn reopen_closed_item(&mut self, cx: &mut ViewContext<Workspace>) -> Task<Result<()>> {
+// 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>) {
+// self.titlebar_item = Some(item);
+// cx.notify();
+// }
+
+// pub fn titlebar_item(&self) -> Option<AnyViewHandle> {
+// 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<T, F>(
+// &mut self,
+// cx: &mut ViewContext<Self>,
+// callback: F,
+// ) -> Task<Result<T>>
+// where
+// T: 'static,
+// F: 'static + FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> 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<Item = ModelHandle<Worktree>> {
+// self.project.read(cx).worktrees(cx)
+// }
+
+// pub fn visible_worktrees<'a>(
+// &self,
+// cx: &'a AppContext,
+// ) -> impl 'a + Iterator<Item = ModelHandle<Worktree>> {
+// self.project.read(cx).visible_worktrees(cx)
+// }
+
+// pub fn worktree_scans_complete(&self, cx: &AppContext) -> impl Future<Output = ()> + 'static {
+// let futures = self
+// .worktrees(cx)
+// .filter_map(|worktree| worktree.read(cx).as_local())
+// .map(|worktree| worktree.scan_complete())
+// .collect::<Vec<_>>();
+// 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<Self>,
+// ) -> Option<Task<Result<()>>> {
+// 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<Self>,
+// ) -> Task<Result<bool>> {
+// 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::<Workspace>())
+// .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<Self>,
+// ) -> Option<Task<Result<()>>> {
+// 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<Self>,
+// ) -> Task<Result<bool>> {
+// 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::<Vec<_>>();
+
+// 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<Self>) -> Option<Task<Result<()>>> {
+// 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<PathBuf>,
+// cx: &mut ViewContext<Self>,
+// ) -> Task<Result<()>> {
+// let window = cx.window().downcast::<Self>();
+// 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<PathBuf>,
+// visible: bool,
+// cx: &mut ViewContext<Self>,
+// ) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, 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<Self>) {
+// 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<Project>,
+// abs_path: &Path,
+// visible: bool,
+// cx: &mut AppContext,
+// ) -> Task<Result<(ModelHandle<Worktree>, 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<V, F>(
+// &mut self,
+// cx: &mut ViewContext<Self>,
+// add_view: F,
+// ) -> Option<ViewHandle<V>>
+// where
+// V: 'static + Modal,
+// F: FnOnce(&mut Self, &mut ViewContext<Self>) -> ViewHandle<V>,
+// {
+// 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::<V>())
+// {
+// 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<V: 'static + View>(&self) -> Option<ViewHandle<V>> {
+// self.modal
+// .as_ref()
+// .and_then(|modal| modal.view.as_any().clone().downcast::<V>())
+// }
+
+// pub fn dismiss_modal(&mut self, cx: &mut ViewContext<Self>) -> Option<AnyViewHandle> {
+// 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<Item = &Box<dyn ItemHandle>> {
+// self.panes.iter().flat_map(|pane| pane.read(cx).items())
+// }
+
+// pub fn item_of_type<T: Item>(&self, cx: &AppContext) -> Option<ViewHandle<T>> {
+// 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<Item = ViewHandle<T>> {
+// self.panes
+// .iter()
+// .flat_map(|pane| pane.read(cx).items_of_type())
+// }
+
+// pub fn active_item(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
+// self.active_pane().read(cx).active_item()
+// }
+
+// fn active_project_path(&self, cx: &ViewContext<Self>) -> Option<ProjectPath> {
+// self.active_item(cx).and_then(|item| item.project_path(cx))
+// }
+
+// pub fn save_active_item(
+// &mut self,
+// save_intent: SaveIntent,
+// cx: &mut ViewContext<Self>,
+// ) -> Task<Result<()>> {
+// 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<Self>,
+// ) -> Option<Task<Result<()>>> {
+// self.close_all_internal(true, SaveIntent::Close, cx)
+// }
+
+// pub fn close_all_items_and_panes(
+// &mut self,
+// action: &CloseAllItemsAndPanes,
+// cx: &mut ViewContext<Self>,
+// ) -> Option<Task<Result<()>>> {
+// 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<Self>,
+// ) -> Option<Task<Result<()>>> {
+// 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<Self>) {
+// 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<Self>) {
+// 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<T: Panel>(&mut self, cx: &mut ViewContext<Self>) -> Option<ViewHandle<T>> {
+// self.focus_or_unfocus_panel::<T>(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<T: Panel>(&mut self, cx: &mut ViewContext<Self>) {
+// self.focus_or_unfocus_panel::<T>(cx, |panel, cx| !panel.has_focus(cx));
+// }
+
+// /// Focus or unfocus the given panel type, depending on the given callback.
+// fn focus_or_unfocus_panel<T: Panel>(
+// &mut self,
+// cx: &mut ViewContext<Self>,
+// should_focus: impl Fn(&dyn PanelHandle, &mut ViewContext<Dock>) -> bool,
+// ) -> Option<Rc<dyn PanelHandle>> {
+// for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
+// if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
+// 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<T: Panel>(&self, cx: &WindowContext) -> Option<ViewHandle<T>> {
+// for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
+// let dock = dock.read(cx);
+// if let Some(panel) = dock.panel::<T>() {
+// return Some(panel);
+// }
+// }
+// None
+// }
+
+// fn zoom_out(&mut self, cx: &mut ViewContext<Self>) {
+// 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<AnyViewHandle> {
+// self.zoomed.and_then(|view| view.upgrade(cx))
+// }
+
+// fn dismiss_zoomed_items_to_reveal(
+// &mut self,
+// dock_to_reveal: Option<DockPosition>,
+// cx: &mut ViewContext<Self>,
+// ) {
+// // 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<Self>) -> ViewHandle<Pane> {
+// 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<dyn ItemHandle>,
+// cx: &mut ViewContext<Self>,
+// ) -> 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<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
+// 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<dyn ItemHandle>,
+// cx: &mut ViewContext<Self>,
+// ) {
+// 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<Self>,
+// ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
+// 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<Self>,
+// ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
+// 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<ProjectPath>,
+// pane: Option<WeakViewHandle<Pane>>,
+// focus_item: bool,
+// cx: &mut ViewContext<Self>,
+// ) -> Task<Result<Box<dyn ItemHandle>, 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<ProjectPath>,
+// cx: &mut ViewContext<Self>,
+// ) -> Task<Result<Box<dyn ItemHandle>, 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<Self>,
+// ) -> Task<
+// Result<(
+// ProjectEntryId,
+// impl 'static + FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
+// )>,
+// > {
+// 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::<ProjectItemBuilders>()
+// .get(&project_item.model_type())
+// .ok_or_else(|| anyhow!("no item builder for project item"))
+// .cloned()
+// })?;
+// let build_item =
+// move |cx: &mut ViewContext<Pane>| build_item(project, project_item, cx);
+// Ok((project_entry_id, build_item))
+// })
+// }
+
+// pub fn open_project_item<T>(
+// &mut self,
+// project_item: ModelHandle<T::Item>,
+// cx: &mut ViewContext<Self>,
+// ) -> ViewHandle<T>
+// 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<T>(
+// &mut self,
+// project_item: ModelHandle<T::Item>,
+// cx: &mut ViewContext<Self>,
+// ) -> ViewHandle<T>
+// 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<Self>) {
+// 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<Self>) -> 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<Self>) {
+// 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<Self>) {
+// 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<Self>) {
+// 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<Self>,
+// ) {
+// 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<Self>,
+// ) {
+// 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<Self>,
+// ) -> Option<&ViewHandle<Pane>> {
+// 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<Pane>, cx: &mut ViewContext<Self>) {
+// 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<Pane>,
+// event: &pane::Event,
+// cx: &mut ViewContext<Self>,
+// ) {
+// 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<Pane>,
+// split_direction: SplitDirection,
+// cx: &mut ViewContext<Self>,
+// ) -> ViewHandle<Pane> {
+// 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<Pane>,
+// direction: SplitDirection,
+// cx: &mut ViewContext<Self>,
+// ) -> Option<ViewHandle<Pane>> {
+// 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<Pane>,
+// split_direction: SplitDirection,
+// from: WeakViewHandle<Pane>,
+// item_id_to_move: usize,
+// cx: &mut ViewContext<Self>,
+// ) {
+// 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<Pane>,
+// split_direction: SplitDirection,
+// project_entry: ProjectEntryId,
+// cx: &mut ViewContext<Self>,
+// ) -> Option<Task<Result<()>>> {
+// 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<Pane>,
+// destination: ViewHandle<Pane>,
+// item_id_to_move: usize,
+// destination_index: usize,
+// cx: &mut ViewContext<Self>,
+// ) {
+// 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<Pane>, cx: &mut ViewContext<Self>) {
+// 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<Pane>] {
+// &self.panes
+// }
+
+// pub fn active_pane(&self) -> &ViewHandle<Pane> {
+// &self.active_pane
+// }
+
+// fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
+// 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<Self>,
+// ) -> Option<Task<Result<()>>> {
+// 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<Self>,
+// ) -> Option<Task<Result<()>>> {
+// 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<Self>,
+// ) -> Option<Task<Result<()>>> {
+// 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<Pane>,
+// cx: &mut ViewContext<Self>,
+// ) -> Option<PeerId> {
+// 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<Self>) -> AnyElement<Self> {
+// // 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::<TitleBar, _>(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<Self>) {
+// 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<Self>) {
+// 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<Self>) {
+// 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<Workspace>,
+// ) -> Option<AnyElement<Workspace>> {
+// if self.project.read(cx).is_read_only() {
+// enum DisconnectedOverlay {}
+// Some(
+// MouseEventHandler::new::<DisconnectedOverlay, _>(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<AnyElement<Workspace>> {
+// 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<u64>,
+// cx: &mut ViewContext<Self>,
+// ) -> 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>,
+// ) {
+// self.leader_updates_tx
+// .unbounded_send((leader_id, message))
+// .ok();
+// }
+
+// async fn process_leader_update(
+// this: &WeakViewHandle<Self>,
+// 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<Self>,
+// leader_id: PeerId,
+// panes: Vec<ViewHandle<Pane>>,
+// views: Vec<proto::View>,
+// cx: &mut AsyncAppContext,
+// ) -> Result<()> {
+// let this = this
+// .upgrade(cx)
+// .ok_or_else(|| anyhow!("workspace dropped"))?;
+
+// let item_builders = cx.update(|cx| {
+// cx.default_global::<FollowableItemBuilders>()
+// .values()
+// .map(|b| b.0)
+// .collect::<Vec<_>>()
+// });
+
+// 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<Pane>) -> Option<PeerId> {
+// self.follower_states.get(pane).map(|state| state.leader_id)
+// }
+
+// fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> 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<Pane>,
+// cx: &mut ViewContext<Self>,
+// ) -> Option<ViewHandle<SharedScreen>> {
+// 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::<SharedScreen>() {
+// 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<Self>) {
+// 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::<WorkspaceSettings>(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<ActiveCall>> {
+// self.active_call.as_ref().map(|(call, _)| call)
+// }
+
+// fn on_active_call_event(
+// &mut self,
+// _: ModelHandle<ActiveCall>,
+// event: &call::room::Event,
+// cx: &mut ViewContext<Self>,
+// ) {
+// 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<WorkspaceLocation> {
+// let project = self.project().read(cx);
+
+// if project.is_local() {
+// Some(
+// project
+// .visible_worktrees(cx)
+// .map(|worktree| worktree.read(cx).abs_path())
+// .collect::<Vec<_>>()
+// .into(),
+// )
+// } else {
+// None
+// }
+// }
+
+// fn remove_panes(&mut self, member: Member, cx: &mut ViewContext<Workspace>) {
+// 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<Pane>, cx: &mut ViewContext<Workspace>) {
+// 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>) {
+// 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<Self>) {
+// fn serialize_pane_handle(
+// pane_handle: &ViewHandle<Pane>,
+// 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::<Vec<_>>(),
+// 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::<Vec<_>>(),
+// 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<Workspace>) -> 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<Workspace>,
+// serialized_workspace: SerializedWorkspace,
+// paths_to_open: Vec<Option<ProjectPath>>,
+// cx: &mut AppContext,
+// ) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
+// 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::<HashMap<_, _>>()
+// });
+
+// 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::<Vec<_>>();
+
+// // 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<Project>, cx: &mut ViewContext<Self>) -> 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<AnyElement<Self>> {
+// 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<WindowBounds> {
+// 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<SerializedWorkspace>,
+// workspace: &WeakViewHandle<Workspace>,
+// mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
+// app_state: Arc<AppState>,
+// mut cx: AsyncAppContext,
+// ) -> Result<Vec<Option<Result<Box<dyn ItemHandle>>>>> {
+// 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::<HashSet<_>>()
+// });
+
+// 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<Workspace>, 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::<MessageNotification>(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::<MessageNotification>(MESSAGE_ID, cx)
+// })
+// .unwrap_or(false)
+// {
+// cx.update(|cx| {
+// cx.update_global::<NotificationTracker, _, _>(|tracker, _| {
+// let entry = tracker
+// .entry(TypeId::of::<MessageNotification>())
+// .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::<ThemeSettings>(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<Workspace>, 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<Self>) -> AnyElement<Self> {
+// 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::<ZoomBackground>(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::<ModalBackground, _>(
+// 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::<WorkspaceBounds>()
+// .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<Self>) {
+// if cx.is_self_focused() {
+// cx.focus(&self.active_pane);
+// }
+// }
+
+// fn modifiers_changed(&mut self, e: &ModifiersChangedEvent, cx: &mut ViewContext<Self>) -> bool {
+// DragAndDrop::<Workspace>::update_modifiers(e.modifiers, cx)
+// }
+// }
+
+// impl WorkspaceStore {
+// pub fn new(client: Arc<Client>, cx: &mut ModelContext<Self>) -> 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<u64>,
+// update: proto::update_followers::Variant,
+// cx: &AppContext,
+// ) -> Option<()> {
+// if !cx.has_global::<ModelHandle<ActiveCall>>() {
+// 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<Self>,
+// envelope: TypedEnvelope<proto::Follow>,
+// _: Arc<Client>,
+// mut cx: AsyncAppContext,
+// ) -> Result<proto::FollowResponse> {
+// 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<Self>,
+// envelope: TypedEnvelope<proto::Unfollow>,
+// _: Arc<Client>,
+// 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<Self>,
+// envelope: TypedEnvelope<proto::UpdateFollowers>,
+// _: Arc<Client>,
+// 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<Self> {
+// 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<ProjectPath>;
+// }
+
+// impl WorkspaceHandle for ViewHandle<Workspace> {
+// fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
+// 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::<Vec<_>>()
+// }
+// }
+
+// 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<Workspace>);
+
+// pub fn activate_workspace_for_project(
+// cx: &mut AsyncAppContext,
+// predicate: impl Fn(&mut Project, &mut ModelContext<Project>) -> bool,
+// ) -> Option<WeakViewHandle<Workspace>> {
+// for window in cx.windows() {
+// let handle = window
+// .update(cx, |cx| {
+// if let Some(workspace_handle) = cx.root_view().clone().downcast::<Workspace>() {
+// 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<WorkspaceLocation> {
+// DB.last_workspace().await.log_err().flatten()
+// }
+
+// async fn join_channel_internal(
+// channel_id: u64,
+// app_state: &Arc<AppState>,
+// requesting_window: Option<WindowHandle<Workspace>>,
+// active_call: &ModelHandle<ActiveCall>,
+// cx: &mut AsyncAppContext,
+// ) -> Result<bool> {
+// 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<AppState>,
+// requesting_window: Option<WindowHandle<Workspace>>,
+// cx: &mut AppContext,
+// ) -> Task<Result<()>> {
+// 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<AnyWindowHandle> {
+// for window in cx.windows() {
+// let found = window.update(cx, |cx| {
+// let is_workspace = cx.root_view().clone().downcast::<Workspace>().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<AppState>,
+// requesting_window: Option<WindowHandle<Workspace>>,
+// cx: &mut AppContext,
+// ) -> Task<
+// Result<(
+// WeakViewHandle<Workspace>,
+// Vec<Option<Result<Box<dyn ItemHandle>, 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<AppState>,
+// cx: &mut AppContext,
+// init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + '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<Workspace>,
+// default_content: impl 'static + Send + FnOnce() -> Rope,
+// ) -> Task<Result<Box<dyn ItemHandle>>> {
+// 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<AppState>,
+// cx: &mut AppContext,
+// ) -> Task<Result<()>> {
+// cx.spawn(|mut cx| async move {
+// let windows = cx.windows();
+// let existing_workspace = windows.into_iter().find_map(|window| {
+// window.downcast::<Workspace>().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::<WorkspaceSettings>(cx).confirm_quit;
+// cx.spawn(|mut cx| async move {
+// let mut workspace_windows = cx
+// .windows()
+// .into_iter()
+// .filter_map(|window| window.downcast::<Workspace>())
+// .collect::<Vec<_>>();
+
+// // 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<Vector2F> {
+// 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::<Vec<_>>();
+// 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<Deterministic>, 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::<WorkspaceSettings>(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::<WorkspaceSettings>(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::<WorkspaceSettings>(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::<WorkspaceSettings>(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::<TestPanel>(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::<TestPanel>(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::<TestPanel>(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::<TestPanel>(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);
+// });
+// }
+// }
@@ -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<f32>,
+ pub confirm_quit: Option<bool>,
+ pub show_call_status_icon: Option<bool>,
+ pub autosave: Option<AutosaveSetting>,
+}
+
+#[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<GitGutterSetting>,
+ pub gutter_debounce: Option<u64>,
+}
+
+#[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> {
+ Self::load_via_json_merge(default_value, user_values)
+ }
+}
@@ -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
@@ -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<CliRequest>, IpcSender<CliResponse>),
- _app_state: Arc<AppState>,
- mut _cx: AsyncAppContext,
+ (mut requests, responses): (mpsc::Receiver<CliRequest>, IpcSender<CliResponse>),
+ app_state: Arc<AppState>,
+ mut cx: AsyncAppContext,
) {
if let Some(request) = requests.next().await {
match request {
- CliRequest::Open { paths: _, wait: _ } => {
- // let mut caret_positions = HashMap::new();
-
- // 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();
-
- // 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::<Editor>() {
- // 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();
- // }
- // }
-
- // 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 => {}
- // }
- // }
-
- // 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);
-
- // 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();
- // }
- // }
-
- // responses
- // .send(CliResponse::Exit {
- // status: i32::from(errored),
- // })
- // .log_err();
+ 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 mut errored = false;
+
+ match cx
+ .update(|cx| workspace2::open_paths(&paths, &app_state, None, cx))
+ .await
+ {
+ Ok((workspace, items)) => {
+ let mut item_release_futures = Vec::new();
+
+ 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::<Editor>() {
+ // 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();
+ // }
+ }
+
+ 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 => {}
+ }
+ }
+
+ 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);
+
+ 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();
}
}
}