diff --git a/Cargo.lock b/Cargo.lock index 7545448720..db8e88cb1c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10796,6 +10796,44 @@ 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", + "ui2", + "util", + "uuid 1.4.1", +] + [[package]] name = "ws2_32-sys" version = "0.2.1" @@ -11111,6 +11149,7 @@ dependencies = [ "urlencoding", "util", "uuid 1.4.1", + "workspace2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index ed6e65d479..998ea081a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -99,7 +99,7 @@ members = [ "crates/semantic_index", "crates/vim", "crates/vcs_menu", - "crates/workspace", + "crates/workspace2", "crates/welcome", "crates/xtask", "crates/zed", diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index 38e64dcf1c..da4d59daed 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -822,10 +822,13 @@ impl Context for AppContext { let root_view = window.root_view.clone().unwrap(); let result = update(root_view, &mut WindowContext::new(cx, &mut window)); - cx.windows - .get_mut(handle.id) - .ok_or_else(|| anyhow!("window not found"))? - .replace(window); + + if !window.removed { + cx.windows + .get_mut(handle.id) + .ok_or_else(|| anyhow!("window not found"))? + .replace(window); + } Ok(result) }) diff --git a/crates/gpui2/src/platform/mac/window.rs b/crates/gpui2/src/platform/mac/window.rs index 52dcf31603..affeab57c7 100644 --- a/crates/gpui2/src/platform/mac/window.rs +++ b/crates/gpui2/src/platform/mac/window.rs @@ -678,10 +678,15 @@ impl MacWindow { impl Drop for MacWindow { fn drop(&mut self) { - let native_window = self.0.lock().native_window; - unsafe { - native_window.close(); - } + let this = self.0.lock(); + let window = this.native_window; + this.executor + .spawn(async move { + unsafe { + window.close(); + } + }) + .detach(); } } @@ -868,17 +873,25 @@ impl PlatformWindow for MacWindow { fn zoom(&self) { let this = self.0.lock(); let window = this.native_window; - unsafe { - window.zoom_(nil); - } + this.executor + .spawn(async move { + unsafe { + window.zoom_(nil); + } + }) + .detach(); } fn toggle_full_screen(&self) { let this = self.0.lock(); let window = this.native_window; - unsafe { - window.toggleFullScreen_(nil); - } + this.executor + .spawn(async move { + unsafe { + window.toggleFullScreen_(nil); + } + }) + .detach(); } fn on_input(&self, callback: Box bool>) { diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 37b4d64345..cebf546217 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -4,11 +4,11 @@ use crate::{ Entity, EntityId, EventEmitter, FileDropEvent, FocusEvent, FontId, GlobalElementId, GlyphId, Hsla, ImageData, InputEvent, IsZero, KeyListener, KeyMatch, KeyMatcher, Keystroke, LayoutId, Model, ModelContext, Modifiers, MonochromeSprite, MouseButton, MouseDownEvent, MouseMoveEvent, - MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformWindow, Point, PolychromeSprite, - PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams, RenderSvgParams, ScaledPixels, - SceneBuilder, Shadow, SharedString, Size, Style, SubscriberSet, Subscription, - TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, VisualContext, WeakView, - WindowBounds, WindowOptions, SUBPIXEL_VARIANTS, + MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformWindow, Point, + PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams, + RenderSvgParams, ScaledPixels, SceneBuilder, Shadow, SharedString, Size, Style, SubscriberSet, + Subscription, TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, VisualContext, + WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS, }; use anyhow::{anyhow, Result}; use collections::HashMap; @@ -28,6 +28,7 @@ use std::{ hash::{Hash, Hasher}, marker::PhantomData, mem, + rc::Rc, sync::{ atomic::{AtomicUsize, Ordering::SeqCst}, Arc, @@ -164,6 +165,7 @@ impl Drop for FocusHandle { // Holds the state for a specific window. pub struct Window { pub(crate) handle: AnyWindowHandle, + pub(crate) removed: bool, platform_window: Box, display_id: DisplayId, sprite_atlas: Arc, @@ -256,6 +258,7 @@ impl Window { Window { handle, + removed: false, platform_window, display_id, sprite_atlas, @@ -351,6 +354,11 @@ impl<'a> WindowContext<'a> { self.window.dirty = true; } + /// Close this window. + pub fn remove_window(&mut self) { + self.window.removed = true; + } + /// Obtain a new `FocusHandle`, which allows you to track and manipulate the keyboard focus /// for elements rendered within this window. pub fn focus_handle(&mut self) -> FocusHandle { @@ -579,6 +587,21 @@ impl<'a> WindowContext<'a> { self.window.bounds } + pub fn is_window_active(&self) -> bool { + self.window.active + } + + pub fn zoom_window(&self) { + self.window.platform_window.zoom(); + } + + pub fn display(&self) -> Option> { + self.platform + .displays() + .into_iter() + .find(|display| display.id() == self.window.display_id) + } + /// The scale factor of the display associated with the window. For example, it could /// return 2.0 for a "retina" display, indicating that each logical pixel should actually /// be rendered as two pixels on screen. diff --git a/crates/storybook2/src/storybook2.rs b/crates/storybook2/src/storybook2.rs index bd97ef1cab..4e2c439db0 100644 --- a/crates/storybook2/src/storybook2.rs +++ b/crates/storybook2/src/storybook2.rs @@ -82,8 +82,8 @@ fn main() { ..Default::default() }, move |cx| { - let theme_settings = ThemeSettings::get_global(cx); - cx.set_rem_size(theme_settings.ui_font_size); + let ui_font_size = ThemeSettings::get_global(cx).ui_font_size; + cx.set_rem_size(ui_font_size); cx.build_view(|cx| StoryWrapper::new(selector.story(cx))) }, diff --git a/crates/workspace2/Cargo.toml b/crates/workspace2/Cargo.toml new file mode 100644 index 0000000000..5072f2b8f9 --- /dev/null +++ b/crates/workspace2/Cargo.toml @@ -0,0 +1,66 @@ +[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" } +ui = { package = "ui2", path = "../ui2" } + +async-recursion = "1.0.0" +itertools = "0.10" +bincode = "1.2.1" +anyhow.workspace = true +futures.workspace = true +lazy_static.workspace = true +log.workspace = true +parking_lot.workspace = true +postage.workspace = true +schemars.workspace = true +serde.workspace = true +serde_derive.workspace = true +serde_json.workspace = true +smallvec.workspace = true +uuid.workspace = true + +[dev-dependencies] +call2 = { path = "../call2", features = ["test-support"] } +client2 = { path = "../client2", features = ["test-support"] } +gpui2 = { path = "../gpui2", features = ["test-support"] } +project2 = { path = "../project2", features = ["test-support"] } +settings2 = { path = "../settings2", features = ["test-support"] } +fs2 = { path = "../fs2", features = ["test-support"] } +db2 = { path = "../db2", features = ["test-support"] } + +indoc.workspace = true +env_logger.workspace = true diff --git a/crates/workspace2/src/dock.rs b/crates/workspace2/src/dock.rs new file mode 100644 index 0000000000..20a06d1658 --- /dev/null +++ b/crates/workspace2/src/dock.rs @@ -0,0 +1,750 @@ +use crate::{status_bar::StatusItemView, Axis, Workspace}; +use gpui2::{ + div, Action, AnyView, AppContext, Div, Entity, EntityId, EventEmitter, ParentElement, Render, + Subscription, View, ViewContext, WeakView, WindowContext, +}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; + +pub trait Panel: Render + EventEmitter { + fn persistent_name(&self) -> &'static str; + fn position(&self, cx: &WindowContext) -> DockPosition; + fn position_is_valid(&self, position: DockPosition) -> bool; + fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext); + fn size(&self, cx: &WindowContext) -> f32; + fn set_size(&mut self, size: Option, cx: &mut ViewContext); + fn icon_path(&self, cx: &WindowContext) -> Option<&'static str>; + fn icon_tooltip(&self) -> (String, Option>); + fn icon_label(&self, _: &WindowContext) -> Option { + None + } + fn should_change_position_on_event(_: &Self::Event) -> bool; + fn should_zoom_in_on_event(_: &Self::Event) -> bool { + false + } + fn should_zoom_out_on_event(_: &Self::Event) -> bool { + false + } + fn is_zoomed(&self, _cx: &WindowContext) -> bool { + false + } + fn set_zoomed(&mut self, _zoomed: bool, _cx: &mut ViewContext) {} + fn set_active(&mut self, _active: bool, _cx: &mut ViewContext) {} + fn should_activate_on_event(_: &Self::Event) -> bool { + false + } + fn should_close_on_event(_: &Self::Event) -> bool { + false + } + fn has_focus(&self, cx: &WindowContext) -> bool; + fn is_focus_event(_: &Self::Event) -> bool; +} + +pub trait PanelHandle: Send + Sync { + fn id(&self) -> EntityId; + fn persistent_name(&self, cx: &WindowContext) -> &'static str; + fn position(&self, cx: &WindowContext) -> DockPosition; + fn position_is_valid(&self, position: DockPosition, cx: &WindowContext) -> bool; + fn set_position(&self, position: DockPosition, cx: &mut WindowContext); + fn is_zoomed(&self, cx: &WindowContext) -> bool; + fn set_zoomed(&self, zoomed: bool, cx: &mut WindowContext); + fn set_active(&self, active: bool, cx: &mut WindowContext); + fn size(&self, cx: &WindowContext) -> f32; + fn set_size(&self, size: Option, cx: &mut WindowContext); + fn icon_path(&self, cx: &WindowContext) -> Option<&'static str>; + fn icon_tooltip(&self, cx: &WindowContext) -> (String, Option>); + fn icon_label(&self, cx: &WindowContext) -> Option; + fn has_focus(&self, cx: &WindowContext) -> bool; + fn to_any(&self) -> AnyView; +} + +impl PanelHandle for View +where + T: Panel, +{ + fn id(&self) -> EntityId { + self.entity_id() + } + + fn persistent_name(&self, cx: &WindowContext) -> &'static str { + self.read(cx).persistent_name() + } + + 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 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 size(&self, cx: &WindowContext) -> f32 { + self.read(cx).size(cx) + } + + fn set_size(&self, size: Option, cx: &mut WindowContext) { + self.update(cx, |this, cx| this.set_size(size, cx)) + } + + fn icon_path(&self, cx: &WindowContext) -> Option<&'static str> { + self.read(cx).icon_path(cx) + } + + fn icon_tooltip(&self, cx: &WindowContext) -> (String, Option>) { + self.read(cx).icon_tooltip() + } + + fn icon_label(&self, cx: &WindowContext) -> Option { + self.read(cx).icon_label(cx) + } + + fn has_focus(&self, cx: &WindowContext) -> bool { + self.read(cx).has_focus(cx) + } + + fn to_any(&self) -> AnyView { + self.clone().into() + } +} + +impl From<&dyn PanelHandle> for AnyView { + fn from(val: &dyn PanelHandle) -> Self { + val.to_any() + } +} + +pub struct Dock { + position: DockPosition, + panel_entries: Vec, + is_open: bool, + active_panel_index: usize, +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum DockPosition { + Left, + Bottom, + Right, +} + +impl DockPosition { + fn to_label(&self) -> &'static str { + match self { + Self::Left => "left", + Self::Bottom => "bottom", + Self::Right => "right", + } + } + + // todo!() + // 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: Arc, + // todo!() + // context_menu: View, + _subscriptions: [Subscription; 2], +} + +pub struct PanelButtons { + dock: View, + workspace: WeakView, +} + +impl Dock { + pub fn new(position: DockPosition) -> Self { + Self { + position, + panel_entries: Default::default(), + active_panel_index: 0, + is_open: false, + } + } + + pub fn position(&self) -> DockPosition { + self.position + } + + pub fn is_open(&self) -> bool { + self.is_open + } + + // pub fn has_focus(&self, cx: &WindowContext) -> bool { + // self.visible_panel() + // .map_or(false, |panel| panel.has_focus(cx)) + // } + + // pub fn panel(&self) -> Option> { + // self.panel_entries + // .iter() + // .find_map(|entry| entry.panel.as_any().clone().downcast()) + // } + + // pub fn panel_index_for_type(&self) -> Option { + // self.panel_entries + // .iter() + // .position(|entry| entry.panel.as_any().is::()) + // } + + pub fn panel_index_for_ui_name(&self, _ui_name: &str, _cx: &AppContext) -> Option { + todo!() + // self.panel_entries.iter().position(|entry| { + // let panel = entry.panel.as_any(); + // cx.view_ui_name(panel.window(), panel.id()) == Some(ui_name) + // }) + } + + // pub fn active_panel_index(&self) -> usize { + // self.active_panel_index + // } + + pub(crate) fn set_open(&mut self, open: bool, cx: &mut ViewContext) { + if open != self.is_open { + self.is_open = open; + if let Some(active_panel) = self.panel_entries.get(self.active_panel_index) { + active_panel.panel.set_active(open, cx); + } + + cx.notify(); + } + } + + // pub fn set_panel_zoomed(&mut self, panel: &AnyView, zoomed: bool, cx: &mut ViewContext) { + // for entry in &mut self.panel_entries { + // if entry.panel.as_any() == panel { + // if zoomed != entry.panel.is_zoomed(cx) { + // entry.panel.set_zoomed(zoomed, cx); + // } + // } else if entry.panel.is_zoomed(cx) { + // entry.panel.set_zoomed(false, cx); + // } + // } + + // cx.notify(); + // } + + // pub fn zoom_out(&mut self, cx: &mut ViewContext) { + // for entry in &mut self.panel_entries { + // if entry.panel.is_zoomed(cx) { + // entry.panel.set_zoomed(false, cx); + // } + // } + // } + + // pub(crate) fn add_panel(&mut self, panel: View, cx: &mut ViewContext) { + // let subscriptions = [ + // cx.observe(&panel, |_, _, cx| cx.notify()), + // cx.subscribe(&panel, |this, panel, event, cx| { + // if T::should_activate_on_event(event) { + // if let Some(ix) = this + // .panel_entries + // .iter() + // .position(|entry| entry.panel.id() == panel.id()) + // { + // this.set_open(true, cx); + // this.activate_panel(ix, cx); + // cx.focus(&panel); + // } + // } else if T::should_close_on_event(event) + // && this.visible_panel().map_or(false, |p| p.id() == panel.id()) + // { + // this.set_open(false, cx); + // } + // }), + // ]; + + // let dock_view_id = cx.view_id(); + // self.panel_entries.push(PanelEntry { + // panel: Arc::new(panel), + // // todo!() + // // context_menu: cx.add_view(|cx| { + // // let mut menu = ContextMenu::new(dock_view_id, cx); + // // menu.set_position_mode(OverlayPositionMode::Local); + // // menu + // // }), + // _subscriptions: subscriptions, + // }); + // cx.notify() + // } + + // pub fn remove_panel(&mut self, panel: &View, cx: &mut ViewContext) { + // if let Some(panel_ix) = self + // .panel_entries + // .iter() + // .position(|entry| entry.panel.id() == panel.id()) + // { + // if panel_ix == self.active_panel_index { + // self.active_panel_index = 0; + // self.set_open(false, cx); + // } else if panel_ix < self.active_panel_index { + // self.active_panel_index -= 1; + // } + // self.panel_entries.remove(panel_ix); + // cx.notify(); + // } + // } + + // pub fn panels_len(&self) -> usize { + // self.panel_entries.len() + // } + + pub fn activate_panel(&mut self, panel_ix: usize, cx: &mut ViewContext) { + if panel_ix != self.active_panel_index { + if let Some(active_panel) = self.panel_entries.get(self.active_panel_index) { + active_panel.panel.set_active(false, cx); + } + + self.active_panel_index = panel_ix; + if let Some(active_panel) = self.panel_entries.get(self.active_panel_index) { + active_panel.panel.set_active(true, cx); + } + + cx.notify(); + } + } + + pub fn visible_panel(&self) -> Option<&Arc> { + let entry = self.visible_entry()?; + Some(&entry.panel) + } + + pub fn active_panel(&self) -> Option<&Arc> { + Some(&self.panel_entries.get(self.active_panel_index)?.panel) + } + + fn visible_entry(&self) -> Option<&PanelEntry> { + if self.is_open { + self.panel_entries.get(self.active_panel_index) + } else { + None + } + } + + // pub fn zoomed_panel(&self, cx: &WindowContext) -> Option> { + // let entry = self.visible_entry()?; + // if entry.panel.is_zoomed(cx) { + // Some(entry.panel.clone()) + // } else { + // None + // } + // } + + // pub fn panel_size(&self, panel: &dyn PanelHandle, cx: &WindowContext) -> Option { + // self.panel_entries + // .iter() + // .find(|entry| entry.panel.id() == panel.id()) + // .map(|entry| entry.panel.size(cx)) + // } + + // pub fn active_panel_size(&self, cx: &WindowContext) -> Option { + // if self.is_open { + // self.panel_entries + // .get(self.active_panel_index) + // .map(|entry| entry.panel.size(cx)) + // } else { + // None + // } + // } + + // pub fn resize_active_panel(&mut self, size: Option, cx: &mut ViewContext) { + // if let Some(entry) = self.panel_entries.get_mut(self.active_panel_index) { + // entry.panel.set_size(size, cx); + // cx.notify(); + // } + // } + + // pub fn render_placeholder(&self, cx: &WindowContext) -> AnyElement { + // todo!() + // if let Some(active_entry) = self.visible_entry() { + // Empty::new() + // .into_any() + // .contained() + // .with_style(self.style(cx)) + // .resizable::( + // self.position.to_resize_handle_side(), + // active_entry.panel.size(cx), + // |_, _, _| {}, + // ) + // .into_any() + // } else { + // Empty::new().into_any() + // } + // } +} + +// todo!() +// impl View for Dock { +// fn ui_name() -> &'static str { +// "Dock" +// } + +// fn render(&mut self, cx: &mut ViewContext) -> AnyElement { +// if let Some(active_entry) = self.visible_entry() { +// let style = self.style(cx); +// ChildView::new(active_entry.panel.as_any(), cx) +// .contained() +// .with_style(style) +// .resizable::( +// self.position.to_resize_handle_side(), +// active_entry.panel.size(cx), +// |dock: &mut Self, size, cx| dock.resize_active_panel(size, cx), +// ) +// .into_any() +// } else { +// Empty::new().into_any() +// } +// } + +// fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { +// if cx.is_self_focused() { +// if let Some(active_entry) = self.visible_entry() { +// cx.focus(active_entry.panel.as_any()); +// } else { +// cx.focus_parent(); +// } +// } +// } +// } + +impl PanelButtons { + pub fn new( + dock: View, + workspace: WeakView, + cx: &mut ViewContext, + ) -> Self { + cx.observe(&dock, |_, _, cx| cx.notify()).detach(); + Self { dock, workspace } + } +} + +impl EventEmitter for PanelButtons { + type Event = (); +} + +// impl Render for PanelButtons { +// type Element = (); + +// fn render(&mut self, cx: &mut ViewContext) -> Self::Element { +// todo!("") +// } + +// fn ui_name() -> &'static str { +// "PanelButtons" +// } + +// fn render(&mut self, cx: &mut ViewContext) -> AnyElement { +// let theme = &settings::get::(cx).theme; +// let tooltip_style = theme.tooltip.clone(); +// let theme = &theme.workspace.status_bar.panel_buttons; +// let button_style = theme.button.clone(); +// let dock = self.dock.read(cx); +// let active_ix = dock.active_panel_index; +// let is_open = dock.is_open; +// let dock_position = dock.position; +// let group_style = match dock_position { +// DockPosition::Left => theme.group_left, +// DockPosition::Bottom => theme.group_bottom, +// DockPosition::Right => theme.group_right, +// }; +// let menu_corner = match dock_position { +// DockPosition::Left => AnchorCorner::BottomLeft, +// DockPosition::Bottom | DockPosition::Right => AnchorCorner::BottomRight, +// }; + +// let panels = dock +// .panel_entries +// .iter() +// .map(|item| (item.panel.clone(), item.context_menu.clone())) +// .collect::>(); +// Flex::row() +// .with_children(panels.into_iter().enumerate().filter_map( +// |(panel_ix, (view, context_menu))| { +// let icon_path = view.icon_path(cx)?; +// let is_active = is_open && panel_ix == active_ix; +// let (tooltip, tooltip_action) = if is_active { +// ( +// format!("Close {} dock", dock_position.to_label()), +// Some(match dock_position { +// DockPosition::Left => crate::ToggleLeftDock.boxed_clone(), +// DockPosition::Bottom => crate::ToggleBottomDock.boxed_clone(), +// DockPosition::Right => crate::ToggleRightDock.boxed_clone(), +// }), +// ) +// } else { +// view.icon_tooltip(cx) +// }; +// Some( +// Stack::new() +// .with_child( +// MouseEventHandler::new::(panel_ix, cx, |state, cx| { +// let style = button_style.in_state(is_active); + +// let style = style.style_for(state); +// Flex::row() +// .with_child( +// Svg::new(icon_path) +// .with_color(style.icon_color) +// .constrained() +// .with_width(style.icon_size) +// .aligned(), +// ) +// .with_children(if let Some(label) = view.icon_label(cx) { +// Some( +// Label::new(label, style.label.text.clone()) +// .contained() +// .with_style(style.label.container) +// .aligned(), +// ) +// } else { +// None +// }) +// .constrained() +// .with_height(style.icon_size) +// .contained() +// .with_style(style.container) +// }) +// .with_cursor_style(CursorStyle::PointingHand) +// .on_click(MouseButton::Left, { +// let tooltip_action = +// tooltip_action.as_ref().map(|action| action.boxed_clone()); +// move |_, this, cx| { +// if let Some(tooltip_action) = &tooltip_action { +// let window = cx.window(); +// let view_id = this.workspace.id(); +// let tooltip_action = tooltip_action.boxed_clone(); +// cx.spawn(|_, mut cx| async move { +// window.dispatch_action( +// view_id, +// &*tooltip_action, +// &mut cx, +// ); +// }) +// .detach(); +// } +// } +// }) +// .on_click(MouseButton::Right, { +// let view = view.clone(); +// let menu = context_menu.clone(); +// move |_, _, cx| { +// const POSITIONS: [DockPosition; 3] = [ +// DockPosition::Left, +// DockPosition::Right, +// DockPosition::Bottom, +// ]; + +// menu.update(cx, |menu, cx| { +// let items = POSITIONS +// .into_iter() +// .filter(|position| { +// *position != dock_position +// && view.position_is_valid(*position, cx) +// }) +// .map(|position| { +// let view = view.clone(); +// ContextMenuItem::handler( +// format!("Dock {}", position.to_label()), +// move |cx| view.set_position(position, cx), +// ) +// }) +// .collect(); +// menu.show(Default::default(), menu_corner, items, cx); +// }) +// } +// }) +// .with_tooltip::( +// panel_ix, +// tooltip, +// tooltip_action, +// tooltip_style.clone(), +// cx, +// ), +// ) +// .with_child(ChildView::new(&context_menu, cx)), +// ) +// }, +// )) +// .contained() +// .with_style(group_style) +// .into_any() +// } +// } + +impl Render for PanelButtons { + type Element = Div; + + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + // todo!() + let dock = self.dock.read(cx); + div().children( + dock.panel_entries + .iter() + .map(|panel| panel.panel.persistent_name(cx)), + ) + } +} + +impl StatusItemView for PanelButtons { + fn set_active_pane_item( + &mut self, + _active_pane_item: Option<&dyn crate::ItemHandle>, + _cx: &mut ViewContext, + ) { + todo!() + } +} + +#[cfg(any(test, feature = "test-support"))] +pub mod test { + use super::*; + use gpui2::{div, Div, 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 EventEmitter for TestPanel { + type Event = TestPanelEvent; + } + + impl TestPanel { + pub fn new(position: DockPosition) -> Self { + Self { + position, + zoomed: false, + active: false, + has_focus: false, + size: 300., + } + } + } + + impl Render for TestPanel { + type Element = Div; + + fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { + div() + } + } + + impl Panel for TestPanel { + fn persistent_name(&self) -> &'static str { + "TestPanel" + } + + fn position(&self, _: &gpui2::WindowContext) -> super::DockPosition { + self.position + } + + fn position_is_valid(&self, _: super::DockPosition) -> bool { + true + } + + fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext) { + self.position = position; + cx.emit(TestPanelEvent::PositionChanged); + } + + fn size(&self, _: &WindowContext) -> f32 { + self.size + } + + fn set_size(&mut self, size: Option, _: &mut ViewContext) { + self.size = size.unwrap_or(300.); + } + + fn icon_path(&self, _: &WindowContext) -> Option<&'static str> { + Some("icons/test_panel.svg") + } + + fn icon_tooltip(&self) -> (String, Option>) { + ("Test Panel".into(), None) + } + + fn should_change_position_on_event(event: &Self::Event) -> bool { + matches!(event, TestPanelEvent::PositionChanged) + } + + fn should_zoom_in_on_event(event: &Self::Event) -> bool { + matches!(event, TestPanelEvent::ZoomIn) + } + + fn should_zoom_out_on_event(event: &Self::Event) -> bool { + matches!(event, TestPanelEvent::ZoomOut) + } + + fn is_zoomed(&self, _: &WindowContext) -> bool { + self.zoomed + } + + fn set_zoomed(&mut self, zoomed: bool, _cx: &mut ViewContext) { + self.zoomed = zoomed; + } + + fn set_active(&mut self, active: bool, _cx: &mut ViewContext) { + self.active = active; + } + + fn should_activate_on_event(event: &Self::Event) -> bool { + matches!(event, TestPanelEvent::Activated) + } + + fn should_close_on_event(event: &Self::Event) -> bool { + matches!(event, TestPanelEvent::Closed) + } + + fn has_focus(&self, _cx: &WindowContext) -> bool { + self.has_focus + } + + fn is_focus_event(event: &Self::Event) -> bool { + matches!(event, TestPanelEvent::Focus) + } + } +} diff --git a/crates/workspace2/src/item.rs b/crates/workspace2/src/item.rs index 90d08d6c4a..c2d5c25781 100644 --- a/crates/workspace2/src/item.rs +++ b/crates/workspace2/src/item.rs @@ -1,88 +1,80 @@ -// use crate::{ -// pane, persistence::model::ItemId, searchable::SearchableItemHandle, FollowableItemBuilders, -// ItemNavHistory, Pane, ToolbarItemLocation, ViewId, Workspace, WorkspaceId, -// }; -// use crate::{AutosaveSetting, DelayedDebouncedEditAction, WorkspaceSettings}; +use crate::{ + pane::{self, Pane}, + persistence::model::ItemId, + searchable::SearchableItemHandle, + workspace_settings::{AutosaveSetting, WorkspaceSettings}, + DelayedDebouncedEditAction, FollowableItemBuilders, ItemNavHistory, ToolbarItemLocation, + ViewId, Workspace, WorkspaceId, +}; use anyhow::Result; use client2::{ - proto::{self, PeerId, ViewId}, + proto::{self, PeerId}, Client, }; +use gpui2::{ + AnyElement, AnyView, AppContext, Entity, EntityId, EventEmitter, HighlightStyle, Model, Pixels, + Point, Render, SharedString, Task, View, ViewContext, WeakView, WindowContext, +}; +use parking_lot::Mutex; +use project2::{Project, ProjectEntryId, ProjectPath}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; use settings2::Settings; -use theme2::Theme; -// use client2::{ -// proto::{self, PeerId}, -// Client, -// }; -// use gpui2::geometry::vector::Vector2F; -// use gpui2::AnyWindowHandle; -// use gpui2::{ -// fonts::HighlightStyle, AnyElement, AnyViewHandle, AppContext, Handle, Task, View, -// ViewContext, View, 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; +use smallvec::SmallVec; +use std::{ + any::{Any, TypeId}, + ops::Range, + path::PathBuf, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, + time::Duration, +}; +use theme2::ThemeVariant; -// #[derive(Deserialize)] -// pub struct ItemSettings { -// pub git_status: bool, -// pub close_position: ClosePosition, -// } +#[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, -// } +#[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, -// } -// } -// } +impl ClosePosition { + pub fn right(&self) -> bool { + match self { + ClosePosition::Left => false, + ClosePosition::Right => true, + } + } +} -// #[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] -// pub struct ItemSettingsContent { -// git_status: Option, -// close_position: Option, -// } +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] +pub struct ItemSettingsContent { + git_status: Option, + close_position: Option, +} -// impl Setting for ItemSettings { -// const KEY: Option<&'static str> = Some("tabs"); +impl Settings for ItemSettings { + const KEY: Option<&'static str> = Some("tabs"); -// type FileContent = ItemSettingsContent; + type FileContent = ItemSettingsContent; -// fn load( -// default_value: &Self::FileContent, -// user_values: &[&Self::FileContent], -// _: &gpui2::AppContext, -// ) -> anyhow::Result { -// Self::load_via_json_merge(default_value, user_values) -// } -// } + fn load( + default_value: &Self::FileContent, + user_values: &[&Self::FileContent], + _: &mut AppContext, + ) -> Result { + Self::load_via_json_merge(default_value, user_values) + } +} #[derive(Eq, PartialEq, Hash, Debug)] pub enum ItemEvent { @@ -98,12 +90,12 @@ pub struct BreadcrumbText { pub highlights: Option, HighlightStyle)>>, } -pub trait Item: EventEmitter + Sized { - // fn deactivated(&mut self, _: &mut ViewContext) {} - // fn workspace_deactivated(&mut self, _: &mut ViewContext) {} - // fn navigate(&mut self, _: Box, _: &mut ViewContext) -> bool { - // false - // } +pub trait Item: Render + EventEmitter + Send { + fn deactivated(&mut self, _: &mut ViewContext) {} + fn workspace_deactivated(&mut self, _: &mut ViewContext) {} + fn navigate(&mut self, _: Box, _: &mut ViewContext) -> bool { + false + } fn tab_tooltip_text(&self, _: &AppContext) -> Option { None } @@ -117,132 +109,104 @@ pub trait Item: EventEmitter + Sized { fn is_singleton(&self, _cx: &AppContext) -> bool { false } - // fn set_nav_history(&mut self, _: ItemNavHistory, _: &mut ViewContext) {} - fn clone_on_split(&self, _workspace_id: WorkspaceId, _: &mut ViewContext) -> Option + fn set_nav_history(&mut self, _: ItemNavHistory, _: &mut ViewContext) {} + fn clone_on_split( + &self, + _workspace_id: WorkspaceId, + _: &mut ViewContext, + ) -> Option> where Self: Sized, { None } - // fn is_dirty(&self, _: &AppContext) -> bool { - // false - // } - // fn has_conflict(&self, _: &AppContext) -> bool { - // false - // } - // fn can_save(&self, _cx: &AppContext) -> bool { - // false - // } - // fn save( - // &mut self, - // _project: Handle, - // _cx: &mut ViewContext, - // ) -> Task> { - // unimplemented!("save() must be implemented if can_save() returns true") - // } - // fn save_as( - // &mut self, - // _project: Handle, - // _abs_path: PathBuf, - // _cx: &mut ViewContext, - // ) -> Task> { - // unimplemented!("save_as() must be implemented if can_save() returns true") - // } - // fn reload( - // &mut self, - // _project: Handle, - // _cx: &mut ViewContext, - // ) -> Task> { - // unimplemented!("reload() must be implemented if can_save() returns true") - // } + 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: Model, _cx: &mut ViewContext) -> Task> { + unimplemented!("save() must be implemented if can_save() returns true") + } + fn save_as( + &mut self, + _project: Model, + _abs_path: PathBuf, + _cx: &mut ViewContext, + ) -> Task> { + unimplemented!("save_as() must be implemented if can_save() returns true") + } + fn reload( + &mut self, + _project: Model, + _cx: &mut ViewContext, + ) -> Task> { + unimplemented!("reload() must be implemented if can_save() returns true") + } fn to_item_events(_event: &Self::Event) -> SmallVec<[ItemEvent; 2]> { SmallVec::new() } - // fn should_close_item_on_event(_: &Self::Event) -> bool { - // false - // } - // fn should_update_tab_on_event(_: &Self::Event) -> bool { - // false - // } + fn 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 View, - // _: &'a AppContext, - // ) -> Option<&AnyViewHandle> { - // if TypeId::of::() == type_id { - // Some(self_handle) - // } else { - // None - // } - // } + fn act_as_type<'a>( + &'a self, + type_id: TypeId, + self_handle: &'a View, + _: &'a AppContext, + ) -> Option { + if TypeId::of::() == type_id { + Some(self_handle.clone().into()) + } else { + None + } + } - // fn as_searchable(&self, _: &View) -> Option> { - // None - // } + fn as_searchable(&self, _: &View) -> Option> { + None + } - // fn breadcrumb_location(&self) -> ToolbarItemLocation { - // ToolbarItemLocation::Hidden - // } + fn breadcrumb_location(&self) -> ToolbarItemLocation { + ToolbarItemLocation::Hidden + } - // fn breadcrumbs(&self, _theme: &Theme, _cx: &AppContext) -> Option> { - // None - // } + fn breadcrumbs(&self, _theme: &ThemeVariant, _cx: &AppContext) -> Option> { + None + } - // fn added_to_workspace(&mut self, _workspace: &mut Workspace, _cx: &mut ViewContext) {} + fn added_to_workspace(&mut self, _workspace: &mut Workspace, _cx: &mut ViewContext) {} - // fn serialized_item_kind() -> Option<&'static str> { - // None - // } + fn serialized_item_kind() -> Option<&'static str> { + None + } - // fn deserialize( - // _project: Handle, - // _workspace: WeakViewHandle, - // _workspace_id: WorkspaceId, - // _item_id: ItemId, - // _cx: &mut ViewContext, - // ) -> Task>> { - // unimplemented!( - // "deserialize() must be implemented if serialized_item_kind() returns Some(_)" - // ) - // } - // fn show_toolbar(&self) -> bool { - // true - // } - // fn pixel_position_of_cursor(&self, _: &AppContext) -> Option { - // None - // } + fn deserialize( + _project: Model, + _workspace: WeakView, + _workspace_id: WorkspaceId, + _item_id: ItemId, + _cx: &mut ViewContext, + ) -> Task>> { + unimplemented!( + "deserialize() must be implemented if serialized_item_kind() returns Some(_)" + ) + } + fn show_toolbar(&self) -> bool { + true + } + fn pixel_position_of_cursor(&self, _: &AppContext) -> Option> { + None + } } -use std::{ - any::Any, - cell::RefCell, - ops::Range, - path::PathBuf, - rc::Rc, - sync::{ - atomic::{AtomicBool, Ordering}, - Arc, - }, - time::Duration, -}; - -use gpui2::{ - AnyElement, AnyWindowHandle, AppContext, EventEmitter, Handle, HighlightStyle, Pixels, Point, - SharedString, Task, View, ViewContext, VisualContext, WindowContext, -}; -use project2::{Project, ProjectEntryId, ProjectPath}; -use smallvec::SmallVec; - -use crate::{ - pane::{self, Pane}, - searchable::SearchableItemHandle, - workspace_settings::{AutosaveSetting, WorkspaceSettings}, - DelayedDebouncedEditAction, FollowableItemBuilders, ToolbarItemLocation, Workspace, - WorkspaceId, -}; - pub trait ItemHandle: 'static + Send { fn subscribe_to_item_events( &self, @@ -273,52 +237,49 @@ pub trait ItemHandle: 'static + Send { fn deactivated(&self, cx: &mut WindowContext); fn workspace_deactivated(&self, cx: &mut WindowContext); fn navigate(&self, data: Box, cx: &mut WindowContext) -> bool; - fn id(&self) -> usize; - fn window(&self) -> AnyWindowHandle; - // fn as_any(&self) -> &AnyView; todo!() + fn id(&self) -> EntityId; + fn to_any(&self) -> AnyView; 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: Handle, cx: &mut WindowContext) -> Task>; + fn save(&self, project: Model, cx: &mut WindowContext) -> Task>; fn save_as( &self, - project: Handle, + project: Model, abs_path: PathBuf, cx: &mut WindowContext, ) -> Task>; - fn reload(&self, project: Handle, cx: &mut WindowContext) -> Task>; - // fn act_as_type<'a>(&'a self, type_id: TypeId, cx: &'a AppContext) -> Option<&'a AnyViewHandle>; todo!() + fn reload(&self, project: Model, cx: &mut WindowContext) -> Task>; + fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option; fn to_followable_item_handle(&self, cx: &AppContext) -> Option>; fn on_release( - &self, + &mut self, cx: &mut AppContext, - callback: Box, + callback: Box, ) -> gpui2::Subscription; fn to_searchable_item_handle(&self, cx: &AppContext) -> Option>; fn breadcrumb_location(&self, cx: &AppContext) -> ToolbarItemLocation; - fn breadcrumbs(&self, theme: &Theme, cx: &AppContext) -> Option>; + fn breadcrumbs(&self, theme: &ThemeVariant, cx: &AppContext) -> Option>; fn serialized_item_kind(&self) -> Option<&'static str>; fn show_toolbar(&self, cx: &AppContext) -> bool; fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option>; } -pub trait WeakItemHandle { - fn id(&self) -> usize; - fn window(&self) -> AnyWindowHandle; - fn upgrade(&self, cx: &AppContext) -> Option>; +pub trait WeakItemHandle: Send + Sync { + fn id(&self) -> EntityId; + fn upgrade(&self) -> Option>; } -// todo!() -// impl dyn ItemHandle { -// pub fn downcast(&self) -> Option> { -// self.as_any().clone().downcast() -// } +impl dyn ItemHandle { + pub fn downcast(&self) -> Option> { + self.to_any().downcast().ok() + } -// pub fn act_as(&self, cx: &AppContext) -> Option> { -// self.act_as_type(TypeId::of::(), cx) -// .and_then(|t| t.clone().downcast()) -// } -// } + pub fn act_as(&self, cx: &AppContext) -> Option> { + self.act_as_type(TypeId::of::(), cx) + .and_then(|t| t.downcast().ok()) + } +} impl ItemHandle for View { fn subscribe_to_item_events( @@ -399,10 +360,8 @@ impl ItemHandle for View { workspace_id: WorkspaceId, cx: &mut WindowContext, ) -> Option> { - self.update(cx, |item, cx| { - cx.add_option_view(|cx| item.clone_on_split(workspace_id, cx)) - }) - .map(|handle| Box::new(handle) as Box) + self.update(cx, |item, cx| item.clone_on_split(workspace_id, cx)) + .map(|handle| Box::new(handle) as Box) } fn added_to_pane( @@ -439,109 +398,107 @@ impl ItemHandle for View { .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 pending_update = Arc::new(Mutex::new(None)); + let pending_update_scheduled = Arc::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)) + let 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()) + { + 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.lock(), cx) + && !pending_update_scheduled.load(Ordering::SeqCst) { - pane - } else { - log::error!("unexpected item event after pane was dropped"); - return; - }; + pending_update_scheduled.store(true, Ordering::SeqCst); + todo!("replace with on_next_frame?"); + // 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, + // ); + // } + // }); + } + } - 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); + 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; } - 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, - ); - } + ItemEvent::UpdateTab => { + pane.update(cx, |_, cx| { + cx.emit(pane::Event::ChangeItemTitle); + cx.notify(); }); } - } - 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 = WorkspaceSettings::get_global(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) }); } - - ItemEvent::Edit => { - let autosave = WorkspaceSettings::get_global(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 - && WorkspaceSettings::get_global(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(); + todo!("observe focus"); + // cx.observe_focus(self, move |workspace, item, focused, cx| { + // if !focused + // && WorkspaceSettings::get_global(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| { @@ -561,20 +518,14 @@ impl ItemHandle for View { self.update(cx, |this, cx| this.navigate(data, cx)) } - fn id(&self) -> usize { - self.id() + fn id(&self) -> EntityId { + self.entity_id() } - fn window(&self) -> AnyWindowHandle { - todo!() - // AnyViewHandle::window(self) + fn to_any(&self) -> AnyView { + self.clone().into() } - // todo!() - // fn as_any(&self) -> &AnyViewHandle { - // self - // } - fn is_dirty(&self, cx: &AppContext) -> bool { self.read(cx).is_dirty(cx) } @@ -587,42 +538,41 @@ impl ItemHandle for View { self.read(cx).can_save(cx) } - fn save(&self, project: Handle, cx: &mut WindowContext) -> Task> { + fn save(&self, project: Model, cx: &mut WindowContext) -> Task> { self.update(cx, |item, cx| item.save(project, cx)) } fn save_as( &self, - project: Handle, + project: Model, abs_path: PathBuf, cx: &mut WindowContext, ) -> Task> { self.update(cx, |item, cx| item.save_as(project, abs_path, cx)) } - fn reload(&self, project: Handle, cx: &mut WindowContext) -> Task> { + fn reload(&self, project: Model, cx: &mut WindowContext) -> Task> { self.update(cx, |item, cx| item.reload(project, cx)) } - // todo!() - // 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 act_as_type<'a>(&'a self, type_id: TypeId, cx: &'a AppContext) -> Option { + self.read(cx).act_as_type(type_id, self, cx) + } fn to_followable_item_handle(&self, cx: &AppContext) -> Option> { if cx.has_global::() { let builders = cx.global::(); - let item = self.as_any(); - Some(builders.get(&item.view_type())?.1(item)) + let item = self.to_any(); + Some(builders.get(&item.entity_type())?.1(&item)) } else { None } } fn on_release( - &self, + &mut self, cx: &mut AppContext, - callback: Box, + callback: Box, ) -> gpui2::Subscription { cx.observe_release(self, move |_, cx| callback(cx)) } @@ -635,7 +585,7 @@ impl ItemHandle for View { self.read(cx).breadcrumb_location() } - fn breadcrumbs(&self, theme: &Theme, cx: &AppContext) -> Option> { + fn breadcrumbs(&self, theme: &ThemeVariant, cx: &AppContext) -> Option> { self.read(cx).breadcrumbs(theme, cx) } @@ -652,17 +602,17 @@ impl ItemHandle for View { } } -// impl From> for AnyViewHandle { -// fn from(val: Box) -> Self { -// val.as_any().clone() -// } -// } +impl From> for AnyView { + fn from(val: Box) -> Self { + val.to_any() + } +} -// impl From<&Box> for AnyViewHandle { -// fn from(val: &Box) -> Self { -// val.as_any().clone() -// } -// } +impl From<&Box> for AnyView { + fn from(val: &Box) -> Self { + val.to_any() + } +} impl Clone for Box { fn clone(&self) -> Box { @@ -670,26 +620,22 @@ impl Clone for Box { } } -// impl WeakItemHandle for WeakViewHandle { -// fn id(&self) -> usize { -// self.id() -// } +impl WeakItemHandle for WeakView { + fn id(&self) -> EntityId { + self.entity_id() + } -// fn window(&self) -> AnyWindowHandle { -// self.window() -// } - -// fn upgrade(&self, cx: &AppContext) -> Option> { -// self.upgrade(cx).map(|v| Box::new(v) as Box) -// } -// } + fn upgrade(&self) -> Option> { + self.upgrade().map(|v| Box::new(v) as Box) + } +} pub trait ProjectItem: Item { type Item: project2::Item; fn for_project_item( - project: Handle, - item: Handle, + project: Model, + item: Model, cx: &mut ViewContext, ) -> Self where @@ -714,7 +660,7 @@ pub trait FollowableItem: Item { ) -> bool; fn apply_update_proto( &mut self, - project: &Handle, + project: &Model, message: proto::update_view::Variant, cx: &mut ViewContext, ) -> Task>; @@ -736,7 +682,7 @@ pub trait FollowableItemHandle: ItemHandle { ) -> bool; fn apply_update_proto( &self, - project: &Handle, + project: &Model, message: proto::update_view::Variant, cx: &mut WindowContext, ) -> Task>; @@ -744,65 +690,65 @@ pub trait FollowableItemHandle: ItemHandle { fn is_project_item(&self, cx: &AppContext) -> bool; } -// impl FollowableItemHandle for View { -// fn remote_id(&self, client: &Arc, cx: &AppContext) -> Option { -// self.read(cx).remote_id().or_else(|| { -// client.peer_id().map(|creator| ViewId { -// creator, -// id: self.id() as u64, -// }) -// }) -// } +impl FollowableItemHandle for View { + fn remote_id(&self, client: &Arc, cx: &AppContext) -> Option { + self.read(cx).remote_id().or_else(|| { + client.peer_id().map(|creator| ViewId { + creator, + id: self.id().as_u64(), + }) + }) + } -// fn set_leader_peer_id(&self, leader_peer_id: Option, cx: &mut WindowContext) { -// self.update(cx, |this, cx| this.set_leader_peer_id(leader_peer_id, cx)) -// } + fn set_leader_peer_id(&self, leader_peer_id: Option, cx: &mut WindowContext) { + self.update(cx, |this, cx| this.set_leader_peer_id(leader_peer_id, cx)) + } -// fn to_state_proto(&self, cx: &AppContext) -> Option { -// self.read(cx).to_state_proto(cx) -// } + fn to_state_proto(&self, cx: &AppContext) -> Option { + self.read(cx).to_state_proto(cx) + } -// fn add_event_to_update_proto( -// &self, -// event: &dyn Any, -// update: &mut Option, -// cx: &AppContext, -// ) -> bool { -// if let Some(event) = event.downcast_ref() { -// self.read(cx).add_event_to_update_proto(event, update, cx) -// } else { -// false -// } -// } + fn add_event_to_update_proto( + &self, + event: &dyn Any, + update: &mut Option, + cx: &AppContext, + ) -> bool { + if let Some(event) = event.downcast_ref() { + self.read(cx).add_event_to_update_proto(event, update, cx) + } else { + false + } + } -// fn apply_update_proto( -// &self, -// project: &Handle, -// message: proto::update_view::Variant, -// cx: &mut WindowContext, -// ) -> Task> { -// self.update(cx, |this, cx| this.apply_update_proto(project, message, cx)) -// } + fn apply_update_proto( + &self, + project: &Model, + message: proto::update_view::Variant, + cx: &mut WindowContext, + ) -> Task> { + self.update(cx, |this, cx| this.apply_update_proto(project, message, cx)) + } -// fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool { -// if let Some(event) = event.downcast_ref() { -// T::should_unfollow_on_event(event, cx) -// } else { -// false -// } -// } + fn 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) -// } -// } + 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, Handle, Task, View, +// elements::Empty, AnyElement, AppContext, Element, Entity, Model, Task, View, // ViewContext, View, WeakViewHandle, // }; // use project2::{Project, ProjectEntryId, ProjectPath, WorktreeId}; @@ -824,7 +770,7 @@ pub trait FollowableItemHandle: ItemHandle { // pub is_dirty: bool, // pub is_singleton: bool, // pub has_conflict: bool, -// pub project_items: Vec>, +// pub project_items: Vec>, // pub nav_history: Option, // pub tab_descriptions: Option>, // pub tab_detail: Cell>, @@ -869,7 +815,7 @@ pub trait FollowableItemHandle: ItemHandle { // } // impl TestProjectItem { -// pub fn new(id: u64, path: &str, cx: &mut AppContext) -> Handle { +// pub fn new(id: u64, path: &str, cx: &mut AppContext) -> Model { // let entry_id = Some(ProjectEntryId::from_proto(id)); // let project_path = Some(ProjectPath { // worktree_id: WorktreeId::from_usize(0), @@ -881,7 +827,7 @@ pub trait FollowableItemHandle: ItemHandle { // }) // } -// pub fn new_untitled(cx: &mut AppContext) -> Handle { +// pub fn new_untitled(cx: &mut AppContext) -> Model { // cx.add_model(|_| Self { // project_path: None, // entry_id: None, @@ -934,7 +880,7 @@ pub trait FollowableItemHandle: ItemHandle { // self // } -// pub fn with_project_items(mut self, items: &[Handle]) -> Self { +// pub fn with_project_items(mut self, items: &[Model]) -> Self { // self.project_items.clear(); // self.project_items.extend(items.iter().cloned()); // self @@ -1045,7 +991,7 @@ pub trait FollowableItemHandle: ItemHandle { // fn save( // &mut self, -// _: Handle, +// _: Model, // _: &mut ViewContext, // ) -> Task> { // self.save_count += 1; @@ -1055,7 +1001,7 @@ pub trait FollowableItemHandle: ItemHandle { // fn save_as( // &mut self, -// _: Handle, +// _: Model, // _: std::path::PathBuf, // _: &mut ViewContext, // ) -> Task> { @@ -1066,7 +1012,7 @@ pub trait FollowableItemHandle: ItemHandle { // fn reload( // &mut self, -// _: Handle, +// _: Model, // _: &mut ViewContext, // ) -> Task> { // self.reload_count += 1; @@ -1083,7 +1029,7 @@ pub trait FollowableItemHandle: ItemHandle { // } // fn deserialize( -// _project: Handle, +// _project: Model, // _workspace: WeakViewHandle, // workspace_id: WorkspaceId, // _item_id: ItemId, diff --git a/crates/workspace2/src/notifications.rs b/crates/workspace2/src/notifications.rs new file mode 100644 index 0000000000..9922bcdd26 --- /dev/null +++ b/crates/workspace2/src/notifications.rs @@ -0,0 +1,404 @@ +use crate::{Toast, Workspace}; +use collections::HashMap; +use gpui2::{AnyView, AppContext, Entity, EntityId, EventEmitter, Render, View, ViewContext}; +use std::{any::TypeId, ops::DerefMut}; + +pub fn init(cx: &mut AppContext) { + cx.set_global(NotificationTracker::new()); + // todo!() + // simple_message_notification::init(cx); +} + +pub trait Notification: EventEmitter + Render { + fn should_dismiss_notification_on_event(&self, event: &Self::Event) -> bool; +} + +pub trait NotificationHandle: Send { + fn id(&self) -> EntityId; + fn to_any(&self) -> AnyView; +} + +impl NotificationHandle for View { + fn id(&self) -> EntityId { + self.entity_id() + } + + fn to_any(&self) -> AnyView { + self.clone().into() + } +} + +impl From<&dyn NotificationHandle> for AnyView { + fn from(val: &dyn NotificationHandle) -> Self { + val.to_any() + } +} + +pub(crate) struct NotificationTracker { + notifications_sent: HashMap>, +} + +impl std::ops::Deref for NotificationTracker { + type Target = HashMap>; + + fn deref(&self) -> &Self::Target { + &self.notifications_sent + } +} + +impl DerefMut for NotificationTracker { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.notifications_sent + } +} + +impl NotificationTracker { + fn new() -> Self { + Self { + notifications_sent: Default::default(), + } + } +} + +impl Workspace { + pub fn has_shown_notification_once( + &self, + id: usize, + cx: &ViewContext, + ) -> bool { + cx.global::() + .get(&TypeId::of::()) + .map(|ids| ids.contains(&id)) + .unwrap_or(false) + } + + pub fn show_notification_once( + &mut self, + id: usize, + cx: &mut ViewContext, + build_notification: impl FnOnce(&mut ViewContext) -> View, + ) { + if !self.has_shown_notification_once::(id, cx) { + let tracker = cx.global_mut::(); + let entry = tracker.entry(TypeId::of::()).or_default(); + entry.push(id); + self.show_notification::(id, cx, build_notification) + } + } + + pub fn show_notification( + &mut self, + id: usize, + cx: &mut ViewContext, + build_notification: impl FnOnce(&mut ViewContext) -> View, + ) { + let type_id = TypeId::of::(); + if self + .notifications + .iter() + .all(|(existing_type_id, existing_id, _)| { + (*existing_type_id, *existing_id) != (type_id, id) + }) + { + let notification = build_notification(cx); + cx.subscribe(¬ification, move |this, handle, event, cx| { + if handle.read(cx).should_dismiss_notification_on_event(event) { + this.dismiss_notification_internal(type_id, id, cx); + } + }) + .detach(); + self.notifications + .push((type_id, id, Box::new(notification))); + cx.notify(); + } + } + + pub fn dismiss_notification(&mut self, id: usize, cx: &mut ViewContext) { + let type_id = TypeId::of::(); + + self.dismiss_notification_internal(type_id, id, cx) + } + + pub fn show_toast(&mut self, toast: Toast, cx: &mut ViewContext) { + todo!() + // self.dismiss_notification::(toast.id, cx); + // self.show_notification(toast.id, cx, |cx| { + // cx.add_view(|_cx| match toast.on_click.as_ref() { + // Some((click_msg, on_click)) => { + // let on_click = on_click.clone(); + // simple_message_notification::MessageNotification::new(toast.msg.clone()) + // .with_click_message(click_msg.clone()) + // .on_click(move |cx| on_click(cx)) + // } + // None => simple_message_notification::MessageNotification::new(toast.msg.clone()), + // }) + // }) + } + + pub fn dismiss_toast(&mut self, id: usize, cx: &mut ViewContext) { + todo!() + // self.dismiss_notification::(id, cx); + } + + fn dismiss_notification_internal( + &mut self, + type_id: TypeId, + id: usize, + cx: &mut ViewContext, + ) { + self.notifications + .retain(|(existing_type_id, existing_id, _)| { + if (*existing_type_id, *existing_id) == (type_id, id) { + cx.notify(); + false + } else { + true + } + }); + } +} + +pub mod simple_message_notification { + use super::Notification; + use gpui2::{AnyElement, AppContext, Div, EventEmitter, Render, TextStyle, ViewContext}; + use serde::Deserialize; + use std::{borrow::Cow, sync::Arc}; + + // todo!() + // actions!(message_notifications, [CancelMessageNotification]); + + #[derive(Clone, Default, Deserialize, PartialEq)] + pub struct OsOpen(pub Cow<'static, str>); + + impl OsOpen { + pub fn new>>(url: I) -> Self { + OsOpen(url.into()) + } + } + + // todo!() + // impl_actions!(message_notifications, [OsOpen]); + // + // todo!() + // pub fn init(cx: &mut AppContext) { + // cx.add_action(MessageNotification::dismiss); + // cx.add_action( + // |_workspace: &mut Workspace, open_action: &OsOpen, cx: &mut ViewContext| { + // cx.platform().open_url(open_action.0.as_ref()); + // }, + // ) + // } + + enum NotificationMessage { + Text(Cow<'static, str>), + Element(fn(TextStyle, &AppContext) -> AnyElement), + } + + pub struct MessageNotification { + message: NotificationMessage, + on_click: Option) + Send + Sync>>, + click_message: Option>, + } + + pub enum MessageNotificationEvent { + Dismiss, + } + + impl EventEmitter for MessageNotification { + type Event = MessageNotificationEvent; + } + + impl MessageNotification { + pub fn new(message: S) -> MessageNotification + where + S: Into>, + { + Self { + message: NotificationMessage::Text(message.into()), + on_click: None, + click_message: None, + } + } + + // todo!() + // pub fn new_element( + // message: fn(TextStyle, &AppContext) -> AnyElement, + // ) -> MessageNotification { + // Self { + // message: NotificationMessage::Element(message), + // on_click: None, + // click_message: None, + // } + // } + + // pub fn with_click_message(mut self, message: S) -> Self + // where + // S: Into>, + // { + // self.click_message = Some(message.into()); + // self + // } + + // pub fn on_click(mut self, on_click: F) -> Self + // where + // F: 'static + Fn(&mut ViewContext), + // { + // self.on_click = Some(Arc::new(on_click)); + // self + // } + + // pub fn dismiss(&mut self, _: &CancelMessageNotification, cx: &mut ViewContext) { + // cx.emit(MessageNotificationEvent::Dismiss); + // } + } + + impl Render for MessageNotification { + type Element = Div; + + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + todo!() + } + } + // todo!() + // impl View for MessageNotification { + // fn ui_name() -> &'static str { + // "MessageNotification" + // } + + // fn render(&mut self, cx: &mut gpui2::ViewContext) -> gpui::AnyElement { + // let theme = theme2::current(cx).clone(); + // let theme = &theme.simple_message_notification; + + // enum MessageNotificationTag {} + + // let click_message = self.click_message.clone(); + // let message = match &self.message { + // NotificationMessage::Text(text) => { + // Text::new(text.to_owned(), theme.message.text.clone()).into_any() + // } + // NotificationMessage::Element(e) => e(theme.message.text.clone(), cx), + // }; + // let on_click = self.on_click.clone(); + // let has_click_action = on_click.is_some(); + + // Flex::column() + // .with_child( + // Flex::row() + // .with_child( + // message + // .contained() + // .with_style(theme.message.container) + // .aligned() + // .top() + // .left() + // .flex(1., true), + // ) + // .with_child( + // MouseEventHandler::new::(0, cx, |state, _| { + // let style = theme.dismiss_button.style_for(state); + // Svg::new("icons/x.svg") + // .with_color(style.color) + // .constrained() + // .with_width(style.icon_width) + // .aligned() + // .contained() + // .with_style(style.container) + // .constrained() + // .with_width(style.button_width) + // .with_height(style.button_width) + // }) + // .with_padding(Padding::uniform(5.)) + // .on_click(MouseButton::Left, move |_, this, cx| { + // this.dismiss(&Default::default(), cx); + // }) + // .with_cursor_style(CursorStyle::PointingHand) + // .aligned() + // .constrained() + // .with_height(cx.font_cache().line_height(theme.message.text.font_size)) + // .aligned() + // .top() + // .flex_float(), + // ), + // ) + // .with_children({ + // click_message + // .map(|click_message| { + // MouseEventHandler::new::( + // 0, + // cx, + // |state, _| { + // let style = theme.action_message.style_for(state); + + // Flex::row() + // .with_child( + // Text::new(click_message, style.text.clone()) + // .contained() + // .with_style(style.container), + // ) + // .contained() + // }, + // ) + // .on_click(MouseButton::Left, move |_, this, cx| { + // if let Some(on_click) = on_click.as_ref() { + // on_click(cx); + // this.dismiss(&Default::default(), cx); + // } + // }) + // // Since we're not using a proper overlay, we have to capture these extra events + // .on_down(MouseButton::Left, |_, _, _| {}) + // .on_up(MouseButton::Left, |_, _, _| {}) + // .with_cursor_style(if has_click_action { + // CursorStyle::PointingHand + // } else { + // CursorStyle::Arrow + // }) + // }) + // .into_iter() + // }) + // .into_any() + // } + // } + + impl Notification for MessageNotification { + fn should_dismiss_notification_on_event(&self, event: &Self::Event) -> bool { + match event { + MessageNotificationEvent::Dismiss => true, + } + } + } +} + +pub trait NotifyResultExt { + type Ok; + + fn notify_err( + self, + workspace: &mut Workspace, + cx: &mut ViewContext, + ) -> Option; +} + +impl NotifyResultExt for Result +where + E: std::fmt::Debug, +{ + type Ok = T; + + fn notify_err(self, workspace: &mut Workspace, cx: &mut ViewContext) -> Option { + match self { + Ok(value) => Some(value), + Err(err) => { + log::error!("TODO {err:?}"); + // todo!() + // workspace.show_notification(0, cx, |cx| { + // cx.add_view(|_cx| { + // simple_message_notification::MessageNotification::new(format!( + // "Error: {err:?}", + // )) + // }) + // }); + None + } + } + } +} diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index e0eb1b7ec2..b30ec0b7f8 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -1,48 +1,33 @@ // 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 crate::{ + item::{Item, ItemHandle, ItemSettings, WeakItemHandle}, + toolbar::Toolbar, + workspace_settings::{AutosaveSetting, WorkspaceSettings}, + SplitDirection, Workspace, +}; +use anyhow::Result; +use collections::{HashMap, HashSet, VecDeque}; +use gpui2::{ + AppContext, AsyncWindowContext, Component, Div, EntityId, EventEmitter, Model, PromptLevel, + Render, Task, View, ViewContext, VisualContext, WeakView, WindowContext, +}; +use parking_lot::Mutex; +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; +use settings2::Settings; +use std::{ + any::Any, + cmp, fmt, mem, + path::{Path, PathBuf}, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, + }, +}; +use ui::v_stack; +use ui::{prelude::*, Icon, IconButton, IconColor, IconElement}; +use util::truncate_and_remove_front; #[derive(PartialEq, Clone, Copy, Deserialize, Debug)] #[serde(rename_all = "camelCase")] @@ -69,19 +54,19 @@ pub enum SaveIntent { // #[derive(Clone, PartialEq)] // pub struct CloseItemById { // pub item_id: usize, -// pub pane: WeakViewHandle, +// pub pane: WeakView, // } // #[derive(Clone, PartialEq)] // pub struct CloseItemsToTheLeftById { // pub item_id: usize, -// pub pane: WeakViewHandle, +// pub pane: WeakView, // } // #[derive(Clone, PartialEq)] // pub struct CloseItemsToTheRightById { // pub item_id: usize, -// pub pane: WeakViewHandle, +// pub pane: WeakView, // } // #[derive(Clone, PartialEq, Debug, Deserialize, Default)] @@ -96,6 +81,7 @@ pub enum SaveIntent { // pub save_intent: Option, // } +// todo!() // actions!( // pane, // [ @@ -118,40 +104,40 @@ pub enum SaveIntent { // impl_actions!(pane, [ActivateItem, CloseActiveItem, CloseAllItems]); -// const MAX_NAVIGATION_HISTORY_LEN: usize = 1024; +const MAX_NAVIGATION_HISTORY_LEN: usize = 1024; -// 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)); -// } +pub fn init(cx: &mut AppContext) { + // todo!() + // cx.add_action(Pane::toggle_zoom); + // cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| { + // pane.activate_item(action.0, true, true, cx); + // }); + // cx.add_action(|pane: &mut Pane, _: &ActivateLastItem, cx| { + // pane.activate_item(pane.items.len() - 1, true, true, cx); + // }); + // cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| { + // pane.activate_prev_item(true, cx); + // }); + // cx.add_action(|pane: &mut Pane, _: &ActivateNextItem, cx| { + // pane.activate_next_item(true, cx); + // }); + // cx.add_async_action(Pane::close_active_item); + // cx.add_async_action(Pane::close_inactive_items); + // cx.add_async_action(Pane::close_clean_items); + // cx.add_async_action(Pane::close_items_to_the_left); + // cx.add_async_action(Pane::close_items_to_the_right); + // cx.add_async_action(Pane::close_all_items); + // cx.add_action(|pane: &mut Pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx)); + // cx.add_action(|pane: &mut Pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx)); + // cx.add_action(|pane: &mut Pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx)); + // cx.add_action(|pane: &mut Pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx)); +} -#[derive(Debug)] pub enum Event { AddItem { item: Box }, ActivateItem { local: bool }, Remove, - RemoveItem { item_id: usize }, + RemoveItem { item_id: EntityId }, Split(SplitDirection), ChangeItemTitle, Focus, @@ -159,36 +145,45 @@ pub enum Event { ZoomOut, } -use crate::{ - item::{ItemHandle, WeakItemHandle}, - SplitDirection, -}; -use collections::{HashMap, VecDeque}; -use gpui2::{Handle, ViewContext, WeakView}; -use project2::{Project, ProjectEntryId, ProjectPath}; -use std::{ - any::Any, - cell::RefCell, - cmp, mem, - path::PathBuf, - rc::Rc, - sync::{atomic::AtomicUsize, Arc}, -}; +impl fmt::Debug for Event { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Event::AddItem { item } => f.debug_struct("AddItem").field("item", &item.id()).finish(), + Event::ActivateItem { local } => f + .debug_struct("ActivateItem") + .field("local", local) + .finish(), + Event::Remove => f.write_str("Remove"), + Event::RemoveItem { item_id } => f + .debug_struct("RemoveItem") + .field("item_id", item_id) + .finish(), + Event::Split(direction) => f + .debug_struct("Split") + .field("direction", direction) + .finish(), + Event::ChangeItemTitle => f.write_str("ChangeItemTitle"), + Event::Focus => f.write_str("Focus"), + Event::ZoomIn => f.write_str("ZoomIn"), + Event::ZoomOut => f.write_str("ZoomOut"), + } + } +} pub struct Pane { items: Vec>, - // activation_history: Vec, - // zoomed: bool, - // active_item_index: usize, + activation_history: Vec, + zoomed: bool, + active_item_index: usize, // last_focused_view_by_item: HashMap, - // autoscroll: bool, + autoscroll: bool, nav_history: NavHistory, - // toolbar: ViewHandle, + toolbar: View, // tab_bar_context_menu: TabBarContextMenu, // tab_context_menu: ViewHandle, - // workspace: WeakViewHandle, - project: Handle, - // has_focus: bool, + workspace: WeakView, + project: Model, + has_focus: bool, // can_drop: Rc, &WindowContext) -> bool>, // can_split: bool, // render_tab_bar_buttons: Rc) -> AnyElement>, @@ -196,18 +191,18 @@ pub struct Pane { pub struct ItemNavHistory { history: NavHistory, - item: Rc, + item: Arc, } #[derive(Clone)] -pub struct NavHistory(Rc>); +pub struct NavHistory(Arc>); struct NavHistoryState { mode: NavigationMode, backward_stack: VecDeque, forward_stack: VecDeque, closed_stack: VecDeque, - paths_by_item: HashMap)>, + paths_by_item: HashMap)>, pane: WeakView, next_timestamp: Arc, } @@ -229,14 +224,14 @@ impl Default for NavigationMode { } pub struct NavigationEntry { - pub item: Rc, - pub data: Option>, + pub item: Arc, + pub data: Option>, pub timestamp: usize, } // pub struct DraggedItem { // pub handle: Box, -// pub pane: WeakViewHandle, +// pub pane: WeakView, // } // pub enum ReorderBehavior { @@ -315,114 +310,119 @@ pub struct NavigationEntry { // .into_any_named("nav button") // } +impl EventEmitter for Pane { + type Event = Event; +} + impl Pane { - // pub fn new( - // workspace: WeakViewHandle, - // project: ModelHandle, - // next_timestamp: Arc, - // cx: &mut ViewContext, - // ) -> Self { - // let pane_view_id = cx.view_id(); - // let handle = cx.weak_handle(); - // let context_menu = cx.add_view(|cx| ContextMenu::new(pane_view_id, cx)); - // context_menu.update(cx, |menu, _| { - // menu.set_position_mode(OverlayPositionMode::Local) - // }); + pub fn new( + workspace: WeakView, + project: Model, + next_timestamp: Arc, + cx: &mut ViewContext, + ) -> Self { + // todo!("context menu") + // let pane_view_id = cx.view_id(); + // 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)), - // 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"; - // } + let handle = cx.view().downgrade(); + 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(Arc::new(Mutex::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.build_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)), + 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() - // }), - // } - // } + // 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 { + // pub(crate) fn workspace(&self) -> &WeakView { // &self.workspace // } - // pub fn has_focus(&self) -> bool { - // self.has_focus - // } + pub fn has_focus(&self) -> bool { + self.has_focus + } // pub fn active_item_index(&self) -> usize { // self.active_item_index @@ -455,40 +455,40 @@ impl Pane { // cx.notify(); // } - // pub fn nav_history_for_item(&self, item: &ViewHandle) -> ItemNavHistory { - // ItemNavHistory { - // history: self.nav_history.clone(), - // item: Rc::new(item.downgrade()), - // } - // } + pub fn nav_history_for_item(&self, item: &View) -> ItemNavHistory { + ItemNavHistory { + history: self.nav_history.clone(), + item: Arc::new(item.downgrade()), + } + } - // pub fn nav_history(&self) -> &NavHistory { - // &self.nav_history - // } + pub fn nav_history(&self) -> &NavHistory { + &self.nav_history + } - // pub fn nav_history_mut(&mut self) -> &mut NavHistory { - // &mut 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 disable_history(&mut self) { + self.nav_history.disable(); + } - // pub fn enable_history(&mut self) { - // self.nav_history.enable(); - // } + 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_backward(&self) -> bool { + !self.nav_history.0.lock().backward_stack.is_empty() + } - // pub fn can_navigate_forward(&self) -> bool { - // !self.nav_history.0.borrow().forward_stack.is_empty() - // } + pub fn can_navigate_forward(&self) -> bool { + !self.nav_history.0.lock().forward_stack.is_empty() + } - // fn history_updated(&mut self, cx: &mut ViewContext) { - // self.toolbar.update(cx, |_, cx| cx.notify()); - // } + fn history_updated(&mut self, cx: &mut ViewContext) { + self.toolbar.update(cx, |_, cx| cx.notify()); + } pub(crate) fn open_item( &mut self, @@ -532,7 +532,7 @@ impl Pane { let abs_path = project.absolute_path(&project_path, cx); self.nav_history .0 - .borrow_mut() + .lock() .paths_by_item .insert(item.id(), (project_path, abs_path)); } @@ -615,13 +615,13 @@ impl Pane { cx.emit(Event::AddItem { item }); } - // pub fn items_len(&self) -> usize { - // self.items.len() - // } + pub fn items_len(&self) -> usize { + self.items.len() + } - // pub fn items(&self) -> impl Iterator> + DoubleEndedIterator { - // self.items.iter() - // } + pub fn items(&self) -> impl Iterator> + DoubleEndedIterator { + self.items.iter() + } // pub fn items_of_type(&self) -> impl '_ + Iterator> { // self.items @@ -629,9 +629,9 @@ impl Pane { // .filter_map(|item| item.as_any().clone().downcast()) // } - // pub fn active_item(&self) -> Option> { - // self.items.get(self.active_item_index).cloned() - // } + pub fn active_item(&self) -> Option> { + self.items.get(self.active_item_index).cloned() + } // pub fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option { // self.items @@ -653,9 +653,9 @@ impl Pane { // }) // } - // pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option { - // self.items.iter().position(|i| i.id() == item.id()) - // } + pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option { + self.items.iter().position(|i| i.id() == item.id()) + } // pub fn toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext) { // // Potentially warn the user of the new keybinding @@ -751,448 +751,445 @@ impl Pane { // )) // } - // pub fn close_item_by_id( - // &mut self, - // item_id_to_close: usize, - // save_intent: SaveIntent, - // cx: &mut ViewContext, - // ) -> Task> { - // self.close_items(cx, save_intent, move |view_id| view_id == item_id_to_close) + pub fn close_item_by_id( + &mut self, + item_id_to_close: EntityId, + save_intent: SaveIntent, + cx: &mut ViewContext, + ) -> Task> { + self.close_items(cx, save_intent, move |view_id| view_id == item_id_to_close) + } + + // pub fn close_inactive_items( + // &mut self, + // _: &CloseInactiveItems, + // cx: &mut ViewContext, + // ) -> Option>> { + // if self.items.is_empty() { + // return None; // } - // pub fn close_inactive_items( - // &mut self, - // _: &CloseInactiveItems, - // cx: &mut ViewContext, - // ) -> Option>> { - // if self.items.is_empty() { - // return None; - // } + // let active_item_id = self.items[self.active_item_index].id(); + // Some(self.close_items(cx, SaveIntent::Close, move |item_id| { + // item_id != active_item_id + // })) + // } - // let active_item_id = self.items[self.active_item_index].id(); - // Some(self.close_items(cx, SaveIntent::Close, move |item_id| { - // item_id != active_item_id - // })) + // pub fn close_clean_items( + // &mut self, + // _: &CloseCleanItems, + // cx: &mut ViewContext, + // ) -> Option>> { + // let item_ids: Vec<_> = self + // .items() + // .filter(|item| !item.is_dirty(cx)) + // .map(|item| item.id()) + // .collect(); + // Some(self.close_items(cx, SaveIntent::Close, move |item_id| { + // item_ids.contains(&item_id) + // })) + // } + + // pub fn close_items_to_the_left( + // &mut self, + // _: &CloseItemsToTheLeft, + // cx: &mut ViewContext, + // ) -> Option>> { + // if self.items.is_empty() { + // return None; + // } + // let active_item_id = self.items[self.active_item_index].id(); + // Some(self.close_items_to_the_left_by_id(active_item_id, cx)) + // } + + // pub fn close_items_to_the_left_by_id( + // &mut self, + // item_id: usize, + // cx: &mut ViewContext, + // ) -> Task> { + // let item_ids: Vec<_> = self + // .items() + // .take_while(|item| item.id() != item_id) + // .map(|item| item.id()) + // .collect(); + // self.close_items(cx, SaveIntent::Close, move |item_id| { + // item_ids.contains(&item_id) + // }) + // } + + // pub fn close_items_to_the_right( + // &mut self, + // _: &CloseItemsToTheRight, + // cx: &mut ViewContext, + // ) -> Option>> { + // if self.items.is_empty() { + // return None; + // } + // let active_item_id = self.items[self.active_item_index].id(); + // Some(self.close_items_to_the_right_by_id(active_item_id, cx)) + // } + + // pub fn close_items_to_the_right_by_id( + // &mut self, + // item_id: usize, + // cx: &mut ViewContext, + // ) -> Task> { + // let item_ids: Vec<_> = self + // .items() + // .rev() + // .take_while(|item| item.id() != item_id) + // .map(|item| item.id()) + // .collect(); + // self.close_items(cx, SaveIntent::Close, move |item_id| { + // item_ids.contains(&item_id) + // }) + // } + + // pub fn close_all_items( + // &mut self, + // action: &CloseAllItems, + // cx: &mut ViewContext, + // ) -> Option>> { + // if self.items.is_empty() { + // return None; // } - // pub fn close_clean_items( - // &mut self, - // _: &CloseCleanItems, - // cx: &mut ViewContext, - // ) -> Option>> { - // let item_ids: Vec<_> = self - // .items() - // .filter(|item| !item.is_dirty(cx)) - // .map(|item| item.id()) - // .collect(); - // Some(self.close_items(cx, SaveIntent::Close, move |item_id| { - // item_ids.contains(&item_id) - // })) - // } + // Some( + // self.close_items(cx, action.save_intent.unwrap_or(SaveIntent::Close), |_| { + // true + // }), + // ) + // } - // pub fn close_items_to_the_left( - // &mut self, - // _: &CloseItemsToTheLeft, - // cx: &mut ViewContext, - // ) -> Option>> { - // if self.items.is_empty() { - // return None; - // } - // let active_item_id = self.items[self.active_item_index].id(); - // Some(self.close_items_to_the_left_by_id(active_item_id, cx)) - // } + pub(super) fn file_names_for_prompt( + items: &mut dyn Iterator>, + all_dirty_items: usize, + cx: &AppContext, + ) -> String { + /// Quantity of item paths displayed in prompt prior to cutoff.. + const FILE_NAMES_CUTOFF_POINT: usize = 10; + let mut file_names: Vec<_> = items + .filter_map(|item| { + item.project_path(cx).and_then(|project_path| { + project_path + .path + .file_name() + .and_then(|name| name.to_str().map(ToOwned::to_owned)) + }) + }) + .take(FILE_NAMES_CUTOFF_POINT) + .collect(); + let should_display_followup_text = + all_dirty_items > FILE_NAMES_CUTOFF_POINT || file_names.len() != all_dirty_items; + if should_display_followup_text { + let not_shown_files = all_dirty_items - file_names.len(); + if not_shown_files == 1 { + file_names.push(".. 1 file not shown".into()); + } else { + file_names.push(format!(".. {} files not shown", not_shown_files).into()); + } + } + let file_names = file_names.join("\n"); + format!( + "Do you want to save changes to the following {} files?\n{file_names}", + all_dirty_items + ) + } - // pub fn close_items_to_the_left_by_id( - // &mut self, - // item_id: usize, - // cx: &mut ViewContext, - // ) -> Task> { - // let item_ids: Vec<_> = self - // .items() - // .take_while(|item| item.id() != item_id) - // .map(|item| item.id()) - // .collect(); - // self.close_items(cx, SaveIntent::Close, move |item_id| { - // item_ids.contains(&item_id) - // }) - // } + pub fn close_items( + &mut self, + cx: &mut ViewContext, + mut save_intent: SaveIntent, + should_close: impl 'static + Fn(EntityId) -> bool, + ) -> Task> { + // Find the items to close. + let mut items_to_close = Vec::new(); + let mut dirty_items = Vec::new(); + for item in &self.items { + if should_close(item.id()) { + items_to_close.push(item.boxed_clone()); + if item.is_dirty(cx) { + dirty_items.push(item.boxed_clone()); + } + } + } - // pub fn close_items_to_the_right( - // &mut self, - // _: &CloseItemsToTheRight, - // cx: &mut ViewContext, - // ) -> Option>> { - // if self.items.is_empty() { - // return None; - // } - // let active_item_id = self.items[self.active_item_index].id(); - // Some(self.close_items_to_the_right_by_id(active_item_id, cx)) - // } + // 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)); - // pub fn close_items_to_the_right_by_id( - // &mut self, - // item_id: usize, - // cx: &mut ViewContext, - // ) -> Task> { - // let item_ids: Vec<_> = self - // .items() - // .rev() - // .take_while(|item| item.id() != item_id) - // .map(|item| item.id()) - // .collect(); - // self.close_items(cx, SaveIntent::Close, move |item_id| { - // item_ids.contains(&item_id) - // }) - // } + let workspace = self.workspace.clone(); + cx.spawn(|pane, mut cx| async move { + if save_intent == SaveIntent::Close && dirty_items.len() > 1 { + let 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.await { + Ok(0) => save_intent = SaveIntent::SaveAll, + Ok(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.update(&mut 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; + }; - // pub fn close_all_items( - // &mut self, - // action: &CloseAllItems, - // cx: &mut ViewContext, - // ) -> Option>> { - // if self.items.is_empty() { - // return None; - // } + // 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.update(&mut 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)); - // Some( - // self.close_items(cx, action.save_intent.unwrap_or(SaveIntent::Close), |_| { - // true - // }), - // ) - // } + if should_save + && !Self::save_item( + project.clone(), + &pane, + item_ix, + &*item, + save_intent, + &mut cx, + ) + .await? + { + break; + } - // pub(super) fn file_names_for_prompt( - // items: &mut dyn Iterator>, - // all_dirty_items: usize, - // cx: &AppContext, - // ) -> String { - // /// Quantity of item paths displayed in prompt prior to cutoff.. - // const FILE_NAMES_CUTOFF_POINT: usize = 10; - // let mut file_names: Vec<_> = items - // .filter_map(|item| { - // item.project_path(cx).and_then(|project_path| { - // project_path - // .path - // .file_name() - // .and_then(|name| name.to_str().map(ToOwned::to_owned)) - // }) - // }) - // .take(FILE_NAMES_CUTOFF_POINT) - // .collect(); - // let should_display_followup_text = - // all_dirty_items > FILE_NAMES_CUTOFF_POINT || file_names.len() != all_dirty_items; - // if should_display_followup_text { - // let not_shown_files = all_dirty_items - file_names.len(); - // if not_shown_files == 1 { - // file_names.push(".. 1 file not shown".into()); - // } else { - // file_names.push(format!(".. {} files not shown", not_shown_files).into()); - // } - // } - // let file_names = file_names.join("\n"); - // format!( - // "Do you want to save changes to the following {} files?\n{file_names}", - // all_dirty_items - // ) - // } + // 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); + } + })?; + } - // pub fn close_items( - // &mut self, - // cx: &mut ViewContext, - // mut save_intent: SaveIntent, - // should_close: impl 'static + Fn(usize) -> bool, - // ) -> Task> { - // // Find the items to close. - // let mut items_to_close = Vec::new(); - // let mut dirty_items = Vec::new(); - // for item in &self.items { - // if should_close(item.id()) { - // items_to_close.push(item.boxed_clone()); - // if item.is_dirty(cx) { - // dirty_items.push(item.boxed_clone()); - // } - // } - // } + pane.update(&mut cx, |_, cx| cx.notify())?; + Ok(()) + }) + } - // // 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)); + pub fn remove_item( + &mut self, + item_index: usize, + activate_pane: bool, + cx: &mut ViewContext, + ) { + self.activation_history + .retain(|&history_entry| history_entry != self.items[item_index].id()); - // 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; - // }; + 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)); - // // 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)); + let should_activate = activate_pane || self.has_focus; + self.activate_item(index_to_activate, should_activate, should_activate, cx); + } - // if should_save - // && !Self::save_item( - // project.clone(), - // &pane, - // item_ix, - // &*item, - // save_intent, - // &mut cx, - // ) - // .await? - // { - // break; - // } + let item = self.items.remove(item_index); - // // 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); - // } - // })?; - // } + cx.emit(Event::RemoveItem { item_id: item.id() }); + if self.items.is_empty() { + item.deactivated(cx); + self.update_toolbar(cx); + cx.emit(Event::Remove); + } - // pane.update(&mut cx, |_, cx| cx.notify())?; - // Ok(()) - // }) - // } + if item_index < self.active_item_index { + self.active_item_index -= 1; + } - // pub fn remove_item( - // &mut self, - // item_index: usize, - // activate_pane: bool, - // cx: &mut ViewContext, - // ) { - // self.activation_history - // .retain(|&history_entry| history_entry != self.items[item_index].id()); + self.nav_history.set_mode(NavigationMode::ClosingItem); + item.deactivated(cx); + self.nav_history.set_mode(NavigationMode::Normal); - // 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)); + if let Some(path) = item.project_path(cx) { + let abs_path = self + .nav_history + .0 + .lock() + .paths_by_item + .get(&item.id()) + .and_then(|(_, abs_path)| abs_path.clone()); - // let should_activate = activate_pane || self.has_focus; - // self.activate_item(index_to_activate, should_activate, should_activate, cx); - // } + self.nav_history + .0 + .lock() + .paths_by_item + .insert(item.id(), (path, abs_path)); + } else { + self.nav_history.0.lock().paths_by_item.remove(&item.id()); + } - // let item = self.items.remove(item_index); + if self.items.is_empty() && self.zoomed { + cx.emit(Event::ZoomOut); + } - // cx.emit(Event::RemoveItem { item_id: item.id() }); - // if self.items.is_empty() { - // item.deactivated(cx); - // self.update_toolbar(cx); - // cx.emit(Event::Remove); - // } + cx.notify(); + } - // if item_index < self.active_item_index { - // self.active_item_index -= 1; - // } + pub async fn save_item( + project: Model, + pane: &WeakView, + item_ix: usize, + item: &dyn ItemHandle, + save_intent: SaveIntent, + cx: &mut AsyncWindowContext, + ) -> Result { + const CONFLICT_MESSAGE: &str = + "This file has changed on disk since you started editing it. Do you want to overwrite it?"; - // self.nav_history.set_mode(NavigationMode::ClosingItem); - // item.deactivated(cx); - // self.nav_history.set_mode(NavigationMode::Normal); + if save_intent == SaveIntent::Skip { + return Ok(true); + } - // 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()); + let (mut has_conflict, mut is_dirty, mut can_save, can_save_as) = cx.update(|_, cx| { + ( + item.has_conflict(cx), + item.is_dirty(cx), + item.can_save(cx), + item.is_singleton(cx), + ) + })?; - // 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()); - // } + // when saving a single buffer, we ignore whether or not it's dirty. + if save_intent == SaveIntent::Save { + is_dirty = true; + } - // if self.items.is_empty() && self.zoomed { - // cx.emit(Event::ZoomOut); - // } + if save_intent == SaveIntent::SaveAs { + is_dirty = true; + has_conflict = false; + can_save = false; + } - // cx.notify(); - // } + if save_intent == SaveIntent::Overwrite { + has_conflict = false; + } - // pub async fn save_item( - // project: ModelHandle, - // pane: &WeakViewHandle, - // item_ix: usize, - // item: &dyn ItemHandle, - // save_intent: SaveIntent, - // cx: &mut AsyncAppContext, - // ) -> Result { - // const CONFLICT_MESSAGE: &str = - // "This file has changed on disk since you started editing it. Do you want to overwrite it?"; + if has_conflict && can_save { + let 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.await { + Ok(0) => pane.update(cx, |_, cx| item.save(project, cx))?.await?, + Ok(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.update(|_, cx| { + matches!( + WorkspaceSettings::get_global(cx).autosave, + AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange + ) && Self::can_autosave_item(&*item, cx) + })?; + if !will_autosave { + let 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.await { + Ok(0) => {} + Ok(1) => return Ok(true), // Don't save this file + _ => return Ok(false), // Cancel + } + } + } - // if save_intent == SaveIntent::Skip { - // return Ok(true); - // } + if can_save { + pane.update(cx, |_, cx| item.save(project, cx))?.await?; + } else if can_save_as { + let start_abs_path = project + .update(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 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), - // ) - // }); + let abs_path = cx.update(|_, cx| cx.prompt_for_new_path(&start_abs_path))?; + if let Some(abs_path) = abs_path.await.ok().flatten() { + pane.update(cx, |_, cx| item.save_as(project, abs_path, cx))? + .await?; + } else { + return Ok(false); + } + } + } + Ok(true) + } - // // when saving a single buffer, we ignore whether or not it's dirty. - // if save_intent == SaveIntent::Save { - // is_dirty = 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 + } - // if save_intent == SaveIntent::SaveAs { - // is_dirty = true; - // has_conflict = false; - // can_save = false; - // } + pub fn autosave_item( + item: &dyn ItemHandle, + project: Model, + cx: &mut WindowContext, + ) -> Task> { + if Self::can_autosave_item(item, cx) { + item.save(project, cx) + } else { + Task::ready(Ok(())) + } + } - // if save_intent == SaveIntent::Overwrite { - // has_conflict = false; - // } - - // if has_conflict && can_save { - // let mut answer = pane.update(cx, |pane, cx| { - // pane.activate_item(item_ix, true, true, cx); - // cx.prompt( - // PromptLevel::Warning, - // CONFLICT_MESSAGE, - // &["Overwrite", "Discard", "Cancel"], - // ) - // })?; - // match answer.next().await { - // Some(0) => pane.update(cx, |_, cx| item.save(project, cx))?.await?, - // Some(1) => pane.update(cx, |_, cx| item.reload(project, cx))?.await?, - // _ => return Ok(false), - // } - // } else if is_dirty && (can_save || can_save_as) { - // if save_intent == SaveIntent::Close { - // let will_autosave = cx.read(|cx| { - // matches!( - // settings::get::(cx).autosave, - // AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange - // ) && Self::can_autosave_item(&*item, cx) - // }); - // if !will_autosave { - // let mut answer = pane.update(cx, |pane, cx| { - // pane.activate_item(item_ix, true, true, cx); - // let prompt = dirty_message_for(item.project_path(cx)); - // cx.prompt( - // PromptLevel::Warning, - // &prompt, - // &["Save", "Don't Save", "Cancel"], - // ) - // })?; - // match answer.next().await { - // Some(0) => {} - // Some(1) => return Ok(true), // Don't save his file - // _ => return Ok(false), // Cancel - // } - // } - // } - - // if can_save { - // pane.update(cx, |_, cx| item.save(project, cx))?.await?; - // } else if can_save_as { - // let start_abs_path = project - // .read_with(cx, |project, cx| { - // let worktree = project.visible_worktrees(cx).next()?; - // Some(worktree.read(cx).as_local()?.abs_path().to_path_buf()) - // }) - // .unwrap_or_else(|| Path::new("").into()); - - // let mut abs_path = cx.update(|cx| cx.prompt_for_new_path(&start_abs_path)); - // if let Some(abs_path) = abs_path.next().await.flatten() { - // pane.update(cx, |_, cx| item.save_as(project, abs_path, cx))? - // .await?; - // } else { - // return Ok(false); - // } - // } - // } - // Ok(true) - // } - - // fn can_autosave_item(item: &dyn ItemHandle, cx: &AppContext) -> bool { - // let is_deleted = item.project_entry_ids(cx).is_empty(); - // item.is_dirty(cx) && !item.has_conflict(cx) && item.can_save(cx) && !is_deleted - // } - - // pub fn autosave_item( - // item: &dyn ItemHandle, - // project: ModelHandle, - // cx: &mut WindowContext, - // ) -> Task> { - // if Self::can_autosave_item(item, cx) { - // item.save(project, cx) - // } else { - // Task::ready(Ok(())) - // } - // } - - // pub fn focus_active_item(&mut self, cx: &mut ViewContext) { - // if let Some(active_item) = self.active_item() { - // cx.focus(active_item.as_any()); - // } - // } + pub fn focus_active_item(&mut self, cx: &mut ViewContext) { + todo!(); + // if let Some(active_item) = self.active_item() { + // cx.focus(active_item.as_any()); + // } + } // pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext) { // cx.emit(Event::Split(direction)); @@ -1317,38 +1314,178 @@ impl Pane { // }); // } - // pub fn toolbar(&self) -> &ViewHandle { - // &self.toolbar - // } + pub fn toolbar(&self) -> &View { + &self.toolbar + } - // pub fn handle_deleted_project_item( - // &mut self, - // entry_id: ProjectEntryId, - // cx: &mut ViewContext, - // ) -> Option<()> { - // let (item_index_to_delete, item_id) = self.items().enumerate().find_map(|(i, item)| { - // if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] { - // Some((i, item.id())) - // } else { - // None - // } - // })?; + pub fn handle_deleted_project_item( + &mut self, + entry_id: ProjectEntryId, + cx: &mut ViewContext, + ) -> Option<()> { + let (item_index_to_delete, item_id) = self.items().enumerate().find_map(|(i, item)| { + if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] { + Some((i, item.id())) + } else { + None + } + })?; - // self.remove_item(item_index_to_delete, false, cx); - // self.nav_history.remove_item(item_id); + self.remove_item(item_index_to_delete, false, cx); + self.nav_history.remove_item(item_id); - // Some(()) - // } + Some(()) + } - // fn update_toolbar(&mut self, cx: &mut ViewContext) { - // let active_item = self - // .items - // .get(self.active_item_index) - // .map(|item| item.as_ref()); - // self.toolbar.update(cx, |toolbar, cx| { - // toolbar.set_active_item(active_item, cx); - // }); - // } + fn update_toolbar(&mut self, cx: &mut ViewContext) { + let active_item = self + .items + .get(self.active_item_index) + .map(|item| item.as_ref()); + self.toolbar.update(cx, |toolbar, cx| { + toolbar.set_active_item(active_item, cx); + }); + } + + fn render_tab( + &self, + ix: usize, + item: &Box, + detail: usize, + cx: &mut ViewContext<'_, Pane>, + ) -> impl Component { + let label = item.tab_content(Some(detail), cx); + let close_icon = || IconElement::new(Icon::Close).color(IconColor::Muted); + + let (tab_bg, tab_hover_bg, tab_active_bg) = match ix == self.active_item_index { + false => ( + cx.theme().colors().tab_inactive, + cx.theme().colors().ghost_element_hover, + cx.theme().colors().ghost_element_active, + ), + true => ( + cx.theme().colors().tab_active, + cx.theme().colors().element_hover, + cx.theme().colors().element_active, + ), + }; + + let close_right = ItemSettings::get_global(cx).close_position.right(); + + div() + .id(item.id()) + // .on_drag(move |pane, cx| pane.render_tab(ix, item.boxed_clone(), detail, cx)) + // .drag_over::(|d| d.bg(cx.theme().colors().element_drop_target)) + // .on_drop(|_view, state: View, cx| { + // eprintln!("{:?}", state.read(cx)); + // }) + .px_2() + .py_0p5() + .flex() + .items_center() + .justify_center() + .bg(tab_bg) + .hover(|h| h.bg(tab_hover_bg)) + .active(|a| a.bg(tab_active_bg)) + .child( + div() + .px_1() + .flex() + .items_center() + .gap_1p5() + .children(if item.has_conflict(cx) { + Some( + IconElement::new(Icon::ExclamationTriangle) + .size(ui::IconSize::Small) + .color(IconColor::Warning), + ) + } else if item.is_dirty(cx) { + Some( + IconElement::new(Icon::ExclamationTriangle) + .size(ui::IconSize::Small) + .color(IconColor::Info), + ) + } else { + None + }) + .children(if !close_right { + Some(close_icon()) + } else { + None + }) + .child(label) + .children(if close_right { + Some(close_icon()) + } else { + None + }), + ) + } + + fn render_tab_bar(&mut self, cx: &mut ViewContext<'_, Pane>) -> impl Component { + div() + .group("tab_bar") + .id("tab_bar") + .w_full() + .flex() + .bg(cx.theme().colors().tab_bar) + // Left Side + .child( + div() + .relative() + .px_1() + .flex() + .flex_none() + .gap_2() + // Nav Buttons + .child( + div() + .right_0() + .flex() + .items_center() + .gap_px() + .child(IconButton::new("navigate_backward", Icon::ArrowLeft).state( + InteractionState::Enabled.if_enabled(self.can_navigate_backward()), + )) + .child(IconButton::new("navigate_forward", Icon::ArrowRight).state( + InteractionState::Enabled.if_enabled(self.can_navigate_forward()), + )), + ), + ) + .child( + div().w_0().flex_1().h_full().child( + div().id("tabs").flex().overflow_x_scroll().children( + self.items + .iter() + .enumerate() + .zip(self.tab_details(cx)) + .map(|((ix, item), detail)| self.render_tab(ix, item, detail, cx)), + ), + ), + ) + // Right Side + .child( + div() + // We only use absolute here since we don't + // have opacity or `hidden()` yet + .absolute() + .neg_top_7() + .px_1() + .flex() + .flex_none() + .gap_2() + .group_hover("tab_bar", |this| this.top_0()) + // Nav Buttons + .child( + div() + .flex() + .items_center() + .gap_px() + .child(IconButton::new("plus", Icon::Plus)) + .child(IconButton::new("split", Icon::Split)), + ), + ) + } // fn render_tabs(&mut self, cx: &mut ViewContext) -> impl Element { // let theme = theme::current(cx).clone(); @@ -1505,46 +1642,46 @@ impl Pane { // row // } - // fn tab_details(&self, cx: &AppContext) -> Vec { - // let mut tab_details = (0..self.items.len()).map(|_| 0).collect::>(); + fn tab_details(&self, cx: &AppContext) -> Vec { + let mut tab_details = self.items.iter().map(|_| 0).collect::>(); - // let mut tab_descriptions = HashMap::default(); - // let mut done = false; - // while !done { - // done = true; + 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); - // } - // } - // } + // 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; - // } - // } - // } - // } + // If two or more items have the same tab description, increase eir 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 - // } + tab_details + } // fn render_tab( // item: &Box, - // pane: WeakViewHandle, + // pane: WeakView, // first: bool, // detail: Option, // hovered: bool, @@ -1557,7 +1694,7 @@ impl Pane { // fn render_dragged_tab( // item: &Box, - // pane: WeakViewHandle, + // pane: WeakView, // first: bool, // detail: Option, // hovered: bool, @@ -1571,7 +1708,7 @@ impl Pane { // fn render_tab_with_title( // title: AnyElement, // item: &Box, - // pane: WeakViewHandle, + // pane: WeakView, // first: bool, // hovered: bool, // tab_style: &theme::Tab, @@ -1736,398 +1873,406 @@ impl Pane { // pub fn is_zoomed(&self) -> bool { // self.zoomed // } - // } +} - // impl Entity for Pane { - // type Event = Event; - // } +// impl Entity for Pane { +// type Event = Event; +// } - // impl View for Pane { - // fn ui_name() -> &'static str { - // "Pane" - // } +impl Render for Pane { + type Element = Div; - // fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - // enum MouseNavigationHandler {} + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + v_stack() + .child(self.render_tab_bar(cx)) + .child(div() /* toolbar */) + .child(if let Some(item) = self.active_item() { + item.to_any().render() + } else { + // todo!() + div().child("Empty Pane").render() + }) - // MouseEventHandler::new::(0, cx, |_, cx| { - // let active_item_index = self.active_item_index; + // enum MouseNavigationHandler {} - // if let Some(active_item) = self.active_item() { - // Flex::column() - // .with_child({ - // let theme = theme::current(cx).clone(); + // MouseEventHandler::new::(0, cx, |_, cx| { + // let active_item_index = self.active_item_index; - // let mut stack = Stack::new(); + // if let Some(active_item) = self.active_item() { + // Flex::column() + // .with_child({ + // let theme = theme::current(cx).clone(); - // enum TabBarEventHandler {} - // stack.add_child( - // MouseEventHandler::new::(0, cx, |_, _| { - // Empty::new() - // .contained() - // .with_style(theme.workspace.tab_bar.container) - // }) - // .on_down( - // MouseButton::Left, - // move |_, this, cx| { - // this.activate_item(active_item_index, true, true, cx); - // }, - // ), - // ); - // let tooltip_style = theme.tooltip.clone(); - // let tab_bar_theme = theme.workspace.tab_bar.clone(); + // let mut stack = Stack::new(); - // 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(); + // enum TabBarEventHandler {} + // stack.add_child( + // MouseEventHandler::new::(0, cx, |_, _| { + // Empty::new() + // .contained() + // .with_style(theme.workspace.tab_bar.container) + // }) + // .on_down( + // MouseButton::Left, + // move |_, this, cx| { + // this.activate_item(active_item_index, true, true, cx); + // }, + // ), + // ); + // let tooltip_style = theme.tooltip.clone(); + // let tab_bar_theme = theme.workspace.tab_bar.clone(); - // let 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")); + // 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(); - // 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(), - // ) - // } + // 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")); - // stack.add_child(tab_row); - // stack - // .constrained() - // .with_height(theme.workspace.tab_bar.height) - // .flex(1., false) - // .into_any_named("tab bar") - // }) - // .with_child({ - // enum PaneContentTabDropTarget {} - // dragged_item_receiver::( - // self, - // 0, - // self.active_item_index + 1, - // !self.can_split, - // if self.can_split { Some(100.) } else { None }, - // cx, - // { - // let toolbar = self.toolbar.clone(); - // let toolbar_hidden = toolbar.read(cx).hidden(); - // move |_, cx| { - // Flex::column() - // .with_children( - // (!toolbar_hidden) - // .then(|| ChildView::new(&toolbar, cx).expanded()), - // ) - // .with_child( - // ChildView::new(active_item.as_any(), cx).flex(1., true), - // ) - // } - // }, - // ) - // .flex(1., true) - // }) - // .with_child(ChildView::new(&self.tab_context_menu, cx)) - // .into_any() - // } else { - // enum EmptyPane {} - // let theme = theme::current(cx).clone(); + // 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(), + // ) + // } - // dragged_item_receiver::(self, 0, 0, false, None, cx, |_, cx| { - // self.render_blank_pane(&theme, cx) - // }) - // .on_down(MouseButton::Left, |_, _, cx| { - // cx.focus_parent(); - // }) - // .into_any() - // } - // }) - // .on_down( - // MouseButton::Navigate(NavigationDirection::Back), - // move |_, pane, cx| { - // if let Some(workspace) = pane.workspace.upgrade(cx) { - // let pane = cx.weak_handle(); - // cx.window_context().defer(move |cx| { - // workspace.update(cx, |workspace, cx| { - // workspace.go_back(pane, cx).detach_and_log_err(cx) - // }) - // }) - // } - // }, - // ) - // .on_down(MouseButton::Navigate(NavigationDirection::Forward), { - // move |_, pane, cx| { - // if let Some(workspace) = pane.workspace.upgrade(cx) { - // let pane = cx.weak_handle(); - // cx.window_context().defer(move |cx| { - // workspace.update(cx, |workspace, cx| { - // workspace.go_forward(pane, cx).detach_and_log_err(cx) - // }) - // }) - // } - // } - // }) - // .into_any_named("pane") - // } + // stack.add_child(tab_row); + // stack + // .constrained() + // .with_height(theme.workspace.tab_bar.height) + // .flex(1., false) + // .into_any_named("tab bar") + // }) + // .with_child({ + // enum PaneContentTabDropTarget {} + // dragged_item_receiver::( + // self, + // 0, + // self.active_item_index + 1, + // !self.can_split, + // if self.can_split { Some(100.) } else { None }, + // cx, + // { + // let toolbar = self.toolbar.clone(); + // let toolbar_hidden = toolbar.read(cx).hidden(); + // move |_, cx| { + // Flex::column() + // .with_children( + // (!toolbar_hidden) + // .then(|| ChildView::new(&toolbar, cx).expanded()), + // ) + // .with_child( + // ChildView::new(active_item.as_any(), cx).flex(1., true), + // ) + // } + // }, + // ) + // .flex(1., true) + // }) + // .with_child(ChildView::new(&self.tab_context_menu, cx)) + // .into_any() + // } else { + // enum EmptyPane {} + // let theme = theme::current(cx).clone(); - // fn focus_in(&mut self, focused: AnyViewHandle, cx: &mut ViewContext) { - // if !self.has_focus { - // self.has_focus = true; - // cx.emit(Event::Focus); - // cx.notify(); - // } + // dragged_item_receiver::(self, 0, 0, false, None, cx, |_, cx| { + // self.render_blank_pane(&theme, cx) + // }) + // .on_down(MouseButton::Left, |_, _, cx| { + // cx.focus_parent(); + // }) + // .into_any() + // } + // }) + // .on_down( + // MouseButton::Navigate(NavigationDirection::Back), + // move |_, pane, cx| { + // if let Some(workspace) = pane.workspace.upgrade(cx) { + // let pane = cx.weak_handle(); + // cx.window_context().defer(move |cx| { + // workspace.update(cx, |workspace, cx| { + // workspace.go_back(pane, cx).detach_and_log_err(cx) + // }) + // }) + // } + // }, + // ) + // .on_down(MouseButton::Navigate(NavigationDirection::Forward), { + // move |_, pane, cx| { + // if let Some(workspace) = pane.workspace.upgrade(cx) { + // let pane = cx.weak_handle(); + // cx.window_context().defer(move |cx| { + // workspace.update(cx, |workspace, cx| { + // workspace.go_forward(pane, cx).detach_and_log_err(cx) + // }) + // }) + // } + // } + // }) + // .into_any_named("pane") + } - // self.toolbar.update(cx, |toolbar, cx| { - // toolbar.focus_changed(true, cx); - // }); - - // if let Some(active_item) = self.active_item() { - // if cx.is_self_focused() { - // // Pane was focused directly. We need to either focus a view inside the active item, - // // or focus the active item itself - // if let Some(weak_last_focused_view) = - // self.last_focused_view_by_item.get(&active_item.id()) - // { - // if let Some(last_focused_view) = weak_last_focused_view.upgrade(cx) { - // cx.focus(&last_focused_view); - // return; - // } else { - // self.last_focused_view_by_item.remove(&active_item.id()); - // } - // } - - // cx.focus(active_item.as_any()); - // } else if focused != self.tab_bar_context_menu.handle { - // self.last_focused_view_by_item - // .insert(active_item.id(), focused.downgrade()); - // } - // } - // } - - // fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { - // self.has_focus = false; - // self.toolbar.update(cx, |toolbar, cx| { - // toolbar.focus_changed(false, cx); - // }); + // fn focus_in(&mut self, focused: AnyViewHandle, cx: &mut ViewContext) { + // if !self.has_focus { + // self.has_focus = true; + // cx.emit(Event::Focus); // cx.notify(); // } - // fn update_keymap_context(&self, keymap: &mut KeymapContext, _: &AppContext) { - // Self::reset_to_default_keymap_context(keymap); + // 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()); + // } // } // } - // impl ItemNavHistory { - // pub fn push(&mut self, data: Option, cx: &mut WindowContext) { - // self.history.push(data, self.item.clone(), cx); - // } - - // pub fn pop_backward(&mut self, cx: &mut WindowContext) -> Option { - // self.history.pop(NavigationMode::GoingBack, cx) - // } - - // pub fn pop_forward(&mut self, cx: &mut WindowContext) -> Option { - // self.history.pop(NavigationMode::GoingForward, cx) - // } + // fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { + // self.has_focus = false; + // self.toolbar.update(cx, |toolbar, cx| { + // toolbar.focus_changed(false, cx); + // }); + // cx.notify(); // } - // impl NavHistory { - // pub fn for_each_entry( - // &self, - // cx: &AppContext, - // mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option)), - // ) { - // let borrowed_history = self.0.borrow(); - // borrowed_history - // .forward_stack - // .iter() - // .chain(borrowed_history.backward_stack.iter()) - // .chain(borrowed_history.closed_stack.iter()) - // .for_each(|entry| { - // if let Some(project_and_abs_path) = - // borrowed_history.paths_by_item.get(&entry.item.id()) - // { - // f(entry, project_and_abs_path.clone()); - // } else if let Some(item) = entry.item.upgrade(cx) { - // if let Some(path) = item.project_path(cx) { - // f(entry, (path, None)); - // } - // } - // }) - // } - - // pub fn set_mode(&mut self, mode: NavigationMode) { - // self.0.borrow_mut().mode = mode; - // } - - pub fn mode(&self) -> NavigationMode { - self.0.borrow().mode - } - - // pub fn disable(&mut self) { - // self.0.borrow_mut().mode = NavigationMode::Disabled; - // } - - // pub fn enable(&mut self) { - // self.0.borrow_mut().mode = NavigationMode::Normal; - // } - - // pub fn pop(&mut self, mode: NavigationMode, cx: &mut WindowContext) -> Option { - // let mut state = self.0.borrow_mut(); - // let entry = match mode { - // NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => { - // return None - // } - // NavigationMode::GoingBack => &mut state.backward_stack, - // NavigationMode::GoingForward => &mut state.forward_stack, - // NavigationMode::ReopeningClosedItem => &mut state.closed_stack, - // } - // .pop_back(); - // if entry.is_some() { - // state.did_update(cx); - // } - // entry - // } - - // pub fn push( - // &mut self, - // data: Option, - // item: Rc, - // cx: &mut WindowContext, - // ) { - // let state = &mut *self.0.borrow_mut(); - // match state.mode { - // NavigationMode::Disabled => {} - // NavigationMode::Normal | NavigationMode::ReopeningClosedItem => { - // if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN { - // state.backward_stack.pop_front(); - // } - // state.backward_stack.push_back(NavigationEntry { - // item, - // data: data.map(|data| Box::new(data) as Box), - // timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst), - // }); - // state.forward_stack.clear(); - // } - // NavigationMode::GoingBack => { - // if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN { - // state.forward_stack.pop_front(); - // } - // state.forward_stack.push_back(NavigationEntry { - // item, - // data: data.map(|data| Box::new(data) as Box), - // timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst), - // }); - // } - // NavigationMode::GoingForward => { - // if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN { - // state.backward_stack.pop_front(); - // } - // state.backward_stack.push_back(NavigationEntry { - // item, - // data: data.map(|data| Box::new(data) as Box), - // timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst), - // }); - // } - // NavigationMode::ClosingItem => { - // if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN { - // state.closed_stack.pop_front(); - // } - // state.closed_stack.push_back(NavigationEntry { - // item, - // data: data.map(|data| Box::new(data) as Box), - // timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst), - // }); - // } - // } - // state.did_update(cx); - // } - - // pub fn remove_item(&mut self, item_id: usize) { - // let mut state = self.0.borrow_mut(); - // state.paths_by_item.remove(&item_id); - // state - // .backward_stack - // .retain(|entry| entry.item.id() != item_id); - // state - // .forward_stack - // .retain(|entry| entry.item.id() != item_id); - // state - // .closed_stack - // .retain(|entry| entry.item.id() != item_id); - // } - - // pub fn path_for_item(&self, item_id: usize) -> Option<(ProjectPath, Option)> { - // self.0.borrow().paths_by_item.get(&item_id).cloned() - // } + // fn update_keymap_context(&self, keymap: &mut KeymapContext, _: &AppContext) { + // Self::reset_to_default_keymap_context(keymap); + // } } -// 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)); -// }); -// } -// } -// } +impl ItemNavHistory { + pub fn push(&mut self, data: Option, cx: &mut WindowContext) { + self.history.push(data, self.item.clone(), cx); + } + + pub fn pop_backward(&mut self, cx: &mut WindowContext) -> Option { + self.history.pop(NavigationMode::GoingBack, cx) + } + + pub fn pop_forward(&mut self, cx: &mut WindowContext) -> Option { + self.history.pop(NavigationMode::GoingForward, cx) + } +} + +impl NavHistory { + pub fn for_each_entry( + &self, + cx: &AppContext, + mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option)), + ) { + let borrowed_history = self.0.lock(); + 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() { + if let Some(path) = item.project_path(cx) { + f(entry, (path, None)); + } + } + }) + } + + pub fn set_mode(&mut self, mode: NavigationMode) { + self.0.lock().mode = mode; + } + + pub fn mode(&self) -> NavigationMode { + self.0.lock().mode + } + + pub fn disable(&mut self) { + self.0.lock().mode = NavigationMode::Disabled; + } + + pub fn enable(&mut self) { + self.0.lock().mode = NavigationMode::Normal; + } + + pub fn pop(&mut self, mode: NavigationMode, cx: &mut WindowContext) -> Option { + let mut state = self.0.lock(); + let entry = match mode { + NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => { + return None + } + NavigationMode::GoingBack => &mut state.backward_stack, + NavigationMode::GoingForward => &mut state.forward_stack, + NavigationMode::ReopeningClosedItem => &mut state.closed_stack, + } + .pop_back(); + if entry.is_some() { + state.did_update(cx); + } + entry + } + + pub fn push( + &mut self, + data: Option, + item: Arc, + cx: &mut WindowContext, + ) { + let state = &mut *self.0.lock(); + match state.mode { + NavigationMode::Disabled => {} + NavigationMode::Normal | NavigationMode::ReopeningClosedItem => { + if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN { + state.backward_stack.pop_front(); + } + state.backward_stack.push_back(NavigationEntry { + item, + data: data.map(|data| Box::new(data) as Box), + timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst), + }); + state.forward_stack.clear(); + } + NavigationMode::GoingBack => { + if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN { + state.forward_stack.pop_front(); + } + state.forward_stack.push_back(NavigationEntry { + item, + data: data.map(|data| Box::new(data) as Box), + timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst), + }); + } + NavigationMode::GoingForward => { + if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN { + state.backward_stack.pop_front(); + } + state.backward_stack.push_back(NavigationEntry { + item, + data: data.map(|data| Box::new(data) as Box), + timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst), + }); + } + NavigationMode::ClosingItem => { + if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN { + state.closed_stack.pop_front(); + } + state.closed_stack.push_back(NavigationEntry { + item, + data: data.map(|data| Box::new(data) as Box), + timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst), + }); + } + } + state.did_update(cx); + } + + pub fn remove_item(&mut self, item_id: EntityId) { + let mut state = self.0.lock(); + 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: EntityId) -> Option<(ProjectPath, Option)> { + self.0.lock().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.defer(move |cx| { + pane.update(cx, |pane, cx| pane.history_updated(cx)); + }); + } + } +} // pub struct PaneBackdrop { // child_view: usize, @@ -2221,14 +2366,14 @@ impl Pane { // } // } -// fn dirty_message_for(buffer_path: Option) -> String { -// let path = buffer_path -// .as_ref() -// .and_then(|p| p.path.to_str()) -// .unwrap_or(&"This buffer"); -// let path = truncate_and_remove_front(path, 80); -// format!("{path} contains unsaved edits. Do you want to save it?") -// } +fn dirty_message_for(buffer_path: Option) -> String { + let path = buffer_path + .as_ref() + .and_then(|p| p.path.to_str()) + .unwrap_or(&"This buffer"); + let path = truncate_and_remove_front(path, 80); + format!("{path} contains unsaved edits. Do you want to save it?") +} // todo!("uncomment tests") // #[cfg(test)] @@ -2752,3 +2897,16 @@ impl Pane { // }) // } // } + +#[derive(Clone, Debug)] +struct DraggedTab { + title: String, +} + +impl Render for DraggedTab { + type Element = Div; + + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + div().w_8().h_4().bg(gpui2::red()) + } +} diff --git a/crates/workspace2/src/pane/dragged_item_receiver.rs b/crates/workspace2/src/pane/dragged_item_receiver.rs new file mode 100644 index 0000000000..292529e787 --- /dev/null +++ b/crates/workspace2/src/pane/dragged_item_receiver.rs @@ -0,0 +1,239 @@ +use super::DraggedItem; +use crate::{Pane, SplitDirection, Workspace}; +use gpui2::{ + color::Color, + elements::{Canvas, MouseEventHandler, ParentElement, Stack}, + geometry::{rect::RectF, vector::Vector2F}, + platform::MouseButton, + scene::MouseUp, + AppContext, Element, EventContext, MouseState, Quad, ViewContext, WeakViewHandle, +}; +use project2::ProjectEntryId; + +pub fn dragged_item_receiver( + pane: &Pane, + region_id: usize, + drop_index: usize, + allow_same_pane: bool, + split_margin: Option, + cx: &mut ViewContext, + render_child: F, +) -> MouseEventHandler +where + Tag: 'static, + D: Element, + F: FnOnce(&mut MouseState, &mut ViewContext) -> D, +{ + let drag_and_drop = cx.global::>(); + let drag_position = if (pane.can_drop)(drag_and_drop, cx) { + drag_and_drop + .currently_dragged::(cx.window()) + .map(|(drag_position, _)| drag_position) + .or_else(|| { + drag_and_drop + .currently_dragged::(cx.window()) + .map(|(drag_position, _)| drag_position) + }) + } else { + None + }; + + let mut handler = MouseEventHandler::above::(region_id, cx, |state, cx| { + // Observing hovered will cause a render when the mouse enters regardless + // of if mouse position was accessed before + let drag_position = if state.dragging() { + drag_position + } else { + None + }; + Stack::new() + .with_child(render_child(state, cx)) + .with_children(drag_position.map(|drag_position| { + Canvas::new(move |bounds, _, _, cx| { + if bounds.contains_point(drag_position) { + let overlay_region = split_margin + .and_then(|split_margin| { + drop_split_direction(drag_position, bounds, split_margin) + .map(|dir| (dir, split_margin)) + }) + .map(|(dir, margin)| dir.along_edge(bounds, margin)) + .unwrap_or(bounds); + + cx.scene().push_stacking_context(None, None); + let background = overlay_color(cx); + cx.scene().push_quad(Quad { + bounds: overlay_region, + background: Some(background), + border: Default::default(), + corner_radii: Default::default(), + }); + cx.scene().pop_stacking_context(); + } + }) + })) + }); + + if drag_position.is_some() { + handler = handler + .on_up(MouseButton::Left, { + move |event, pane, cx| { + let workspace = pane.workspace.clone(); + let pane = cx.weak_handle(); + handle_dropped_item( + event, + workspace, + &pane, + drop_index, + allow_same_pane, + split_margin, + cx, + ); + cx.notify(); + } + }) + .on_move(|_, _, cx| { + let drag_and_drop = cx.global::>(); + + if drag_and_drop + .currently_dragged::(cx.window()) + .is_some() + || drag_and_drop + .currently_dragged::(cx.window()) + .is_some() + { + cx.notify(); + } else { + cx.propagate_event(); + } + }) + } + + handler +} + +pub fn handle_dropped_item( + event: MouseUp, + workspace: WeakViewHandle, + pane: &WeakViewHandle, + index: usize, + allow_same_pane: bool, + split_margin: Option, + cx: &mut EventContext, +) { + enum Action { + Move(WeakViewHandle, usize), + Open(ProjectEntryId), + } + let drag_and_drop = cx.global::>(); + let action = if let Some((_, dragged_item)) = + drag_and_drop.currently_dragged::(cx.window()) + { + Action::Move(dragged_item.pane.clone(), dragged_item.handle.id()) + } else if let Some((_, project_entry)) = + drag_and_drop.currently_dragged::(cx.window()) + { + Action::Open(*project_entry) + } else { + cx.propagate_event(); + return; + }; + + if let Some(split_direction) = + split_margin.and_then(|margin| drop_split_direction(event.position, event.region, margin)) + { + let pane_to_split = pane.clone(); + match action { + Action::Move(from, item_id_to_move) => { + cx.window_context().defer(move |cx| { + if let Some(workspace) = workspace.upgrade(cx) { + workspace.update(cx, |workspace, cx| { + workspace.split_pane_with_item( + pane_to_split, + split_direction, + from, + item_id_to_move, + cx, + ); + }) + } + }); + } + Action::Open(project_entry) => { + cx.window_context().defer(move |cx| { + if let Some(workspace) = workspace.upgrade(cx) { + workspace.update(cx, |workspace, cx| { + if let Some(task) = workspace.split_pane_with_project_entry( + pane_to_split, + split_direction, + project_entry, + cx, + ) { + task.detach_and_log_err(cx); + } + }) + } + }); + } + }; + } else { + match action { + Action::Move(from, item_id) => { + if pane != &from || allow_same_pane { + let pane = pane.clone(); + cx.window_context().defer(move |cx| { + if let Some(((workspace, from), to)) = workspace + .upgrade(cx) + .zip(from.upgrade(cx)) + .zip(pane.upgrade(cx)) + { + workspace.update(cx, |workspace, cx| { + workspace.move_item(from, to, item_id, index, cx); + }) + } + }); + } else { + cx.propagate_event(); + } + } + Action::Open(project_entry) => { + let pane = pane.clone(); + cx.window_context().defer(move |cx| { + if let Some(workspace) = workspace.upgrade(cx) { + workspace.update(cx, |workspace, cx| { + if let Some(path) = + workspace.project.read(cx).path_for_entry(project_entry, cx) + { + workspace + .open_path(path, Some(pane), true, cx) + .detach_and_log_err(cx); + } + }); + } + }); + } + } + } +} + +fn drop_split_direction( + position: Vector2F, + region: RectF, + split_margin: f32, +) -> Option { + let mut min_direction = None; + let mut min_distance = split_margin; + for direction in SplitDirection::all() { + let edge_distance = (direction.edge(region) - direction.axis().component(position)).abs(); + + if edge_distance < min_distance { + min_direction = Some(direction); + min_distance = edge_distance; + } + } + + min_direction +} + +fn overlay_color(cx: &AppContext) -> Color { + theme2::current(cx).workspace.drop_target_overlay_color +} diff --git a/crates/workspace2/src/pane_group.rs b/crates/workspace2/src/pane_group.rs index f226f7fc43..e521c51bda 100644 --- a/crates/workspace2/src/pane_group.rs +++ b/crates/workspace2/src/pane_group.rs @@ -1,23 +1,57 @@ use crate::{AppState, FollowerState, Pane, Workspace}; -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, bail, Result}; use call2::ActiveCall; use collections::HashMap; -use gpui2::{size, AnyElement, AnyView, Bounds, Handle, Pixels, Point, View, ViewContext}; +use db2::sqlez::{ + bindable::{Bind, Column, StaticColumnCount}, + statement::Statement, +}; +use gpui2::{ + point, size, AnyElement, AnyWeakView, Bounds, Model, Pixels, Point, View, ViewContext, +}; +use parking_lot::Mutex; use project2::Project; use serde::Deserialize; -use std::{cell::RefCell, rc::Rc, sync::Arc}; -use theme2::Theme; +use std::sync::Arc; +use ui::prelude::*; const HANDLE_HITBOX_SIZE: f32 = 4.0; const HORIZONTAL_MIN_SIZE: f32 = 80.; const VERTICAL_MIN_SIZE: f32 = 100.; +#[derive(Copy, Clone, PartialEq, Eq, Debug)] pub enum Axis { Vertical, Horizontal, } -#[derive(Clone, Debug, PartialEq)] +impl StaticColumnCount for Axis {} +impl Bind for Axis { + fn bind(&self, statement: &Statement, start_index: i32) -> anyhow::Result { + match self { + Axis::Horizontal => "Horizontal", + Axis::Vertical => "Vertical", + } + .bind(statement, start_index) + } +} + +impl Column for Axis { + fn column(statement: &mut Statement, start_index: i32) -> anyhow::Result<(Self, i32)> { + String::column(statement, start_index).and_then(|(axis_text, next_index)| { + Ok(( + match axis_text.as_str() { + "Horizontal" => Axis::Horizontal, + "Vertical" => Axis::Vertical, + _ => bail!("Stored serialized item kind is incorrect"), + }, + next_index, + )) + }) + } +} + +#[derive(Clone, PartialEq)] pub struct PaneGroup { pub(crate) root: Member, } @@ -91,19 +125,17 @@ impl PaneGroup { pub(crate) fn render( &self, - project: &Handle, - theme: &Theme, + project: &Model, follower_states: &HashMap, FollowerState>, - active_call: Option<&Handle>, + active_call: Option<&Model>, active_pane: &View, - zoomed: Option<&AnyView>, + zoomed: Option<&AnyWeakView>, app_state: &Arc, cx: &mut ViewContext, - ) -> AnyElement { + ) -> impl Component { self.root.render( project, 0, - theme, follower_states, active_call, active_pane, @@ -120,7 +152,7 @@ impl PaneGroup { } } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, PartialEq)] pub(crate) enum Member { Axis(PaneAxis), Pane(View), @@ -153,17 +185,51 @@ impl Member { pub fn render( &self, - project: &Handle, + project: &Model, basis: usize, - theme: &Theme, follower_states: &HashMap, FollowerState>, - active_call: Option<&Handle>, + active_call: Option<&Model>, active_pane: &View, - zoomed: Option<&AnyView>, + zoomed: Option<&AnyWeakView>, app_state: &Arc, cx: &mut ViewContext, - ) -> AnyElement { - todo!() + ) -> impl Component { + match self { + Member::Pane(pane) => { + // todo!() + // let pane_element = if Some(pane.into()) == zoomed { + // None + // } else { + // Some(pane) + // }; + + div().child(pane.clone()).render() + + // Stack::new() + // .with_child(pane_element.contained().with_border(leader_border)) + // .with_children(leader_status_box) + // .into_any() + + // let el = div() + // .flex() + // .flex_1() + // .gap_px() + // .w_full() + // .h_full() + // .bg(cx.theme().colors().editor) + // .children(); + } + Member::Axis(axis) => axis.render( + project, + basis + 1, + follower_states, + active_call, + active_pane, + zoomed, + app_state, + cx, + ), + } // enum FollowIntoExternalProject {} @@ -305,18 +371,24 @@ impl Member { } } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone)] pub(crate) struct PaneAxis { pub axis: Axis, pub members: Vec, - pub flexes: Rc>>, - pub bounding_boxes: Rc>>>>, + pub flexes: Arc>>, + pub bounding_boxes: Arc>>>>, +} + +impl PartialEq for PaneAxis { + fn eq(&self, other: &Self) -> bool { + todo!() + } } impl PaneAxis { pub fn new(axis: Axis, members: Vec) -> Self { - let flexes = Rc::new(RefCell::new(vec![1.; members.len()])); - let bounding_boxes = Rc::new(RefCell::new(vec![None; members.len()])); + let flexes = Arc::new(Mutex::new(vec![1.; members.len()])); + let bounding_boxes = Arc::new(Mutex::new(vec![None; members.len()])); Self { axis, members, @@ -329,8 +401,8 @@ impl PaneAxis { 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()])); + let flexes = Arc::new(Mutex::new(flexes)); + let bounding_boxes = Arc::new(Mutex::new(vec![None; members.len()])); Self { axis, members, @@ -360,7 +432,7 @@ impl PaneAxis { } self.members.insert(idx, Member::Pane(new_pane.clone())); - *self.flexes.borrow_mut() = vec![1.; self.members.len()]; + *self.flexes.lock() = vec![1.; self.members.len()]; } else { *member = Member::new_axis(old_pane.clone(), new_pane.clone(), direction); @@ -400,12 +472,12 @@ impl PaneAxis { if found_pane { if let Some(idx) = remove_member { self.members.remove(idx); - *self.flexes.borrow_mut() = vec![1.; self.members.len()]; + *self.flexes.lock() = vec![1.; self.members.len()]; } if self.members.len() == 1 { let result = self.members.pop(); - *self.flexes.borrow_mut() = vec![1.; self.members.len()]; + *self.flexes.lock() = vec![1.; self.members.len()]; Ok(result) } else { Ok(None) @@ -431,13 +503,13 @@ impl PaneAxis { } fn bounding_box_for_pane(&self, pane: &View) -> Option> { - debug_assert!(self.members.len() == self.bounding_boxes.borrow().len()); + debug_assert!(self.members.len() == self.bounding_boxes.lock().len()); for (idx, member) in self.members.iter().enumerate() { match member { Member::Pane(found) => { if pane == found { - return self.bounding_boxes.borrow()[idx]; + return self.bounding_boxes.lock()[idx]; } } Member::Axis(axis) => { @@ -451,9 +523,9 @@ impl PaneAxis { } fn pane_at_pixel_position(&self, coordinate: Point) -> Option<&View> { - debug_assert!(self.members.len() == self.bounding_boxes.borrow().len()); + debug_assert!(self.members.len() == self.bounding_boxes.lock().len()); - let bounding_boxes = self.bounding_boxes.borrow(); + let bounding_boxes = self.bounding_boxes.lock(); for (idx, member) in self.members.iter().enumerate() { if let Some(coordinates) = bounding_boxes[idx] { @@ -470,17 +542,16 @@ impl PaneAxis { fn render( &self, - project: &Handle, + project: &Model, basis: usize, - theme: &Theme, follower_states: &HashMap, FollowerState>, - active_call: Option<&Handle>, + active_call: Option<&Model>, active_pane: &View, - zoomed: Option<&AnyView>, + zoomed: Option<&AnyWeakView>, app_state: &Arc, cx: &mut ViewContext, ) -> AnyElement { - debug_assert!(self.members.len() == self.flexes.borrow().len()); + debug_assert!(self.members.len() == self.flexes.lock().len()); todo!() // let mut pane_axis = PaneAxisElement::new( @@ -546,32 +617,32 @@ impl SplitDirection { [Self::Up, Self::Down, Self::Left, Self::Right] } - pub fn edge(&self, rect: Bounds) -> f32 { + pub fn edge(&self, rect: Bounds) -> Pixels { match self { - Self::Up => rect.min_y(), - Self::Down => rect.max_y(), - Self::Left => rect.min_x(), - Self::Right => rect.max_x(), + Self::Up => rect.origin.y, + Self::Down => rect.lower_left().y, + Self::Left => rect.lower_left().x, + Self::Right => rect.lower_right().x, } } pub fn along_edge(&self, bounds: Bounds, length: Pixels) -> Bounds { match self { Self::Up => Bounds { - origin: bounds.origin(), - size: size(bounds.width(), length), + origin: bounds.origin, + size: size(bounds.size.width, length), }, Self::Down => Bounds { - origin: size(bounds.min_x(), bounds.max_y() - length), - size: size(bounds.width(), length), + origin: point(bounds.lower_left().x, bounds.lower_left().y - length), + size: size(bounds.size.width, length), }, Self::Left => Bounds { - origin: bounds.origin(), - size: size(length, bounds.height()), + origin: bounds.origin, + size: size(length, bounds.size.height), }, Self::Right => Bounds { - origin: size(bounds.max_x() - length, bounds.min_y()), - size: size(length, bounds.height()), + origin: point(bounds.lower_right().x - length, bounds.lower_left().y), + size: size(length, bounds.size.height), }, } } diff --git a/crates/workspace2/src/persistence.rs b/crates/workspace2/src/persistence.rs new file mode 100644 index 0000000000..435518271d --- /dev/null +++ b/crates/workspace2/src/persistence.rs @@ -0,0 +1,973 @@ +#![allow(dead_code)] + +pub mod model; + +use std::path::Path; + +use anyhow::{anyhow, bail, Context, Result}; +use db2::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql}; +use gpui2::WindowBounds; + +use util::{unzip_option, ResultExt}; +use uuid::Uuid; + +use crate::{Axis, WorkspaceId}; + +use model::{ + GroupId, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace, + WorkspaceLocation, +}; + +use self::model::DockStructure; + +define_connection! { + // Current schema shape using pseudo-rust syntax: + // + // workspaces( + // workspace_id: usize, // Primary key for workspaces + // workspace_location: Bincode>, + // dock_visible: bool, // Deprecated + // dock_anchor: DockAnchor, // Deprecated + // dock_pane: Option, // Deprecated + // left_sidebar_open: boolean, + // timestamp: String, // UTC YYYY-MM-DD HH:MM:SS + // window_state: String, // WindowBounds Discriminant + // window_x: Option, // WindowBounds::Fixed RectF x + // window_y: Option, // WindowBounds::Fixed RectF y + // window_width: Option, // WindowBounds::Fixed RectF width + // window_height: Option, // WindowBounds::Fixed RectF height + // display: Option, // Display id + // ) + // + // pane_groups( + // group_id: usize, // Primary key for pane_groups + // workspace_id: usize, // References workspaces table + // parent_group_id: Option, // None indicates that this is the root node + // position: Optiopn, // None indicates that this is the root node + // axis: Option, // 'Vertical', 'Horizontal' + // flexes: Option>, // A JSON array of floats + // ) + // + // panes( + // pane_id: usize, // Primary key for panes + // workspace_id: usize, // References workspaces table + // active: bool, + // ) + // + // center_panes( + // pane_id: usize, // Primary key for center_panes + // parent_group_id: Option, // References pane_groups. If none, this is the root + // position: Option, // None indicates this is the root + // ) + // + // CREATE TABLE items( + // item_id: usize, // This is the item's view id, so this is not unique + // workspace_id: usize, // References workspaces table + // pane_id: usize, // References panes table + // kind: String, // Indicates which view this connects to. This is the key in the item_deserializers global + // position: usize, // Position of the item in the parent pane. This is equivalent to panes' position column + // active: bool, // Indicates if this item is the active one in the pane + // ) + pub static ref DB: WorkspaceDb<()> = + &[sql!( + CREATE TABLE workspaces( + workspace_id INTEGER PRIMARY KEY, + workspace_location BLOB UNIQUE, + dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed. + dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed. + dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed. + left_sidebar_open INTEGER, // Boolean + timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY(dock_pane) REFERENCES panes(pane_id) + ) STRICT; + + CREATE TABLE pane_groups( + group_id INTEGER PRIMARY KEY, + workspace_id INTEGER NOT NULL, + parent_group_id INTEGER, // NULL indicates that this is a root node + position INTEGER, // NULL indicates that this is a root node + axis TEXT NOT NULL, // Enum: 'Vertical' / 'Horizontal' + FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) + ON DELETE CASCADE + ON UPDATE CASCADE, + FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE + ) STRICT; + + CREATE TABLE panes( + pane_id INTEGER PRIMARY KEY, + workspace_id INTEGER NOT NULL, + active INTEGER NOT NULL, // Boolean + FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) + ON DELETE CASCADE + ON UPDATE CASCADE + ) STRICT; + + CREATE TABLE center_panes( + pane_id INTEGER PRIMARY KEY, + parent_group_id INTEGER, // NULL means that this is a root pane + position INTEGER, // NULL means that this is a root pane + FOREIGN KEY(pane_id) REFERENCES panes(pane_id) + ON DELETE CASCADE, + FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE + ) STRICT; + + CREATE TABLE items( + item_id INTEGER NOT NULL, // This is the item's view id, so this is not unique + workspace_id INTEGER NOT NULL, + pane_id INTEGER NOT NULL, + kind TEXT NOT NULL, + position INTEGER NOT NULL, + active INTEGER NOT NULL, + FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) + ON DELETE CASCADE + ON UPDATE CASCADE, + FOREIGN KEY(pane_id) REFERENCES panes(pane_id) + ON DELETE CASCADE, + PRIMARY KEY(item_id, workspace_id) + ) STRICT; + ), + sql!( + ALTER TABLE workspaces ADD COLUMN window_state TEXT; + ALTER TABLE workspaces ADD COLUMN window_x REAL; + ALTER TABLE workspaces ADD COLUMN window_y REAL; + ALTER TABLE workspaces ADD COLUMN window_width REAL; + ALTER TABLE workspaces ADD COLUMN window_height REAL; + ALTER TABLE workspaces ADD COLUMN display BLOB; + ), + // Drop foreign key constraint from workspaces.dock_pane to panes table. + sql!( + CREATE TABLE workspaces_2( + workspace_id INTEGER PRIMARY KEY, + workspace_location BLOB UNIQUE, + dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed. + dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed. + dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed. + left_sidebar_open INTEGER, // Boolean + timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, + window_state TEXT, + window_x REAL, + window_y REAL, + window_width REAL, + window_height REAL, + display BLOB + ) STRICT; + INSERT INTO workspaces_2 SELECT * FROM workspaces; + DROP TABLE workspaces; + ALTER TABLE workspaces_2 RENAME TO workspaces; + ), + // Add panels related information + sql!( + ALTER TABLE workspaces ADD COLUMN left_dock_visible INTEGER; //bool + ALTER TABLE workspaces ADD COLUMN left_dock_active_panel TEXT; + ALTER TABLE workspaces ADD COLUMN right_dock_visible INTEGER; //bool + ALTER TABLE workspaces ADD COLUMN right_dock_active_panel TEXT; + ALTER TABLE workspaces ADD COLUMN bottom_dock_visible INTEGER; //bool + ALTER TABLE workspaces ADD COLUMN bottom_dock_active_panel TEXT; + ), + // Add panel zoom persistence + sql!( + ALTER TABLE workspaces ADD COLUMN left_dock_zoom INTEGER; //bool + ALTER TABLE workspaces ADD COLUMN right_dock_zoom INTEGER; //bool + ALTER TABLE workspaces ADD COLUMN bottom_dock_zoom INTEGER; //bool + ), + // Add pane group flex data + sql!( + ALTER TABLE pane_groups ADD COLUMN flexes TEXT; + ) + ]; +} + +impl WorkspaceDb { + /// Returns a serialized workspace for the given worktree_roots. If the passed array + /// is empty, the most recent workspace is returned instead. If no workspace for the + /// passed roots is stored, returns none. + pub fn workspace_for_roots>( + &self, + worktree_roots: &[P], + ) -> Option { + let workspace_location: WorkspaceLocation = worktree_roots.into(); + + // Note that we re-assign the workspace_id here in case it's empty + // and we've grabbed the most recent workspace + let (workspace_id, workspace_location, bounds, display, docks): ( + WorkspaceId, + WorkspaceLocation, + Option, + Option, + DockStructure, + ) = self + .select_row_bound(sql! { + SELECT + workspace_id, + workspace_location, + window_state, + window_x, + window_y, + window_width, + window_height, + display, + left_dock_visible, + left_dock_active_panel, + left_dock_zoom, + right_dock_visible, + right_dock_active_panel, + right_dock_zoom, + bottom_dock_visible, + bottom_dock_active_panel, + bottom_dock_zoom + FROM workspaces + WHERE workspace_location = ? + }) + .and_then(|mut prepared_statement| (prepared_statement)(&workspace_location)) + .context("No workspaces found") + .warn_on_err() + .flatten()?; + + Some(SerializedWorkspace { + id: workspace_id, + location: workspace_location.clone(), + center_group: self + .get_center_pane_group(workspace_id) + .context("Getting center group") + .log_err()?, + bounds, + display, + docks, + }) + } + + /// Saves a workspace using the worktree roots. Will garbage collect any workspaces + /// that used this workspace previously + pub async fn save_workspace(&self, workspace: SerializedWorkspace) { + self.write(move |conn| { + conn.with_savepoint("update_worktrees", || { + // Clear out panes and pane_groups + conn.exec_bound(sql!( + DELETE FROM pane_groups WHERE workspace_id = ?1; + DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id) + .expect("Clearing old panes"); + + conn.exec_bound(sql!( + DELETE FROM workspaces WHERE workspace_location = ? AND workspace_id != ? + ))?((&workspace.location, workspace.id.clone())) + .context("clearing out old locations")?; + + // Upsert + conn.exec_bound(sql!( + INSERT INTO workspaces( + workspace_id, + workspace_location, + left_dock_visible, + left_dock_active_panel, + left_dock_zoom, + right_dock_visible, + right_dock_active_panel, + right_dock_zoom, + bottom_dock_visible, + bottom_dock_active_panel, + bottom_dock_zoom, + timestamp + ) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, CURRENT_TIMESTAMP) + ON CONFLICT DO + UPDATE SET + workspace_location = ?2, + left_dock_visible = ?3, + left_dock_active_panel = ?4, + left_dock_zoom = ?5, + right_dock_visible = ?6, + right_dock_active_panel = ?7, + right_dock_zoom = ?8, + bottom_dock_visible = ?9, + bottom_dock_active_panel = ?10, + bottom_dock_zoom = ?11, + timestamp = CURRENT_TIMESTAMP + ))?((workspace.id, &workspace.location, workspace.docks)) + .context("Updating workspace")?; + + // Save center pane group + Self::save_pane_group(conn, workspace.id, &workspace.center_group, None) + .context("save pane group in save workspace")?; + + Ok(()) + }) + .log_err(); + }) + .await; + } + + query! { + pub async fn next_id() -> Result { + INSERT INTO workspaces DEFAULT VALUES RETURNING workspace_id + } + } + + query! { + fn recent_workspaces() -> Result> { + SELECT workspace_id, workspace_location + FROM workspaces + WHERE workspace_location IS NOT NULL + ORDER BY timestamp DESC + } + } + + query! { + async fn delete_stale_workspace(id: WorkspaceId) -> Result<()> { + DELETE FROM workspaces + WHERE workspace_id IS ? + } + } + + // Returns the recent locations which are still valid on disk and deletes ones which no longer + // exist. + pub async fn recent_workspaces_on_disk(&self) -> Result> { + let mut result = Vec::new(); + let mut delete_tasks = Vec::new(); + for (id, location) in self.recent_workspaces()? { + if location.paths().iter().all(|path| path.exists()) + && location.paths().iter().any(|path| path.is_dir()) + { + result.push((id, location)); + } else { + delete_tasks.push(self.delete_stale_workspace(id)); + } + } + + futures::future::join_all(delete_tasks).await; + Ok(result) + } + + pub async fn last_workspace(&self) -> Result> { + Ok(self + .recent_workspaces_on_disk() + .await? + .into_iter() + .next() + .map(|(_, location)| location)) + } + + fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result { + Ok(self + .get_pane_group(workspace_id, None)? + .into_iter() + .next() + .unwrap_or_else(|| { + SerializedPaneGroup::Pane(SerializedPane { + active: true, + children: vec![], + }) + })) + } + + fn get_pane_group( + &self, + workspace_id: WorkspaceId, + group_id: Option, + ) -> Result> { + type GroupKey = (Option, WorkspaceId); + type GroupOrPane = ( + Option, + Option, + Option, + Option, + Option, + ); + self.select_bound::(sql!( + SELECT group_id, axis, pane_id, active, flexes + FROM (SELECT + group_id, + axis, + NULL as pane_id, + NULL as active, + position, + parent_group_id, + workspace_id, + flexes + FROM pane_groups + UNION + SELECT + NULL, + NULL, + center_panes.pane_id, + panes.active as active, + position, + parent_group_id, + panes.workspace_id as workspace_id, + NULL + FROM center_panes + JOIN panes ON center_panes.pane_id = panes.pane_id) + WHERE parent_group_id IS ? AND workspace_id = ? + ORDER BY position + ))?((group_id, workspace_id))? + .into_iter() + .map(|(group_id, axis, pane_id, active, flexes)| { + if let Some((group_id, axis)) = group_id.zip(axis) { + let flexes = flexes + .map(|flexes| serde_json::from_str::>(&flexes)) + .transpose()?; + + Ok(SerializedPaneGroup::Group { + axis, + children: self.get_pane_group(workspace_id, Some(group_id))?, + flexes, + }) + } else if let Some((pane_id, active)) = pane_id.zip(active) { + Ok(SerializedPaneGroup::Pane(SerializedPane::new( + self.get_items(pane_id)?, + active, + ))) + } else { + bail!("Pane Group Child was neither a pane group or a pane"); + } + }) + // Filter out panes and pane groups which don't have any children or items + .filter(|pane_group| match pane_group { + Ok(SerializedPaneGroup::Group { children, .. }) => !children.is_empty(), + Ok(SerializedPaneGroup::Pane(pane)) => !pane.children.is_empty(), + _ => true, + }) + .collect::>() + } + + fn save_pane_group( + conn: &Connection, + workspace_id: WorkspaceId, + pane_group: &SerializedPaneGroup, + parent: Option<(GroupId, usize)>, + ) -> Result<()> { + match pane_group { + SerializedPaneGroup::Group { + axis, + children, + flexes, + } => { + let (parent_id, position) = unzip_option(parent); + + let flex_string = flexes + .as_ref() + .map(|flexes| serde_json::json!(flexes).to_string()); + + let group_id = conn.select_row_bound::<_, i64>(sql!( + INSERT INTO pane_groups( + workspace_id, + parent_group_id, + position, + axis, + flexes + ) + VALUES (?, ?, ?, ?, ?) + RETURNING group_id + ))?(( + workspace_id, + parent_id, + position, + *axis, + flex_string, + ))? + .ok_or_else(|| anyhow!("Couldn't retrieve group_id from inserted pane_group"))?; + + for (position, group) in children.iter().enumerate() { + Self::save_pane_group(conn, workspace_id, group, Some((group_id, position)))? + } + + Ok(()) + } + SerializedPaneGroup::Pane(pane) => { + Self::save_pane(conn, workspace_id, &pane, parent)?; + Ok(()) + } + } + } + + fn save_pane( + conn: &Connection, + workspace_id: WorkspaceId, + pane: &SerializedPane, + parent: Option<(GroupId, usize)>, + ) -> Result { + let pane_id = conn.select_row_bound::<_, i64>(sql!( + INSERT INTO panes(workspace_id, active) + VALUES (?, ?) + RETURNING pane_id + ))?((workspace_id, pane.active))? + .ok_or_else(|| anyhow!("Could not retrieve inserted pane_id"))?; + + let (parent_id, order) = unzip_option(parent); + conn.exec_bound(sql!( + INSERT INTO center_panes(pane_id, parent_group_id, position) + VALUES (?, ?, ?) + ))?((pane_id, parent_id, order))?; + + Self::save_items(conn, workspace_id, pane_id, &pane.children).context("Saving items")?; + + Ok(pane_id) + } + + fn get_items(&self, pane_id: PaneId) -> Result> { + Ok(self.select_bound(sql!( + SELECT kind, item_id, active FROM items + WHERE pane_id = ? + ORDER BY position + ))?(pane_id)?) + } + + fn save_items( + conn: &Connection, + workspace_id: WorkspaceId, + pane_id: PaneId, + items: &[SerializedItem], + ) -> Result<()> { + let mut insert = conn.exec_bound(sql!( + INSERT INTO items(workspace_id, pane_id, position, kind, item_id, active) VALUES (?, ?, ?, ?, ?, ?) + )).context("Preparing insertion")?; + for (position, item) in items.iter().enumerate() { + insert((workspace_id, pane_id, position, item))?; + } + + Ok(()) + } + + query! { + pub async fn update_timestamp(workspace_id: WorkspaceId) -> Result<()> { + UPDATE workspaces + SET timestamp = CURRENT_TIMESTAMP + WHERE workspace_id = ? + } + } + + query! { + pub async fn set_window_bounds(workspace_id: WorkspaceId, bounds: WindowBounds, display: Uuid) -> Result<()> { + UPDATE workspaces + SET window_state = ?2, + window_x = ?3, + window_y = ?4, + window_width = ?5, + window_height = ?6, + display = ?7 + WHERE workspace_id = ?1 + } + } +} + +// todo!() +// #[cfg(test)] +// mod tests { +// use super::*; +// use db::open_test_db; + +// #[gpui::test] +// async fn test_next_id_stability() { +// env_logger::try_init().ok(); + +// let db = WorkspaceDb(open_test_db("test_next_id_stability").await); + +// db.write(|conn| { +// conn.migrate( +// "test_table", +// &[sql!( +// CREATE TABLE test_table( +// text TEXT, +// workspace_id INTEGER, +// FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) +// ON DELETE CASCADE +// ) STRICT; +// )], +// ) +// .unwrap(); +// }) +// .await; + +// let id = db.next_id().await.unwrap(); +// // Assert the empty row got inserted +// assert_eq!( +// Some(id), +// db.select_row_bound::(sql!( +// SELECT workspace_id FROM workspaces WHERE workspace_id = ? +// )) +// .unwrap()(id) +// .unwrap() +// ); + +// db.write(move |conn| { +// conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?))) +// .unwrap()(("test-text-1", id)) +// .unwrap() +// }) +// .await; + +// let test_text_1 = db +// .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?)) +// .unwrap()(1) +// .unwrap() +// .unwrap(); +// assert_eq!(test_text_1, "test-text-1"); +// } + +// #[gpui::test] +// async fn test_workspace_id_stability() { +// env_logger::try_init().ok(); + +// let db = WorkspaceDb(open_test_db("test_workspace_id_stability").await); + +// db.write(|conn| { +// conn.migrate( +// "test_table", +// &[sql!( +// CREATE TABLE test_table( +// text TEXT, +// workspace_id INTEGER, +// FOREIGN KEY(workspace_id) +// REFERENCES workspaces(workspace_id) +// ON DELETE CASCADE +// ) STRICT;)], +// ) +// }) +// .await +// .unwrap(); + +// let mut workspace_1 = SerializedWorkspace { +// id: 1, +// location: (["/tmp", "/tmp2"]).into(), +// center_group: Default::default(), +// bounds: Default::default(), +// display: Default::default(), +// docks: Default::default(), +// }; + +// let workspace_2 = SerializedWorkspace { +// id: 2, +// location: (["/tmp"]).into(), +// center_group: Default::default(), +// bounds: Default::default(), +// display: Default::default(), +// docks: Default::default(), +// }; + +// db.save_workspace(workspace_1.clone()).await; + +// db.write(|conn| { +// conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?))) +// .unwrap()(("test-text-1", 1)) +// .unwrap(); +// }) +// .await; + +// db.save_workspace(workspace_2.clone()).await; + +// db.write(|conn| { +// conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?))) +// .unwrap()(("test-text-2", 2)) +// .unwrap(); +// }) +// .await; + +// workspace_1.location = (["/tmp", "/tmp3"]).into(); +// db.save_workspace(workspace_1.clone()).await; +// db.save_workspace(workspace_1).await; +// db.save_workspace(workspace_2).await; + +// let test_text_2 = db +// .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?)) +// .unwrap()(2) +// .unwrap() +// .unwrap(); +// assert_eq!(test_text_2, "test-text-2"); + +// let test_text_1 = db +// .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?)) +// .unwrap()(1) +// .unwrap() +// .unwrap(); +// assert_eq!(test_text_1, "test-text-1"); +// } + +// fn group(axis: gpui::Axis, children: Vec) -> SerializedPaneGroup { +// SerializedPaneGroup::Group { +// axis, +// flexes: None, +// children, +// } +// } + +// #[gpui::test] +// async fn test_full_workspace_serialization() { +// env_logger::try_init().ok(); + +// let db = WorkspaceDb(open_test_db("test_full_workspace_serialization").await); + +// // ----------------- +// // | 1,2 | 5,6 | +// // | - - - | | +// // | 3,4 | | +// // ----------------- +// let center_group = group( +// gpui::Axis::Horizontal, +// vec![ +// group( +// gpui::Axis::Vertical, +// vec![ +// SerializedPaneGroup::Pane(SerializedPane::new( +// vec![ +// SerializedItem::new("Terminal", 5, false), +// SerializedItem::new("Terminal", 6, true), +// ], +// false, +// )), +// SerializedPaneGroup::Pane(SerializedPane::new( +// vec![ +// SerializedItem::new("Terminal", 7, true), +// SerializedItem::new("Terminal", 8, false), +// ], +// false, +// )), +// ], +// ), +// SerializedPaneGroup::Pane(SerializedPane::new( +// vec![ +// SerializedItem::new("Terminal", 9, false), +// SerializedItem::new("Terminal", 10, true), +// ], +// false, +// )), +// ], +// ); + +// let workspace = SerializedWorkspace { +// id: 5, +// location: (["/tmp", "/tmp2"]).into(), +// center_group, +// bounds: Default::default(), +// display: Default::default(), +// docks: Default::default(), +// }; + +// db.save_workspace(workspace.clone()).await; +// let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]); + +// assert_eq!(workspace, round_trip_workspace.unwrap()); + +// // Test guaranteed duplicate IDs +// db.save_workspace(workspace.clone()).await; +// db.save_workspace(workspace.clone()).await; + +// let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]); +// assert_eq!(workspace, round_trip_workspace.unwrap()); +// } + +// #[gpui::test] +// async fn test_workspace_assignment() { +// env_logger::try_init().ok(); + +// let db = WorkspaceDb(open_test_db("test_basic_functionality").await); + +// let workspace_1 = SerializedWorkspace { +// id: 1, +// location: (["/tmp", "/tmp2"]).into(), +// center_group: Default::default(), +// bounds: Default::default(), +// display: Default::default(), +// docks: Default::default(), +// }; + +// let mut workspace_2 = SerializedWorkspace { +// id: 2, +// location: (["/tmp"]).into(), +// center_group: Default::default(), +// bounds: Default::default(), +// display: Default::default(), +// docks: Default::default(), +// }; + +// db.save_workspace(workspace_1.clone()).await; +// db.save_workspace(workspace_2.clone()).await; + +// // Test that paths are treated as a set +// assert_eq!( +// db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(), +// workspace_1 +// ); +// assert_eq!( +// db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(), +// workspace_1 +// ); + +// // Make sure that other keys work +// assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2); +// assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None); + +// // Test 'mutate' case of updating a pre-existing id +// workspace_2.location = (["/tmp", "/tmp2"]).into(); + +// db.save_workspace(workspace_2.clone()).await; +// assert_eq!( +// db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(), +// workspace_2 +// ); + +// // Test other mechanism for mutating +// let mut workspace_3 = SerializedWorkspace { +// id: 3, +// location: (&["/tmp", "/tmp2"]).into(), +// center_group: Default::default(), +// bounds: Default::default(), +// display: Default::default(), +// docks: Default::default(), +// }; + +// db.save_workspace(workspace_3.clone()).await; +// assert_eq!( +// db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(), +// workspace_3 +// ); + +// // Make sure that updating paths differently also works +// workspace_3.location = (["/tmp3", "/tmp4", "/tmp2"]).into(); +// db.save_workspace(workspace_3.clone()).await; +// assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None); +// assert_eq!( +// db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"]) +// .unwrap(), +// workspace_3 +// ); +// } + +// use crate::persistence::model::SerializedWorkspace; +// use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup}; + +// fn default_workspace>( +// workspace_id: &[P], +// center_group: &SerializedPaneGroup, +// ) -> SerializedWorkspace { +// SerializedWorkspace { +// id: 4, +// location: workspace_id.into(), +// center_group: center_group.clone(), +// bounds: Default::default(), +// display: Default::default(), +// docks: Default::default(), +// } +// } + +// #[gpui::test] +// async fn test_simple_split() { +// env_logger::try_init().ok(); + +// let db = WorkspaceDb(open_test_db("simple_split").await); + +// // ----------------- +// // | 1,2 | 5,6 | +// // | - - - | | +// // | 3,4 | | +// // ----------------- +// let center_pane = group( +// gpui::Axis::Horizontal, +// vec![ +// group( +// gpui::Axis::Vertical, +// vec![ +// SerializedPaneGroup::Pane(SerializedPane::new( +// vec![ +// SerializedItem::new("Terminal", 1, false), +// SerializedItem::new("Terminal", 2, true), +// ], +// false, +// )), +// SerializedPaneGroup::Pane(SerializedPane::new( +// vec![ +// SerializedItem::new("Terminal", 4, false), +// SerializedItem::new("Terminal", 3, true), +// ], +// true, +// )), +// ], +// ), +// SerializedPaneGroup::Pane(SerializedPane::new( +// vec![ +// SerializedItem::new("Terminal", 5, true), +// SerializedItem::new("Terminal", 6, false), +// ], +// false, +// )), +// ], +// ); + +// let workspace = default_workspace(&["/tmp"], ¢er_pane); + +// db.save_workspace(workspace.clone()).await; + +// let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap(); + +// assert_eq!(workspace.center_group, new_workspace.center_group); +// } + +// #[gpui::test] +// async fn test_cleanup_panes() { +// env_logger::try_init().ok(); + +// let db = WorkspaceDb(open_test_db("test_cleanup_panes").await); + +// let center_pane = group( +// gpui::Axis::Horizontal, +// vec![ +// group( +// gpui::Axis::Vertical, +// vec![ +// SerializedPaneGroup::Pane(SerializedPane::new( +// vec![ +// SerializedItem::new("Terminal", 1, false), +// SerializedItem::new("Terminal", 2, true), +// ], +// false, +// )), +// SerializedPaneGroup::Pane(SerializedPane::new( +// vec![ +// SerializedItem::new("Terminal", 4, false), +// SerializedItem::new("Terminal", 3, true), +// ], +// true, +// )), +// ], +// ), +// SerializedPaneGroup::Pane(SerializedPane::new( +// vec![ +// SerializedItem::new("Terminal", 5, false), +// SerializedItem::new("Terminal", 6, true), +// ], +// false, +// )), +// ], +// ); + +// let id = &["/tmp"]; + +// let mut workspace = default_workspace(id, ¢er_pane); + +// db.save_workspace(workspace.clone()).await; + +// workspace.center_group = group( +// gpui::Axis::Vertical, +// vec![ +// SerializedPaneGroup::Pane(SerializedPane::new( +// vec![ +// SerializedItem::new("Terminal", 1, false), +// SerializedItem::new("Terminal", 2, true), +// ], +// false, +// )), +// SerializedPaneGroup::Pane(SerializedPane::new( +// vec![ +// SerializedItem::new("Terminal", 4, true), +// SerializedItem::new("Terminal", 3, false), +// ], +// true, +// )), +// ], +// ); + +// db.save_workspace(workspace.clone()).await; + +// let new_workspace = db.workspace_for_roots(id).unwrap(); + +// assert_eq!(workspace.center_group, new_workspace.center_group); +// } +// } diff --git a/crates/workspace2/src/persistence/model.rs b/crates/workspace2/src/persistence/model.rs index 2e28dabffb..de4518f68e 100644 --- a/crates/workspace2/src/persistence/model.rs +++ b/crates/workspace2/src/persistence/model.rs @@ -7,7 +7,7 @@ use db2::sqlez::{ bindable::{Bind, Column, StaticColumnCount}, statement::Statement, }; -use gpui2::{AsyncAppContext, Handle, Task, View, WeakView, WindowBounds}; +use gpui2::{AsyncWindowContext, Model, Task, View, WeakView, WindowBounds}; use project2::Project; use std::{ path::{Path, PathBuf}, @@ -55,7 +55,7 @@ impl Column for WorkspaceLocation { } } -#[derive(Debug, PartialEq, Clone)] +#[derive(PartialEq, Clone)] pub struct SerializedWorkspace { pub id: WorkspaceId, pub location: WorkspaceLocation, @@ -127,7 +127,7 @@ impl Bind for DockData { } } -#[derive(Debug, PartialEq, Clone)] +#[derive(PartialEq, Clone)] pub enum SerializedPaneGroup { Group { axis: Axis, @@ -151,10 +151,10 @@ impl SerializedPaneGroup { #[async_recursion(?Send)] pub(crate) async fn deserialize( self, - project: &Handle, + project: &Model, workspace_id: WorkspaceId, - workspace: &WeakView, - cx: &mut AsyncAppContext, + workspace: WeakView, + cx: &mut AsyncWindowContext, ) -> Option<(Member, Option>, Vec>>)> { match self { SerializedPaneGroup::Group { @@ -167,7 +167,7 @@ impl SerializedPaneGroup { 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) + .deserialize(project, workspace_id, workspace.clone(), cx) .await { members.push(new_member); @@ -196,14 +196,11 @@ impl SerializedPaneGroup { .log_err()?; let active = serialized_pane.active; let new_items = serialized_pane - .deserialize_to(project, &pane, workspace_id, workspace, cx) + .deserialize_to(project, &pane, workspace_id, workspace.clone(), cx) .await .log_err()?; - if pane - .read_with(cx, |pane, _| pane.items_len() != 0) - .log_err()? - { + if pane.update(cx, |pane, _| pane.items_len() != 0).log_err()? { let pane = pane.upgrade()?; Some((Member::Pane(pane.clone()), active.then(|| pane), new_items)) } else { @@ -231,11 +228,11 @@ impl SerializedPane { pub async fn deserialize_to( &self, - project: &Handle, + project: &Model, pane: &WeakView, workspace_id: WorkspaceId, - workspace: &WeakView, - cx: &mut AsyncAppContext, + workspace: WeakView, + cx: &mut AsyncWindowContext, ) -> Result>>> { let mut items = Vec::new(); let mut active_item_index = None; @@ -289,15 +286,15 @@ pub struct SerializedItem { pub active: bool, } -impl SerializedItem { - pub fn new(kind: impl AsRef, item_id: ItemId, active: bool) -> Self { - Self { - kind: Arc::from(kind.as_ref()), - item_id, - active, - } - } -} +// impl SerializedItem { +// pub fn new(kind: impl AsRef, item_id: ItemId, active: bool) -> Self { +// Self { +// kind: Arc::from(kind.as_ref()), +// item_id, +// active, +// } +// } +// } #[cfg(test)] impl Default for SerializedItem { diff --git a/crates/workspace2/src/searchable.rs b/crates/workspace2/src/searchable.rs new file mode 100644 index 0000000000..3935423635 --- /dev/null +++ b/crates/workspace2/src/searchable.rs @@ -0,0 +1,285 @@ +use std::{any::Any, sync::Arc}; + +use gpui2::{AnyView, AppContext, Subscription, Task, View, ViewContext, WindowContext}; +use project2::search::SearchQuery; + +use crate::{ + item::{Item, WeakItemHandle}, + ItemHandle, +}; + +#[derive(Debug)] +pub enum SearchEvent { + MatchesInvalidated, + ActiveMatchChanged, +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Direction { + Prev, + Next, +} + +#[derive(Clone, Copy, Debug, Default)] +pub struct SearchOptions { + pub case: bool, + pub word: bool, + pub regex: bool, + /// Specifies whether the item supports search & replace. + pub replacement: bool, +} + +pub trait SearchableItem: Item { + type Match: Any + Sync + Send + Clone; + + fn supported_options() -> SearchOptions { + SearchOptions { + case: true, + word: true, + regex: true, + replacement: true, + } + } + fn to_search_event( + &mut self, + event: &Self::Event, + cx: &mut ViewContext, + ) -> Option; + fn clear_matches(&mut self, cx: &mut ViewContext); + fn update_matches(&mut self, matches: Vec, cx: &mut ViewContext); + fn query_suggestion(&mut self, cx: &mut ViewContext) -> String; + fn activate_match( + &mut self, + index: usize, + matches: Vec, + cx: &mut ViewContext, + ); + fn select_matches(&mut self, matches: Vec, cx: &mut ViewContext); + fn replace(&mut self, _: &Self::Match, _: &SearchQuery, _: &mut ViewContext); + fn match_index_for_direction( + &mut self, + matches: &Vec, + current_index: usize, + direction: Direction, + count: usize, + _: &mut ViewContext, + ) -> usize { + match direction { + Direction::Prev => { + let count = count % matches.len(); + if current_index >= count { + current_index - count + } else { + matches.len() - (count - current_index) + } + } + Direction::Next => (current_index + count) % matches.len(), + } + } + fn find_matches( + &mut self, + query: Arc, + cx: &mut ViewContext, + ) -> Task>; + fn active_match_index( + &mut self, + matches: Vec, + cx: &mut ViewContext, + ) -> Option; +} + +pub trait SearchableItemHandle: ItemHandle { + fn downgrade(&self) -> Box; + fn boxed_clone(&self) -> Box; + fn supported_options(&self) -> SearchOptions; + fn subscribe_to_search_events( + &self, + cx: &mut WindowContext, + handler: Box, + ) -> Subscription; + fn clear_matches(&self, cx: &mut WindowContext); + fn update_matches(&self, matches: &Vec>, cx: &mut WindowContext); + fn query_suggestion(&self, cx: &mut WindowContext) -> String; + fn activate_match( + &self, + index: usize, + matches: &Vec>, + cx: &mut WindowContext, + ); + fn select_matches(&self, matches: &Vec>, cx: &mut WindowContext); + fn replace(&self, _: &Box, _: &SearchQuery, _: &mut WindowContext); + fn match_index_for_direction( + &self, + matches: &Vec>, + current_index: usize, + direction: Direction, + count: usize, + cx: &mut WindowContext, + ) -> usize; + fn find_matches( + &self, + query: Arc, + cx: &mut WindowContext, + ) -> Task>>; + fn active_match_index( + &self, + matches: &Vec>, + cx: &mut WindowContext, + ) -> Option; +} + +// todo!("here is where we need to use AnyWeakView"); +impl SearchableItemHandle for View { + fn downgrade(&self) -> Box { + // Box::new(self.downgrade()) + todo!() + } + + fn boxed_clone(&self) -> Box { + Box::new(self.clone()) + } + + fn supported_options(&self) -> SearchOptions { + T::supported_options() + } + + fn subscribe_to_search_events( + &self, + cx: &mut WindowContext, + handler: Box, + ) -> Subscription { + cx.subscribe(self, move |handle, event, cx| { + let search_event = handle.update(cx, |handle, cx| handle.to_search_event(event, cx)); + if let Some(search_event) = search_event { + handler(search_event, cx) + } + }) + } + + fn clear_matches(&self, cx: &mut WindowContext) { + self.update(cx, |this, cx| this.clear_matches(cx)); + } + fn update_matches(&self, matches: &Vec>, cx: &mut WindowContext) { + let matches = downcast_matches(matches); + self.update(cx, |this, cx| this.update_matches(matches, cx)); + } + fn query_suggestion(&self, cx: &mut WindowContext) -> String { + self.update(cx, |this, cx| this.query_suggestion(cx)) + } + fn activate_match( + &self, + index: usize, + matches: &Vec>, + cx: &mut WindowContext, + ) { + let matches = downcast_matches(matches); + self.update(cx, |this, cx| this.activate_match(index, matches, cx)); + } + + fn select_matches(&self, matches: &Vec>, cx: &mut WindowContext) { + let matches = downcast_matches(matches); + self.update(cx, |this, cx| this.select_matches(matches, cx)); + } + + fn match_index_for_direction( + &self, + matches: &Vec>, + current_index: usize, + direction: Direction, + count: usize, + cx: &mut WindowContext, + ) -> usize { + let matches = downcast_matches(matches); + self.update(cx, |this, cx| { + this.match_index_for_direction(&matches, current_index, direction, count, cx) + }) + } + fn find_matches( + &self, + query: Arc, + cx: &mut WindowContext, + ) -> Task>> { + let matches = self.update(cx, |this, cx| this.find_matches(query, cx)); + cx.spawn(|cx| async { + let matches = matches.await; + matches + .into_iter() + .map::, _>(|range| Box::new(range)) + .collect() + }) + } + fn active_match_index( + &self, + matches: &Vec>, + cx: &mut WindowContext, + ) -> Option { + let matches = downcast_matches(matches); + self.update(cx, |this, cx| this.active_match_index(matches, cx)) + } + + fn replace(&self, matches: &Box, query: &SearchQuery, cx: &mut WindowContext) { + let matches = matches.downcast_ref().unwrap(); + self.update(cx, |this, cx| this.replace(matches, query, cx)) + } +} + +fn downcast_matches(matches: &Vec>) -> Vec { + matches + .iter() + .map(|range| range.downcast_ref::().cloned()) + .collect::>>() + .expect( + "SearchableItemHandle function called with vec of matches of a different type than expected", + ) +} + +impl From> for AnyView { + fn from(this: Box) -> Self { + this.to_any().clone() + } +} + +impl From<&Box> for AnyView { + fn from(this: &Box) -> Self { + this.to_any().clone() + } +} + +impl PartialEq for Box { + fn eq(&self, other: &Self) -> bool { + self.id() == other.id() + } +} + +impl Eq for Box {} + +pub trait WeakSearchableItemHandle: WeakItemHandle { + fn upgrade(&self, cx: &AppContext) -> Option>; + + // fn into_any(self) -> AnyWeakView; +} + +// todo!() +// impl WeakSearchableItemHandle for WeakView { +// fn upgrade(&self, cx: &AppContext) -> Option> { +// Some(Box::new(self.upgrade(cx)?)) +// } + +// // fn into_any(self) -> AnyView { +// // self.into_any() +// // } +// } + +impl PartialEq for Box { + fn eq(&self, other: &Self) -> bool { + self.id() == other.id() + } +} + +impl Eq for Box {} + +impl std::hash::Hash for Box { + fn hash(&self, state: &mut H) { + self.id().hash(state) + } +} diff --git a/crates/workspace2/src/shared_screen.rs b/crates/workspace2/src/shared_screen.rs new file mode 100644 index 0000000000..b99c5f3ab9 --- /dev/null +++ b/crates/workspace2/src/shared_screen.rs @@ -0,0 +1,151 @@ +use crate::{ + item::{Item, ItemEvent}, + ItemNavHistory, WorkspaceId, +}; +use anyhow::Result; +use call::participant::{Frame, RemoteVideoTrack}; +use client::{proto::PeerId, User}; +use futures::StreamExt; +use gpui::{ + elements::*, + geometry::{rect::RectF, vector::vec2f}, + platform::MouseButton, + AppContext, Entity, Task, View, ViewContext, +}; +use smallvec::SmallVec; +use std::{ + borrow::Cow, + sync::{Arc, Weak}, +}; + +pub enum Event { + Close, +} + +pub struct SharedScreen { + track: Weak, + frame: Option, + pub peer_id: PeerId, + user: Arc, + nav_history: Option, + _maintain_frame: Task>, +} + +impl SharedScreen { + pub fn new( + track: &Arc, + peer_id: PeerId, + user: Arc, + cx: &mut ViewContext, + ) -> Self { + let mut frames = track.frames(); + Self { + track: Arc::downgrade(track), + frame: None, + peer_id, + user, + nav_history: Default::default(), + _maintain_frame: cx.spawn(|this, mut cx| async move { + while let Some(frame) = frames.next().await { + this.update(&mut cx, |this, cx| { + this.frame = Some(frame); + cx.notify(); + })?; + } + this.update(&mut cx, |_, cx| cx.emit(Event::Close))?; + Ok(()) + }), + } + } +} + +impl Entity for SharedScreen { + type Event = Event; +} + +impl View for SharedScreen { + fn ui_name() -> &'static str { + "SharedScreen" + } + + fn render(&mut self, cx: &mut ViewContext) -> AnyElement { + enum Focus {} + + let frame = self.frame.clone(); + MouseEventHandler::new::(0, cx, |_, cx| { + Canvas::new(move |bounds, _, _, cx| { + if let Some(frame) = frame.clone() { + let size = constrain_size_preserving_aspect_ratio( + bounds.size(), + vec2f(frame.width() as f32, frame.height() as f32), + ); + let origin = bounds.origin() + (bounds.size() / 2.) - size / 2.; + cx.scene().push_surface(gpui::platform::mac::Surface { + bounds: RectF::new(origin, size), + image_buffer: frame.image(), + }); + } + }) + .contained() + .with_style(theme::current(cx).shared_screen) + }) + .on_down(MouseButton::Left, |_, _, cx| cx.focus_parent()) + .into_any() + } +} + +impl Item for SharedScreen { + fn tab_tooltip_text(&self, _: &AppContext) -> Option> { + Some(format!("{}'s screen", self.user.github_login).into()) + } + fn deactivated(&mut self, cx: &mut ViewContext) { + if let Some(nav_history) = self.nav_history.as_mut() { + nav_history.push::<()>(None, cx); + } + } + + fn tab_content( + &self, + _: Option, + style: &theme::Tab, + _: &AppContext, + ) -> gpui::AnyElement { + Flex::row() + .with_child( + Svg::new("icons/desktop.svg") + .with_color(style.label.text.color) + .constrained() + .with_width(style.type_icon_width) + .aligned() + .contained() + .with_margin_right(style.spacing), + ) + .with_child( + Label::new( + format!("{}'s screen", self.user.github_login), + style.label.clone(), + ) + .aligned(), + ) + .into_any() + } + + fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext) { + self.nav_history = Some(history); + } + + fn clone_on_split( + &self, + _workspace_id: WorkspaceId, + cx: &mut ViewContext, + ) -> Option { + let track = self.track.upgrade()?; + Some(Self::new(&track, self.peer_id, self.user.clone(), cx)) + } + + fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> { + match event { + Event::Close => smallvec::smallvec!(ItemEvent::CloseItem), + } + } +} diff --git a/crates/workspace2/src/status_bar.rs b/crates/workspace2/src/status_bar.rs new file mode 100644 index 0000000000..c2f78d9ad6 --- /dev/null +++ b/crates/workspace2/src/status_bar.rs @@ -0,0 +1,301 @@ +use std::any::TypeId; + +use crate::{ItemHandle, Pane}; +use gpui2::{ + div, AnyView, Component, Div, ParentElement, Render, Styled, Subscription, View, ViewContext, + WindowContext, +}; +use theme2::ActiveTheme; +use util::ResultExt; + +pub trait StatusItemView: Render { + fn set_active_pane_item( + &mut self, + active_pane_item: Option<&dyn crate::ItemHandle>, + cx: &mut ViewContext, + ); +} + +trait StatusItemViewHandle: Send { + fn to_any(&self) -> AnyView; + fn set_active_pane_item( + &self, + active_pane_item: Option<&dyn ItemHandle>, + cx: &mut WindowContext, + ); + fn item_type(&self) -> TypeId; +} + +pub struct StatusBar { + left_items: Vec>, + right_items: Vec>, + active_pane: View, + _observe_active_pane: Subscription, +} + +impl Render for StatusBar { + type Element = Div; + + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + div() + .py_0p5() + .px_1() + .flex() + .items_center() + .justify_between() + .w_full() + .bg(cx.theme().colors().status_bar) + .child(self.render_left_tools(cx)) + .child(self.render_right_tools(cx)) + } +} + +impl StatusBar { + fn render_left_tools(&self, cx: &mut ViewContext) -> impl Component { + div() + .flex() + .items_center() + .gap_1() + .children(self.left_items.iter().map(|item| item.to_any())) + } + + fn render_right_tools(&self, cx: &mut ViewContext) -> impl Component { + div() + .flex() + .items_center() + .gap_2() + .children(self.right_items.iter().map(|item| item.to_any())) + } +} + +// todo!() +// impl View for StatusBar { +// fn ui_name() -> &'static str { +// "StatusBar" +// } + +// fn render(&mut self, cx: &mut ViewContext) -> AnyElement { +// let theme = &theme::current(cx).workspace.status_bar; + +// StatusBarElement { +// left: Flex::row() +// .with_children(self.left_items.iter().map(|i| { +// ChildView::new(i.as_any(), cx) +// .aligned() +// .contained() +// .with_margin_right(theme.item_spacing) +// })) +// .into_any(), +// right: Flex::row() +// .with_children(self.right_items.iter().rev().map(|i| { +// ChildView::new(i.as_any(), cx) +// .aligned() +// .contained() +// .with_margin_left(theme.item_spacing) +// })) +// .into_any(), +// } +// .contained() +// .with_style(theme.container) +// .constrained() +// .with_height(theme.height) +// .into_any() +// } +// } + +impl StatusBar { + pub fn new(active_pane: &View, cx: &mut ViewContext) -> Self { + let mut this = Self { + left_items: Default::default(), + right_items: Default::default(), + active_pane: active_pane.clone(), + _observe_active_pane: cx + .observe(active_pane, |this, _, cx| this.update_active_pane_item(cx)), + }; + this.update_active_pane_item(cx); + this + } + + pub fn add_left_item(&mut self, item: View, cx: &mut ViewContext) + where + T: 'static + StatusItemView, + { + self.left_items.push(Box::new(item)); + cx.notify(); + } + + pub fn item_of_type(&self) -> Option> { + self.left_items + .iter() + .chain(self.right_items.iter()) + .find_map(|item| item.to_any().clone().downcast().log_err()) + } + + pub fn position_of_item(&self) -> Option + where + T: StatusItemView, + { + for (index, item) in self.left_items.iter().enumerate() { + if item.item_type() == TypeId::of::() { + return Some(index); + } + } + for (index, item) in self.right_items.iter().enumerate() { + if item.item_type() == TypeId::of::() { + return Some(index + self.left_items.len()); + } + } + return None; + } + + pub fn insert_item_after( + &mut self, + position: usize, + item: View, + cx: &mut ViewContext, + ) where + T: 'static + StatusItemView, + { + if position < self.left_items.len() { + self.left_items.insert(position + 1, Box::new(item)) + } else { + self.right_items + .insert(position + 1 - self.left_items.len(), Box::new(item)) + } + cx.notify() + } + + pub fn remove_item_at(&mut self, position: usize, cx: &mut ViewContext) { + if position < self.left_items.len() { + self.left_items.remove(position); + } else { + self.right_items.remove(position - self.left_items.len()); + } + cx.notify(); + } + + pub fn add_right_item(&mut self, item: View, cx: &mut ViewContext) + where + T: 'static + StatusItemView, + { + self.right_items.push(Box::new(item)); + cx.notify(); + } + + pub fn set_active_pane(&mut self, active_pane: &View, cx: &mut ViewContext) { + self.active_pane = active_pane.clone(); + self._observe_active_pane = + cx.observe(active_pane, |this, _, cx| this.update_active_pane_item(cx)); + self.update_active_pane_item(cx); + } + + fn update_active_pane_item(&mut self, cx: &mut ViewContext) { + let active_pane_item = self.active_pane.read(cx).active_item(); + for item in self.left_items.iter().chain(&self.right_items) { + item.set_active_pane_item(active_pane_item.as_deref(), cx); + } + } +} + +impl StatusItemViewHandle for View { + fn to_any(&self) -> AnyView { + self.clone().into() + } + + 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 item_type(&self) -> TypeId { + TypeId::of::() + } +} + +impl From<&dyn StatusItemViewHandle> for AnyView { + fn from(val: &dyn StatusItemViewHandle) -> Self { + val.to_any().clone() + } +} + +// todo!() +// struct StatusBarElement { +// left: AnyElement, +// right: AnyElement, +// } + +// todo!() +// impl Element for StatusBarElement { +// type LayoutState = (); +// type PaintState = (); + +// fn layout( +// &mut self, +// mut constraint: SizeConstraint, +// view: &mut StatusBar, +// cx: &mut ViewContext, +// ) -> (Vector2F, Self::LayoutState) { +// let max_width = constraint.max.x(); +// constraint.min = vec2f(0., constraint.min.y()); + +// let right_size = self.right.layout(constraint, view, cx); +// let constraint = SizeConstraint::new( +// vec2f(0., constraint.min.y()), +// vec2f(max_width - right_size.x(), constraint.max.y()), +// ); + +// self.left.layout(constraint, view, cx); + +// (vec2f(max_width, right_size.y()), ()) +// } + +// fn paint( +// &mut self, +// bounds: RectF, +// visible_bounds: RectF, +// _: &mut Self::LayoutState, +// view: &mut StatusBar, +// cx: &mut ViewContext, +// ) -> Self::PaintState { +// let origin_y = bounds.upper_right().y(); +// let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default(); + +// let left_origin = vec2f(bounds.lower_left().x(), origin_y); +// self.left.paint(left_origin, visible_bounds, view, cx); + +// let right_origin = vec2f(bounds.upper_right().x() - self.right.size().x(), origin_y); +// self.right.paint(right_origin, visible_bounds, view, cx); +// } + +// fn rect_for_text_range( +// &self, +// _: Range, +// _: RectF, +// _: RectF, +// _: &Self::LayoutState, +// _: &Self::PaintState, +// _: &StatusBar, +// _: &ViewContext, +// ) -> Option { +// None +// } + +// fn debug( +// &self, +// bounds: RectF, +// _: &Self::LayoutState, +// _: &Self::PaintState, +// _: &StatusBar, +// _: &ViewContext, +// ) -> serde_json::Value { +// json!({ +// "type": "StatusBarElement", +// "bounds": bounds.to_json() +// }) +// } +// } diff --git a/crates/workspace2/src/toolbar.rs b/crates/workspace2/src/toolbar.rs new file mode 100644 index 0000000000..c3d1e520c7 --- /dev/null +++ b/crates/workspace2/src/toolbar.rs @@ -0,0 +1,298 @@ +use crate::ItemHandle; +use gpui2::{ + AnyView, AppContext, Entity, EntityId, EventEmitter, Render, View, ViewContext, WindowContext, +}; + +pub trait ToolbarItemView: Render + EventEmitter { + fn set_active_pane_item( + &mut self, + active_pane_item: Option<&dyn crate::ItemHandle>, + cx: &mut ViewContext, + ) -> ToolbarItemLocation; + + fn location_for_event( + &self, + _event: &Self::Event, + current_location: ToolbarItemLocation, + _cx: &AppContext, + ) -> ToolbarItemLocation { + current_location + } + + fn pane_focus_update(&mut self, _pane_focused: bool, _cx: &mut ViewContext) {} + + /// Number of times toolbar's height will be repeated to get the effective height. + /// Useful when multiple rows one under each other are needed. + /// The rows have the same width and act as a whole when reacting to resizes and similar events. + fn row_count(&self, _cx: &WindowContext) -> usize { + 1 + } +} + +trait ToolbarItemViewHandle: Send { + fn id(&self) -> EntityId; + fn to_any(&self) -> AnyView; + fn set_active_pane_item( + &self, + active_pane_item: Option<&dyn ItemHandle>, + cx: &mut WindowContext, + ) -> ToolbarItemLocation; + fn focus_changed(&mut self, pane_focused: bool, cx: &mut WindowContext); + fn row_count(&self, cx: &WindowContext) -> usize; +} + +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum ToolbarItemLocation { + Hidden, + PrimaryLeft { flex: Option<(f32, bool)> }, + PrimaryRight { flex: Option<(f32, bool)> }, + Secondary, +} + +pub struct Toolbar { + active_item: Option>, + hidden: bool, + can_navigate: bool, + items: Vec<(Box, ToolbarItemLocation)>, +} + +// todo!() +// impl View for Toolbar { +// fn ui_name() -> &'static str { +// "Toolbar" +// } + +// fn render(&mut self, cx: &mut ViewContext) -> AnyElement { +// let theme = &theme::current(cx).workspace.toolbar; + +// let mut primary_left_items = Vec::new(); +// let mut primary_right_items = Vec::new(); +// let mut secondary_item = None; +// let spacing = theme.item_spacing; +// let mut primary_items_row_count = 1; + +// for (item, position) in &self.items { +// match *position { +// ToolbarItemLocation::Hidden => {} + +// ToolbarItemLocation::PrimaryLeft { flex } => { +// primary_items_row_count = primary_items_row_count.max(item.row_count(cx)); +// let left_item = ChildView::new(item.as_any(), cx).aligned(); +// if let Some((flex, expanded)) = flex { +// primary_left_items.push(left_item.flex(flex, expanded).into_any()); +// } else { +// primary_left_items.push(left_item.into_any()); +// } +// } + +// ToolbarItemLocation::PrimaryRight { flex } => { +// primary_items_row_count = primary_items_row_count.max(item.row_count(cx)); +// let right_item = ChildView::new(item.as_any(), cx).aligned().flex_float(); +// if let Some((flex, expanded)) = flex { +// primary_right_items.push(right_item.flex(flex, expanded).into_any()); +// } else { +// primary_right_items.push(right_item.into_any()); +// } +// } + +// ToolbarItemLocation::Secondary => { +// secondary_item = Some( +// ChildView::new(item.as_any(), cx) +// .constrained() +// .with_height(theme.height * item.row_count(cx) as f32) +// .into_any(), +// ); +// } +// } +// } + +// let container_style = theme.container; +// let height = theme.height * primary_items_row_count as f32; + +// let mut primary_items = Flex::row().with_spacing(spacing); +// primary_items.extend(primary_left_items); +// primary_items.extend(primary_right_items); + +// let mut toolbar = Flex::column(); +// if !primary_items.is_empty() { +// toolbar.add_child(primary_items.constrained().with_height(height)); +// } +// if let Some(secondary_item) = secondary_item { +// toolbar.add_child(secondary_item); +// } + +// if toolbar.is_empty() { +// toolbar.into_any_named("toolbar") +// } else { +// toolbar +// .contained() +// .with_style(container_style) +// .into_any_named("toolbar") +// } +// } +// } + +// <<<<<<< HEAD +// ======= +// #[allow(clippy::too_many_arguments)] +// fn nav_button)>( +// svg_path: &'static str, +// style: theme::Interactive, +// nav_button_height: f32, +// tooltip_style: TooltipStyle, +// enabled: bool, +// spacing: f32, +// on_click: F, +// tooltip_action: A, +// action_name: &'static str, +// cx: &mut ViewContext, +// ) -> AnyElement { +// MouseEventHandler::new::(0, cx, |state, _| { +// let style = if enabled { +// style.style_for(state) +// } else { +// style.disabled_style() +// }; +// Svg::new(svg_path) +// .with_color(style.color) +// .constrained() +// .with_width(style.icon_width) +// .aligned() +// .contained() +// .with_style(style.container) +// .constrained() +// .with_width(style.button_width) +// .with_height(nav_button_height) +// .aligned() +// .top() +// }) +// .with_cursor_style(if enabled { +// CursorStyle::PointingHand +// } else { +// CursorStyle::default() +// }) +// .on_click(MouseButton::Left, move |_, toolbar, cx| { +// on_click(toolbar, cx) +// }) +// .with_tooltip::( +// 0, +// action_name, +// Some(Box::new(tooltip_action)), +// tooltip_style, +// cx, +// ) +// .contained() +// .with_margin_right(spacing) +// .into_any_named("nav button") +// } + +// >>>>>>> 139cbbfd3aebd0863a7d51b0c12d748764cf0b2e +impl Toolbar { + pub fn new() -> Self { + Self { + active_item: None, + items: Default::default(), + hidden: false, + can_navigate: true, + } + } + + pub fn set_can_navigate(&mut self, can_navigate: bool, cx: &mut ViewContext) { + self.can_navigate = can_navigate; + cx.notify(); + } + + pub fn add_item(&mut self, item: View, cx: &mut ViewContext) + where + T: 'static + ToolbarItemView, + { + let location = item.set_active_pane_item(self.active_item.as_deref(), cx); + cx.subscribe(&item, |this, item, event, cx| { + if let Some((_, current_location)) = + this.items.iter_mut().find(|(i, _)| i.id() == item.id()) + { + let new_location = item + .read(cx) + .location_for_event(event, *current_location, cx); + if new_location != *current_location { + *current_location = new_location; + cx.notify(); + } + } + }) + .detach(); + self.items.push((Box::new(item), location)); + cx.notify(); + } + + pub fn set_active_item(&mut self, item: Option<&dyn ItemHandle>, cx: &mut ViewContext) { + self.active_item = item.map(|item| item.boxed_clone()); + self.hidden = self + .active_item + .as_ref() + .map(|item| !item.show_toolbar(cx)) + .unwrap_or(false); + + for (toolbar_item, current_location) in self.items.iter_mut() { + let new_location = toolbar_item.set_active_pane_item(item, cx); + if new_location != *current_location { + *current_location = new_location; + cx.notify(); + } + } + } + + pub fn focus_changed(&mut self, focused: bool, cx: &mut ViewContext) { + for (toolbar_item, _) in self.items.iter_mut() { + toolbar_item.focus_changed(focused, cx); + } + } + + pub fn item_of_type(&self) -> Option> { + self.items + .iter() + .find_map(|(item, _)| item.to_any().downcast().ok()) + } + + pub fn hidden(&self) -> bool { + self.hidden + } +} + +impl ToolbarItemViewHandle for View { + fn id(&self) -> EntityId { + self.entity_id() + } + + fn to_any(&self) -> AnyView { + self.clone().into() + } + + 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(cx).row_count(cx) + } +} + +// todo!() +// impl From<&dyn ToolbarItemViewHandle> for AnyViewHandle { +// fn from(val: &dyn ToolbarItemViewHandle) -> Self { +// val.as_any().clone() +// } +// } diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 3731094b26..6d29751073 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -1,86 +1,82 @@ -// pub mod dock; +#![allow(unused_variables, dead_code, unused_mut)] +// todo!() this is to make transition easier. + +pub mod dock; pub mod item; -// pub mod notifications; +pub mod notifications; pub mod pane; pub mod pane_group; mod persistence; pub mod searchable; // pub mod shared_screen; -// mod status_bar; +mod status_bar; mod toolbar; mod workspace_settings; -use anyhow::{anyhow, Result}; -// use call2::ActiveCall; -// use client2::{ -// proto::{self, PeerId}, -// Client, Status, TypedEnvelope, UserStore, -// }; -// use collections::{hash_map, HashMap, HashSet}; -// use futures::{ -// channel::{mpsc, oneshot}, -// future::try_join_all, -// FutureExt, StreamExt, -// }; -// use gpui2::{ -// 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, -// View, WeakViewHandle, WindowContext, WindowHandle, -// }; -// use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem}; -// use itertools::Itertools; -// use language2::{LanguageRegistry, Rope}; -// use node_runtime::NodeRuntime;// // - -use futures::channel::oneshot; -// 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}; +use crate::persistence::model::{ + DockData, DockStructure, SerializedItem, SerializedPane, SerializedPaneGroup, + SerializedWorkspace, +}; +use anyhow::{anyhow, Context as _, Result}; +use call2::ActiveCall; +use client2::{ + proto::{self, PeerId}, + Client, TypedEnvelope, UserStore, +}; +use collections::{HashMap, HashSet}; +use dock::{Dock, DockPosition, PanelButtons}; +use futures::{ + channel::{mpsc, oneshot}, + future::try_join_all, + Future, FutureExt, StreamExt, +}; +use gpui2::{ + div, point, size, AnyModel, AnyView, AnyWeakView, AppContext, AsyncAppContext, + AsyncWindowContext, Bounds, Component, Div, EntityId, EventEmitter, GlobalPixels, Model, + ModelContext, ParentElement, Point, Render, Size, StatefulInteractive, Styled, Subscription, + Task, View, ViewContext, VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, + WindowOptions, +}; +use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem}; +use language2::LanguageRegistry; +use lazy_static::lazy_static; +use node_runtime::NodeRuntime; +use notifications::{simple_message_notification::MessageNotification, NotificationHandle}; 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}; +use persistence::{ + model::{ItemId, WorkspaceLocation}, + DB, +}; +use postage::stream::Stream; +use project2::{Project, ProjectEntryId, ProjectPath, Worktree}; +use serde::Deserialize; +use settings2::Settings; +use status_bar::StatusBar; +use std::{ + any::TypeId, + borrow::Cow, + env, + path::{Path, PathBuf}, + sync::{atomic::AtomicUsize, Arc}, + time::Duration, +}; +use theme2::ActiveTheme; pub use toolbar::{ToolbarItemLocation, ToolbarItemView}; -// use util::ResultExt; -// pub use workspace_settings::{AutosaveSetting, GitGutterSetting, WorkspaceSettings}; +use util::ResultExt; +use uuid::Uuid; +use workspace_settings::{AutosaveSetting, WorkspaceSettings}; -// lazy_static! { -// static ref ZED_WINDOW_SIZE: Option = env::var("ZED_WINDOW_SIZE") -// .ok() -// .as_deref() -// .and_then(parse_pixel_position_env_var); -// static ref ZED_WINDOW_POSITION: Option = env::var("ZED_WINDOW_POSITION") -// .ok() -// .as_deref() -// .and_then(parse_pixel_position_env_var); -// } +lazy_static! { + static ref ZED_WINDOW_SIZE: Option> = env::var("ZED_WINDOW_SIZE") + .ok() + .as_deref() + .and_then(parse_pixel_size_env_var); + static ref ZED_WINDOW_POSITION: Option> = env::var("ZED_WINDOW_POSITION") + .ok() + .as_deref() + .and_then(parse_pixel_position_env_var); +} // pub trait Modal: View { // fn has_focus(&self) -> bool; @@ -170,13 +166,13 @@ pub use toolbar::{ToolbarItemLocation, ToolbarItemView}; // pub save_intent: Option, // } -// #[derive(Deserialize)] -// pub struct Toast { -// id: usize, -// msg: Cow<'static, str>, -// #[serde(skip)] -// on_click: Option<(Cow<'static, str>, Arc)>, -// } +#[derive(Deserialize)] +pub struct Toast { + id: usize, + msg: Cow<'static, str>, + #[serde(skip)] + on_click: Option<(Cow<'static, str>, Arc)>, +} // impl Toast { // pub fn new>>(id: usize, msg: I) -> Self { @@ -237,143 +233,142 @@ pub use toolbar::{ToolbarItemLocation, ToolbarItemView}; pub type WorkspaceId = i64; -// pub fn init_settings(cx: &mut AppContext) { -// settings::register::(cx); -// settings::register::(cx); -// } +pub fn init_settings(cx: &mut AppContext) { + WorkspaceSettings::register(cx); + ItemSettings::register(cx); +} -// pub fn init(app_state: Arc, cx: &mut AppContext) { -// init_settings(cx); -// pane::init(cx); -// notifications::init(cx); +pub fn init(app_state: Arc, cx: &mut AppContext) { + init_settings(cx); + pane::init(cx); + notifications::init(cx); -// cx.add_global_action({ -// let app_state = Arc::downgrade(&app_state); -// move |_: &Open, cx: &mut AppContext| { -// let mut paths = cx.prompt_for_paths(PathPromptOptions { -// files: true, -// directories: true, -// multiple: true, -// }); + // 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); + // if let Some(app_state) = app_state.upgrade() { + // cx.spawn(move |mut cx| async move { + // if let Some(paths) = paths.recv().await.flatten() { + // cx.update(|cx| { + // open_paths(&paths, &app_state, None, cx).detach_and_log_err(cx) + // }); + // } + // }) + // .detach(); + // } + // } + // }); + // cx.add_async_action(Workspace::open); -// cx.add_async_action(Workspace::follow_next_collaborator); -// cx.add_async_action(Workspace::close); -// cx.add_async_action(Workspace::close_inactive_items_and_panes); -// cx.add_async_action(Workspace::close_all_items_and_panes); -// cx.add_global_action(Workspace::close_global); -// cx.add_global_action(restart); -// cx.add_async_action(Workspace::save_all); -// cx.add_action(Workspace::add_folder_to_project); -// cx.add_action( -// |workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext| { -// let pane = workspace.active_pane().clone(); -// workspace.unfollow(&pane, cx); -// }, -// ); -// cx.add_action( -// |workspace: &mut Workspace, action: &Save, cx: &mut ViewContext| { -// workspace -// .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx) -// .detach_and_log_err(cx); -// }, -// ); -// cx.add_action( -// |workspace: &mut Workspace, _: &SaveAs, cx: &mut ViewContext| { -// workspace -// .save_active_item(SaveIntent::SaveAs, cx) -// .detach_and_log_err(cx); -// }, -// ); -// cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| { -// workspace.activate_previous_pane(cx) -// }); -// cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| { -// workspace.activate_next_pane(cx) -// }); + // cx.add_async_action(Workspace::follow_next_collaborator); + // cx.add_async_action(Workspace::close); + // cx.add_async_action(Workspace::close_inactive_items_and_panes); + // cx.add_async_action(Workspace::close_all_items_and_panes); + // cx.add_global_action(Workspace::close_global); + // cx.add_global_action(restart); + // cx.add_async_action(Workspace::save_all); + // cx.add_action(Workspace::add_folder_to_project); + // cx.add_action( + // |workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext| { + // let pane = workspace.active_pane().clone(); + // workspace.unfollow(&pane, cx); + // }, + // ); + // cx.add_action( + // |workspace: &mut Workspace, action: &Save, cx: &mut ViewContext| { + // workspace + // .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx) + // .detach_and_log_err(cx); + // }, + // ); + // cx.add_action( + // |workspace: &mut Workspace, _: &SaveAs, cx: &mut ViewContext| { + // workspace + // .save_active_item(SaveIntent::SaveAs, cx) + // .detach_and_log_err(cx); + // }, + // ); + // cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| { + // workspace.activate_previous_pane(cx) + // }); + // cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| { + // workspace.activate_next_pane(cx) + // }); -// cx.add_action( -// |workspace: &mut Workspace, action: &ActivatePaneInDirection, cx| { -// workspace.activate_pane_in_direction(action.0, cx) -// }, -// ); + // cx.add_action( + // |workspace: &mut Workspace, action: &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, 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(|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"); + // 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(); -// }); -// } + // workspace.update(&mut cx, |workspace, cx| { + // if matches!(err, Err(_)) { + // err.notify_err(workspace, cx); + // } else { + // workspace.show_notification(1, cx, |cx| { + // cx.build_view(|_| { + // MessageNotification::new("Successfully installed the `zed` binary") + // }) + // }); + // } + // }) + // }) + // .detach(); + // }); +} type ProjectItemBuilders = - HashMap, AnyHandle, &mut ViewContext) -> Box>; + HashMap, AnyModel, &mut ViewContext) -> Box>; pub fn register_project_item(cx: &mut AppContext) { - cx.update_default_global(|builders: &mut ProjectItemBuilders, _| { - builders.insert(TypeId::of::(), |project, model, cx| { - let item = model.downcast::().unwrap(); - Box::new(cx.add_view(|cx| I::for_project_item(project, item, cx))) - }); + let builders = cx.default_global::(); + builders.insert(TypeId::of::(), |project, model, cx| { + let item = model.downcast::().unwrap(); + Box::new(cx.build_view(|cx| I::for_project_item(project, item, cx))) }); } @@ -392,26 +387,25 @@ type FollowableItemBuilders = HashMap< ), >; pub fn register_followable_item(cx: &mut AppContext) { - cx.update_default_global(|builders: &mut FollowableItemBuilders, _| { - builders.insert( - TypeId::of::(), - ( - |pane, workspace, id, state, cx| { - I::from_state_proto(pane, workspace, id, state, cx).map(|task| { - cx.foreground() - .spawn(async move { Ok(Box::new(task.await?) as Box<_>) }) - }) - }, - |this| Box::new(this.clone().downcast::().unwrap()), - ), - ); - }); + let builders = cx.default_global::(); + builders.insert( + TypeId::of::(), + ( + |pane, workspace, id, state, cx| { + I::from_state_proto(pane, workspace, id, state, cx).map(|task| { + cx.foreground_executor() + .spawn(async move { Ok(Box::new(task.await?) as Box<_>) }) + }) + }, + |this| Box::new(this.clone().downcast::().unwrap()), + ), + ); } type ItemDeserializers = HashMap< Arc, fn( - Handle, + Model, WeakView, WorkspaceId, ItemId, @@ -419,13 +413,13 @@ type ItemDeserializers = HashMap< ) -> Task>>, >; pub fn register_deserializable_item(cx: &mut AppContext) { - cx.update_default_global(|deserializers: &mut ItemDeserializers, _cx| { + cx.update_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() + cx.foreground_executor() .spawn(async { Ok(Box::new(task.await?) as Box<_>) }) }, ); @@ -436,18 +430,22 @@ pub fn register_deserializable_item(cx: &mut AppContext) { pub struct AppState { pub languages: Arc, pub client: Arc, - pub user_store: Handle, - pub workspace_store: Handle, + pub user_store: Model, + pub workspace_store: Model, pub fs: Arc, pub build_window_options: - fn(Option, Option, &MainThread) -> WindowOptions, - pub initialize_workspace: - fn(WeakHandle, bool, Arc, AsyncAppContext) -> Task>, + fn(Option, Option, &mut AppContext) -> WindowOptions, + pub initialize_workspace: fn( + WeakView, + bool, + Arc, + AsyncWindowContext, + ) -> Task>, pub node_runtime: Arc, } pub struct WorkspaceStore { - workspaces: HashSet>, + workspaces: HashSet>, followers: Vec, client: Arc, _subscriptions: Vec, @@ -459,40 +457,42 @@ struct Follower { peer_id: PeerId, } -// impl AppState { -// #[cfg(any(test, feature = "test-support"))] -// pub fn test(cx: &mut AppContext) -> Arc { -// use node_runtime::FakeNodeRuntime; -// use settings::SettingsStore; +impl AppState { + #[cfg(any(test, feature = "test-support"))] + pub fn test(cx: &mut AppContext) -> Arc { + use gpui2::Context; + use node_runtime::FakeNodeRuntime; + use settings2::SettingsStore; -// if !cx.has_global::() { -// cx.set_global(SettingsStore::test(cx)); -// } + if !cx.has_global::() { + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + } -// 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)); + let fs = fs2::FakeFs::new(cx.background_executor().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.build_model(|cx| UserStore::new(client.clone(), http_client, cx)); + let workspace_store = cx.build_model(|cx| WorkspaceStore::new(client.clone(), cx)); -// theme::init((), cx); -// client::init(&client, cx); -// crate::init_settings(cx); + // todo!() + // theme::init((), cx); + client2::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(), -// }) -// } -// } + Arc::new(Self { + client, + fs, + languages, + user_store, + workspace_store, + node_runtime: FakeNodeRuntime::new(), + initialize_workspace: |_, _, _, _| Task::ready(Ok(())), + build_window_options: |_, _, _| Default::default(), + }) + } +} struct DelayedDebouncedEditAction { task: Option>, @@ -509,7 +509,7 @@ impl DelayedDebouncedEditAction { fn fire_new(&mut self, delay: Duration, cx: &mut ViewContext, func: F) where - F: 'static + FnOnce(&mut Workspace, &mut ViewContext) -> Task>, + F: 'static + Send + FnOnce(&mut Workspace, &mut ViewContext) -> Task>, { if let Some(channel) = self.cancel_channel.take() { _ = channel.send(()); @@ -519,8 +519,8 @@ impl DelayedDebouncedEditAction { 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(); + self.task = Some(cx.spawn(move |workspace, mut cx| async move { + let mut timer = cx.background_executor().timer(delay).fuse(); if let Some(previous_task) = previous_task { previous_task.await; } @@ -540,41 +540,41 @@ impl DelayedDebouncedEditAction { } } -// pub enum Event { -// PaneAdded(View), -// ContactRequestedJoin(u64), -// } +pub enum Event { + PaneAdded(View), + ContactRequestedJoin(u64), +} pub struct Workspace { - weak_self: WeakHandle, + weak_self: WeakView, // modal: Option, - // zoomed: Option, + zoomed: Option, // zoomed_position: Option, - // center: PaneGroup, - // left_dock: View, - // bottom_dock: View, - // right_dock: View, + center: PaneGroup, + left_dock: View, + bottom_dock: View, + right_dock: View, panes: Vec>, - // panes_by_item: HashMap>, - // active_pane: View, + panes_by_item: HashMap>, + active_pane: View, last_active_center_pane: Option>, - // last_active_view_id: Option, - // status_bar: View, + last_active_view_id: Option, + status_bar: View, // titlebar_item: Option, - // notifications: Vec<(TypeId, usize, Box)>, - project: Handle, - // follower_states: HashMap, FollowerState>, - // last_leaders_by_pane: HashMap, PeerId>, - // window_edited: bool, - // active_call: Option<(ModelHandle, Vec)>, - // leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>, - // database_id: WorkspaceId, + notifications: Vec<(TypeId, usize, Box)>, + project: Model, + follower_states: HashMap, FollowerState>, + last_leaders_by_pane: HashMap, PeerId>, + window_edited: bool, + active_call: Option<(Model, Vec)>, + leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>, + database_id: WorkspaceId, app_state: Arc, - // subscriptions: Vec, - // _apply_leader_updates: Task>, - // _observe_current_user: Task>, - // _schedule_serialize: Option>, - // pane_history_timestamp: Arc, + subscriptions: Vec, + _apply_leader_updates: Task>, + _observe_current_user: Task>, + _schedule_serialize: Option>, + pane_history_timestamp: Arc, } // struct ActiveModal { @@ -582,11 +582,11 @@ pub struct Workspace { // previously_focused_view_id: Option, // } -// #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] -// pub struct ViewId { -// pub creator: PeerId, -// pub id: u64, -// } +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub struct ViewId { + pub creator: PeerId, + pub id: u64, +} #[derive(Default)] struct FollowerState { @@ -595,353 +595,362 @@ struct FollowerState { items_by_leader_view_id: HashMap>, } -// enum WorkspaceBounds {} +enum WorkspaceBounds {} impl Workspace { - // pub fn new( - // workspace_id: WorkspaceId, - // project: ModelHandle, - // app_state: Arc, - // cx: &mut ViewContext, - // ) -> Self { - // cx.observe(&project, |_, _, cx| cx.notify()).detach(); - // cx.subscribe(&project, move |this, _, event, cx| { - // match event { - // project::Event::RemoteIdChanged(_) => { - // this.update_window_title(cx); - // } + pub fn new( + workspace_id: WorkspaceId, + project: Model, + app_state: Arc, + cx: &mut ViewContext, + ) -> Self { + cx.observe(&project, |_, _, cx| cx.notify()).detach(); + cx.subscribe(&project, move |this, _, event, cx| { + match event { + project2::Event::RemoteIdChanged(_) => { + this.update_window_title(cx); + } - // project::Event::CollaboratorLeft(peer_id) => { - // this.collaborator_left(*peer_id, cx); - // } + project2::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); - // } + project2::Event::WorktreeRemoved(_) | project2::Event::WorktreeAdded => { + this.update_window_title(cx); + this.serialize_workspace(cx); + } - // project::Event::DisconnectedFromHost => { - // this.update_window_edited(cx); - // cx.blur(); - // } + project2::Event::DisconnectedFromHost => { + this.update_window_edited(cx); + cx.blur(); + } - // project::Event::Closed => { - // cx.remove_window(); - // } + project2::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) - // }); - // } - // } + project2::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())) - // }), + project2::Event::Notification(message) => this.show_notification(0, cx, |cx| { + cx.build_view(|_| MessageNotification::new(message.clone())) + }), - // _ => {} - // } - // cx.notify() - // }) - // .detach(); + _ => {} + } + cx.notify() + }) + .detach(); - // let weak_handle = cx.weak_handle(); - // let pane_history_timestamp = Arc::new(AtomicUsize::new(0)); + let weak_handle = cx.view().downgrade(); + let pane_history_timestamp = Arc::new(AtomicUsize::new(0)); - // let center_pane = cx.add_view(|cx| { - // Pane::new( - // weak_handle.clone(), - // project.clone(), - // 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())); + let center_pane = cx.build_view(|cx| { + Pane::new( + weak_handle.clone(), + project.clone(), + pane_history_timestamp.clone(), + cx, + ) + }); + cx.subscribe(¢er_pane, Self::handle_pane_event).detach(); + // todo!() + // 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 window_handle = cx.window_handle().downcast::().unwrap(); + app_state.workspace_store.update(cx, |store, _| { + store.workspaces.insert(window_handle); + }); - // 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)); + 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.next().await; + connection_status.next().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(()) - // }); + 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(); - // } + // 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(()) - // }); + Ok(()) + }); - // cx.emit_global(WorkspaceCreated(weak_handle.clone())); + // todo!("replace with a different mechanism") + // 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 - // }); + let left_dock = cx.build_view(|_| Dock::new(DockPosition::Left)); + let bottom_dock = cx.build_view(|_| Dock::new(DockPosition::Bottom)); + let right_dock = cx.build_view(|_| Dock::new(DockPosition::Right)); + let left_dock_buttons = + cx.build_view(|cx| PanelButtons::new(left_dock.clone(), weak_handle.clone(), cx)); + let bottom_dock_buttons = + cx.build_view(|cx| PanelButtons::new(bottom_dock.clone(), weak_handle.clone(), cx)); + let right_dock_buttons = + cx.build_view(|cx| PanelButtons::new(right_dock.clone(), weak_handle.clone(), cx)); + let status_bar = cx.build_view(|cx| { + let mut status_bar = StatusBar::new(¢er_pane.clone(), cx); + status_bar.add_left_item(left_dock_buttons, cx); + status_bar.add_right_item(right_dock_buttons, cx); + status_bar.add_right_item(bottom_dock_buttons, cx); + status_bar + }); - // cx.update_default_global::, _, _>(|drag_and_drop, _| { - // drag_and_drop.register_container(weak_handle.clone()); - // }); + // todo!() + // cx.update_default_global::, _, _>(|drag_and_drop, _| { + // drag_and_drop.register_container(weak_handle.clone()); + // }); - // let mut active_call = None; - // if cx.has_global::>() { - // let call = cx.global::>().clone(); - // let mut subscriptions = Vec::new(); - // subscriptions.push(cx.subscribe(&call, Self::on_active_call_event)); - // active_call = Some((call, subscriptions)); - // } + let mut active_call = None; + if cx.has_global::>() { + let call = cx.global::>().clone(); + let mut subscriptions = Vec::new(); + subscriptions.push(cx.subscribe(&call, Self::on_active_call_event)); + active_call = Some((call, subscriptions)); + } - // let subscriptions = vec![ - // cx.observe_fullscreen(|_, _, cx| cx.notify()), - // cx.observe_window_activation(Self::on_window_activation_changed), - // cx.observe_window_bounds(move |_, mut bounds, display, cx| { - // // Transform fixed bounds to be stored in terms of the containing display - // if let WindowBounds::Fixed(mut window_bounds) = bounds { - // if let Some(screen) = cx.platform().screen_by_id(display) { - // let screen_bounds = screen.bounds(); - // window_bounds - // .set_origin_x(window_bounds.origin_x() - screen_bounds.origin_x()); - // window_bounds - // .set_origin_y(window_bounds.origin_y() - screen_bounds.origin_y()); - // bounds = WindowBounds::Fixed(window_bounds); - // } - // } + let subscriptions = vec![ + cx.observe_window_activation(Self::on_window_activation_changed), + cx.observe_window_bounds(move |_, cx| { + if let Some(display) = cx.display() { + // Transform fixed bounds to be stored in terms of the containing display + let mut bounds = cx.window_bounds(); + if let WindowBounds::Fixed(window_bounds) = &mut bounds { + let display_bounds = display.bounds(); + window_bounds.origin.x -= display_bounds.origin.x; + window_bounds.origin.y -= display_bounds.origin.y; + } - // 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(); - // }), - // ]; + if let Some(display_uuid) = display.uuid().log_err() { + cx.background_executor() + .spawn(DB.set_window_bounds(workspace_id, bounds, display_uuid)) + .detach_and_log_err(cx); + } + } + cx.notify(); + }), + 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, - // } - // } + cx.defer(|this, cx| this.update_window_title(cx)); + Workspace { + weak_self: weak_handle.clone(), + // modal: None, + zoomed: None, + // zoomed_position: None, + center: PaneGroup::new(center_pane.clone()), + panes: vec![center_pane.clone()], + panes_by_item: Default::default(), + active_pane: center_pane.clone(), + last_active_center_pane: Some(center_pane.downgrade()), + last_active_view_id: None, + status_bar, + // titlebar_item: None, + notifications: Default::default(), + left_dock, + bottom_dock, + right_dock, + project: project.clone(), + follower_states: Default::default(), + last_leaders_by_pane: Default::default(), + window_edited: false, + active_call, + database_id: workspace_id, + app_state, + _observe_current_user, + _apply_leader_updates, + _schedule_serialize: None, + leader_updates_tx, + subscriptions, + pane_history_timestamp, + } + } - // fn new_local( - // abs_paths: Vec, - // app_state: Arc, - // requesting_window: Option>, - // cx: &mut AppContext, - // ) -> Task<( - // WeakViewHandle, - // Vec, anyhow::Error>>>, - // )> { - // let project_handle = Project::local( - // app_state.client.clone(), - // app_state.node_runtime.clone(), - // app_state.user_store.clone(), - // app_state.languages.clone(), - // app_state.fs.clone(), - // cx, - // ); + fn new_local( + abs_paths: Vec, + app_state: Arc, + _requesting_window: Option>, + cx: &mut AppContext, + ) -> Task< + anyhow::Result<( + WindowHandle, + Vec, anyhow::Error>>>, + )>, + > { + let project_handle = Project::local( + app_state.client.clone(), + app_state.node_runtime.clone(), + app_state.user_store.clone(), + app_state.languages.clone(), + app_state.fs.clone(), + cx, + ); - // cx.spawn(|mut cx| async move { - // let serialized_workspace = persistence::DB.workspace_for_roots(&abs_paths.as_slice()); + cx.spawn(|mut cx| async move { + let serialized_workspace: Option = None; //persistence::DB.workspace_for_roots(&abs_paths.as_slice()); - // let paths_to_open = Arc::new(abs_paths); + let paths_to_open = Arc::new(abs_paths); - // // Get project paths for all of the abs_paths - // let mut worktree_roots: HashSet> = Default::default(); - // let mut project_paths: Vec<(PathBuf, Option)> = - // Vec::with_capacity(paths_to_open.len()); - // for path in paths_to_open.iter().cloned() { - // if let Some((worktree, project_entry)) = cx - // .update(|cx| { - // Workspace::project_path_for_path(project_handle.clone(), &path, true, cx) - // }) - // .await - // .log_err() - // { - // worktree_roots.insert(worktree.read_with(&mut cx, |tree, _| tree.abs_path())); - // project_paths.push((path, Some(project_entry))); - // } else { - // project_paths.push((path, None)); - // } - // } + // Get project paths for all of the abs_paths + let mut worktree_roots: HashSet> = Default::default(); + let mut project_paths: Vec<(PathBuf, Option)> = + Vec::with_capacity(paths_to_open.len()); + for path in paths_to_open.iter().cloned() { + if let Some((worktree, project_entry)) = cx + .update(|cx| { + Workspace::project_path_for_path(project_handle.clone(), &path, true, cx) + })? + .await + .log_err() + { + worktree_roots.extend(worktree.update(&mut cx, |tree, _| tree.abs_path()).ok()); + 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 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?; + // todo!() + let window = /*if let Some(window) = requesting_window { + cx.update_window(window.into(), |old_workspace, cx| { + cx.replace_root_view(|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 serialized_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; - // } - // } + // 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 { + let screen = + cx.update(|cx| + cx.displays() + .into_iter() + .find(|display| display.uuid().ok() == Some(serialized_display)) + ).ok()??; + let screen_bounds = screen.bounds(); + window_bounds.origin.x += screen_bounds.origin.x; + window_bounds.origin.y += screen_bounds.origin.y; + bounds = WindowBounds::Fixed(window_bounds); + } - // Some((bounds, display)) - // }) - // .unzip() - // }; + Some((bounds, serialized_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, - // ) - // }, - // ) - // } - // }; + // Use the serialized workspace to construct the new window + let options = + cx.update(|cx| (app_state.build_window_options)(bounds, display, cx))?; - // // We haven't yielded the main thread since obtaining the window handle, - // // so the window exists. - // let workspace = window.root(&cx).unwrap(); + cx.open_window(options, { + let app_state = app_state.clone(); + let workspace_id = workspace_id.clone(); + let project_handle = project_handle.clone(); + move |cx| { + cx.build_view(|cx| { + Workspace::new(workspace_id, project_handle, app_state, cx) + }) + } + })? + }; - // (app_state.initialize_workspace)( - // workspace.downgrade(), - // serialized_workspace.is_some(), - // app_state.clone(), - // cx.clone(), - // ) - // .await - // .log_err(); + // todo!() Ask how to do this + let weak_view = window.update(&mut cx, |_, cx| cx.view().downgrade())?; + let async_cx = window.update(&mut cx, |_, cx| cx.to_async())?; - // window.update(&mut cx, |cx| cx.activate_window()); + (app_state.initialize_workspace)( + weak_view, + serialized_workspace.is_some(), + app_state.clone(), + async_cx, + ) + .await + .log_err(); - // 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(); + window + .update(&mut cx, |_, cx| cx.activate_window()) + .log_err(); - // (workspace, opened_items) - // }) - // } + notify_if_database_failed(window, &mut cx); + let opened_items = window + .update(&mut cx, |_workspace, cx| { + open_items( + serialized_workspace, + project_paths, + app_state, + cx, + ) + })? + .await + .unwrap_or_default(); - // pub fn weak_handle(&self) -> WeakViewHandle { - // self.weak_self.clone() - // } + Ok((window, opened_items)) + }) + } - // pub fn left_dock(&self) -> &View { - // &self.left_dock - // } + pub fn weak_handle(&self) -> WeakView { + self.weak_self.clone() + } - // pub fn bottom_dock(&self) -> &View { - // &self.bottom_dock - // } + pub fn left_dock(&self) -> &View { + &self.left_dock + } - // pub fn right_dock(&self) -> &View { - // &self.right_dock - // } + pub fn bottom_dock(&self) -> &View { + &self.bottom_dock + } + + pub fn right_dock(&self) -> &View { + &self.right_dock + } // pub fn add_panel(&mut self, panel: View, cx: &mut ViewContext) // where @@ -1038,15 +1047,15 @@ impl Workspace { // &self.status_bar // } - // pub fn app_state(&self) -> &Arc { - // &self.app_state - // } + pub fn app_state(&self) -> &Arc { + &self.app_state + } - // pub fn user_store(&self) -> &ModelHandle { - // &self.app_state.user_store - // } + pub fn user_store(&self) -> &Model { + &self.app_state.user_store + } - pub fn project(&self) -> &Handle { + pub fn project(&self) -> &Model { &self.project } @@ -1108,7 +1117,7 @@ impl Workspace { // fn navigate_history( // &mut self, - // pane: WeakViewHandle, + // pane: WeakView, // mode: NavigationMode, // cx: &mut ViewContext, // ) -> Task> { @@ -1193,7 +1202,7 @@ impl Workspace { // pub fn go_back( // &mut self, - // pane: WeakViewHandle, + // pane: WeakView, // cx: &mut ViewContext, // ) -> Task> { // self.navigate_history(pane, NavigationMode::GoingBack, cx) @@ -1201,7 +1210,7 @@ impl Workspace { // pub fn go_forward( // &mut self, - // pane: WeakViewHandle, + // pane: WeakView, // cx: &mut ViewContext, // ) -> Task> { // self.navigate_history(pane, NavigationMode::GoingForward, cx) @@ -1498,13 +1507,13 @@ impl Workspace { visible: bool, cx: &mut ViewContext, ) -> Task, anyhow::Error>>>> { - log::info!("open paths {:?}", abs_paths); + 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 { + cx.spawn(move |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 @@ -1523,45 +1532,41 @@ impl Workspace { }; 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(project2::Event::ActiveEntryChanged(Some( - entry_id, - ))); - }) - } + let abs_path = abs_path.clone(); + let fs = fs.clone(); + let task = cx.spawn(move |mut cx| 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()?; - None - } + .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(project2::Event::ActiveEntryChanged(Some(entry_id))); + }) + } + }) + .log_err()?; + None } }); tasks.push(task); @@ -1592,15 +1597,15 @@ impl Workspace { // } fn project_path_for_path( - project: Handle, + project: Model, abs_path: &Path, visible: bool, cx: &mut AppContext, - ) -> Task, ProjectPath)>> { + ) -> Task, ProjectPath)>> { let entry = project.update(cx, |project, cx| { project.find_or_create_local_worktree(abs_path, visible, cx) }); - cx.spawn(|cx| async move { + cx.spawn(|mut cx| async move { let (worktree, path) = entry.await?; let worktree_id = worktree.update(&mut cx, |t, _| t.id())?; Ok(( @@ -1617,7 +1622,7 @@ impl Workspace { // pub fn toggle_modal( // &mut self, // cx: &mut ViewContext, - // add_view: F, + // build_view: F, // ) -> Option> // where // V: 'static + Modal, @@ -1633,7 +1638,7 @@ impl Workspace { // cx.focus_self(); // Some(already_open_modal) // } else { - // let modal = add_view(self, cx); + // let modal = build_view(self, cx); // cx.subscribe(&modal, |this, _, event, cx| { // if V::dismiss_on_event(event) { // this.dismiss_modal(cx); @@ -1670,12 +1675,12 @@ impl Workspace { // } // } - // pub fn items<'a>( - // &'a self, - // cx: &'a AppContext, - // ) -> impl 'a + Iterator> { - // self.panes.iter().flat_map(|pane| pane.read(cx).items()) - // } + pub fn items<'a>( + &'a self, + cx: &'a AppContext, + ) -> impl 'a + Iterator> { + self.panes.iter().flat_map(|pane| pane.read(cx).items()) + } // pub fn item_of_type(&self, cx: &AppContext) -> Option> { // self.items_of_type(cx).max_by_key(|item| item.id()) @@ -1690,9 +1695,9 @@ impl Workspace { // .flat_map(|pane| pane.read(cx).items_of_type()) // } - // pub fn active_item(&self, cx: &AppContext) -> Option> { - // self.active_pane().read(cx).active_item() - // } + pub fn active_item(&self, cx: &AppContext) -> Option> { + self.active_pane().read(cx).active_item() + } // fn active_project_path(&self, cx: &ViewContext) -> Option { // self.active_item(cx).and_then(|item| item.project_path(cx)) @@ -1957,21 +1962,23 @@ impl Workspace { // cx.notify(); // } - // fn add_pane(&mut self, cx: &mut ViewContext) -> View { - // let pane = cx.add_view(|cx| { - // Pane::new( - // self.weak_handle(), - // self.project.clone(), - // 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 - // } + fn add_pane(&mut self, _cx: &mut ViewContext) -> View { + todo!() + // let pane = cx.build_view(|cx| { + // Pane::new( + // self.weak_handle(), + // self.project.clone(), + // self.pane_history_timestamp.clone(), + // cx, + // ) + // }); + // cx.subscribe(&pane, Self::handle_pane_event).detach(); + // self.panes.push(pane.clone()); + // todo!() + // cx.focus(&pane); + // cx.emit(Event::PaneAdded(pane.clone())); + // pane + } // pub fn add_item_to_center( // &mut self, @@ -2069,7 +2076,7 @@ impl Workspace { }); let task = self.load_path(path.into(), cx); - cx.spawn(|_, mut cx| async move { + cx.spawn(move |_, 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) @@ -2116,19 +2123,19 @@ impl Workspace { ) -> Task< Result<( ProjectEntryId, - impl 'static + FnOnce(&mut ViewContext) -> Box, + impl 'static + Send + FnOnce(&mut ViewContext) -> Box, )>, > { let project = self.project().clone(); let project_item = project.update(cx, |project, cx| project.open_path(path, cx)); cx.spawn(|_, mut cx| async move { let (project_entry_id, project_item) = project_item.await?; - let build_item = cx.update(|cx| { + let build_item = cx.update(|_, cx| { cx.default_global::() - .get(&project_item.model_type()) + .get(&project_item.entity_type()) .ok_or_else(|| anyhow!("no item builder for project item")) .cloned() - })?; + })??; let build_item = move |cx: &mut ViewContext| build_item(project, project_item, cx); Ok((project_entry_id, build_item)) @@ -2154,7 +2161,7 @@ impl Workspace { // return item; // } - // let item = cx.add_view(|cx| T::for_project_item(self.project().clone(), project_item, cx)); + // let item = cx.build_view(|cx| T::for_project_item(self.project().clone(), project_item, cx)); // self.add_item(Box::new(item.clone()), cx); // item // } @@ -2178,7 +2185,7 @@ impl Workspace { // return item; // } - // let item = cx.add_view(|cx| T::for_project_item(self.project().clone(), project_item, cx)); + // let item = cx.build_view(|cx| T::for_project_item(self.project().clone(), project_item, cx)); // self.split_item(SplitDirection::Right, Box::new(item.clone()), cx); // item // } @@ -2303,64 +2310,65 @@ impl Workspace { // cx.notify(); // } - // fn handle_pane_event( - // &mut self, - // pane: View, - // event: &pane::Event, - // cx: &mut ViewContext, - // ) { - // match event { - // pane::Event::AddItem { item } => item.added_to_pane(self, pane, cx), - // pane::Event::Split(direction) => { - // self.split_and_clone(pane, *direction, cx); - // } - // pane::Event::Remove => self.remove_pane(pane, cx), - // pane::Event::ActivateItem { local } => { - // if *local { - // self.unfollow(&pane, cx); - // } - // if &pane == self.active_pane() { - // self.active_item_path_changed(cx); - // } - // } - // pane::Event::ChangeItemTitle => { - // if pane == self.active_pane { - // self.active_item_path_changed(cx); - // } - // self.update_window_edited(cx); - // } - // pane::Event::RemoveItem { item_id } => { - // self.update_window_edited(cx); - // if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) { - // if entry.get().id() == pane.id() { - // entry.remove(); - // } - // } - // } - // pane::Event::Focus => { - // self.handle_pane_focused(pane.clone(), cx); - // } - // pane::Event::ZoomIn => { - // if pane == self.active_pane { - // pane.update(cx, |pane, cx| pane.set_zoomed(true, cx)); - // if pane.read(cx).has_focus() { - // self.zoomed = Some(pane.downgrade().into_any()); - // self.zoomed_position = None; - // } - // cx.notify(); - // } - // } - // pane::Event::ZoomOut => { - // pane.update(cx, |pane, cx| pane.set_zoomed(false, cx)); - // if self.zoomed_position.is_none() { - // self.zoomed = None; - // } - // cx.notify(); - // } - // } + fn handle_pane_event( + &mut self, + _pane: View, + _event: &pane::Event, + _cx: &mut ViewContext, + ) { + todo!() + // 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); - // } + // self.serialize_workspace(cx); + } // pub fn split_pane( // &mut self, @@ -2397,9 +2405,9 @@ impl Workspace { // pub fn split_pane_with_item( // &mut self, - // pane_to_split: WeakViewHandle, + // pane_to_split: WeakView, // split_direction: SplitDirection, - // from: WeakViewHandle, + // from: WeakView, // item_id_to_move: usize, // cx: &mut ViewContext, // ) { @@ -2420,7 +2428,7 @@ impl Workspace { // pub fn split_pane_with_project_entry( // &mut self, - // pane_to_split: WeakViewHandle, + // pane_to_split: WeakView, // split_direction: SplitDirection, // project_entry: ProjectEntryId, // cx: &mut ViewContext, @@ -2489,27 +2497,27 @@ impl Workspace { // } // } - // pub fn panes(&self) -> &[View] { - // &self.panes - // } + pub fn panes(&self) -> &[View] { + &self.panes + } - // pub fn active_pane(&self) -> &View { - // &self.active_pane - // } + pub fn active_pane(&self) -> &View { + &self.active_pane + } - // fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext) { - // self.follower_states.retain(|_, state| { - // if state.leader_id == peer_id { - // for item in state.items_by_leader_view_id.values() { - // item.set_leader_peer_id(None, cx); - // } - // false - // } else { - // true - // } - // }); - // cx.notify(); - // } + fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext) { + self.follower_states.retain(|_, state| { + if state.leader_id == peer_id { + for item in state.items_by_leader_view_id.values() { + item.set_leader_peer_id(None, cx); + } + false + } else { + true + } + }); + cx.notify(); + } // fn start_following( // &mut self, @@ -2650,37 +2658,33 @@ impl Workspace { // self.start_following(leader_id, cx) // } - // pub fn unfollow( - // &mut self, - // pane: &View, - // cx: &mut ViewContext, - // ) -> Option { - // let state = self.follower_states.remove(pane)?; - // let leader_id = state.leader_id; - // for (_, item) in state.items_by_leader_view_id { - // item.set_leader_peer_id(None, cx); - // } + pub fn unfollow(&mut self, pane: &View, cx: &mut ViewContext) -> Option { + let state = self.follower_states.remove(pane)?; + let leader_id = state.leader_id; + for (_, item) in state.items_by_leader_view_id { + item.set_leader_peer_id(None, cx); + } - // if self - // .follower_states - // .values() - // .all(|state| state.leader_id != state.leader_id) - // { - // let project_id = self.project.read(cx).remote_id(); - // let room_id = self.active_call()?.read(cx).room()?.read(cx).id(); - // self.app_state - // .client - // .send(proto::Unfollow { - // room_id, - // project_id, - // leader_id: Some(leader_id), - // }) - // .log_err(); - // } + 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) - // } + cx.notify(); + Some(leader_id) + } // pub fn is_being_followed(&self, peer_id: PeerId) -> bool { // self.follower_states @@ -2688,100 +2692,85 @@ impl Workspace { // .any(|state| state.leader_id == peer_id) // } - // fn render_titlebar(&self, theme: &Theme, cx: &mut ViewContext) -> AnyElement { - // // TODO: There should be a better system in place for this - // // (https://github.com/zed-industries/zed/issues/1290) - // let is_fullscreen = cx.window_is_fullscreen(); - // let container_theme = if is_fullscreen { - // let mut container_theme = theme.titlebar.container; - // container_theme.padding.left = container_theme.padding.right; - // container_theme - // } else { - // theme.titlebar.container - // }; + fn render_titlebar(&self, cx: &mut ViewContext) -> impl Component { + div() + .bg(cx.theme().colors().title_bar) + .when( + !matches!(cx.window_bounds(), WindowBounds::Fullscreen), + |s| s.pl_20(), + ) + .id("titlebar") + .on_click(|_, event, cx| { + if event.up.click_count == 2 { + cx.zoom_window(); + } + }) + .child("Collab title bar Item") // self.titlebar_item + } - // enum TitleBar {} - // MouseEventHandler::new::(0, cx, |_, cx| { - // Stack::new() - // .with_children( - // self.titlebar_item - // .as_ref() - // .map(|item| ChildView::new(item, cx)), - // ) - // .contained() - // .with_style(container_theme) - // }) - // .on_click(MouseButton::Left, |event, _, cx| { - // if event.click_count == 2 { - // cx.zoom_window(); - // } - // }) - // .constrained() - // .with_height(theme.titlebar.height) - // .into_any_named("titlebar") - // } + // fn active_item_path_changed(&mut self, cx: &mut ViewContext) { + // let active_entry = self.active_project_path(cx); + // self.project + // .update(cx, |project, cx| project.set_active_path(active_entry, cx)); + // self.update_window_title(cx); + // } - // fn active_item_path_changed(&mut self, cx: &mut ViewContext) { - // let active_entry = self.active_project_path(cx); - // self.project - // .update(cx, |project, cx| project.set_active_path(active_entry, cx)); - // self.update_window_title(cx); - // } + fn update_window_title(&mut self, cx: &mut ViewContext) { + let project = self.project().read(cx); + let mut title = String::new(); - // fn update_window_title(&mut self, cx: &mut ViewContext) { - // let project = self.project().read(cx); - // let mut title = String::new(); + if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) { + let filename = path + .path + .file_name() + .map(|s| s.to_string_lossy()) + .or_else(|| { + Some(Cow::Borrowed( + project + .worktree_for_id(path.worktree_id, cx)? + .read(cx) + .root_name(), + )) + }); - // if let Some(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(" — "); + } + } - // 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); + } - // 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 title.is_empty() { - // title = "empty project".to_string(); - // } + if project.is_remote() { + title.push_str(" ↙"); + } else if project.is_shared() { + title.push_str(" ↗"); + } - // if project.is_remote() { - // title.push_str(" ↙"); - // } else if project.is_shared() { - // title.push_str(" ↗"); - // } + // todo!() + // cx.set_window_title(&title); + } - // cx.set_window_title(&title); - // } - - // fn update_window_edited(&mut self, cx: &mut ViewContext) { - // let is_edited = !self.project.read(cx).is_read_only() - // && self - // .items(cx) - // .any(|item| item.has_conflict(cx) || item.is_dirty(cx)); - // if is_edited != self.window_edited { - // self.window_edited = is_edited; - // cx.set_window_edited(self.window_edited) - // } - // } + fn update_window_edited(&mut self, cx: &mut ViewContext) { + let is_edited = !self.project.read(cx).is_read_only() + && self + .items(cx) + .any(|item| item.has_conflict(cx) || item.is_dirty(cx)); + if is_edited != self.window_edited { + self.window_edited = is_edited; + todo!() + // cx.set_window_edited(self.window_edited) + } + } // fn render_disconnected_overlay( // &self, @@ -2838,298 +2827,300 @@ impl Workspace { // // RPC handlers - // fn handle_follow( - // &mut self, - // follower_project_id: Option, - // cx: &mut ViewContext, - // ) -> proto::FollowResponse { - // let client = &self.app_state.client; - // let project_id = self.project.read(cx).remote_id(); + fn handle_follow( + &mut self, + _follower_project_id: Option, + _cx: &mut ViewContext, + ) -> proto::FollowResponse { + todo!() - // let active_view_id = self.active_item(cx).and_then(|i| { - // Some( - // i.to_followable_item_handle(cx)? - // .remote_id(client, cx)? - // .to_proto(), - // ) - // }); + // let client = &self.app_state.client; + // let project_id = self.project.read(cx).remote_id(); - // cx.notify(); + // let active_view_id = self.active_item(cx).and_then(|i| { + // Some( + // i.to_followable_item_handle(cx)? + // .remote_id(client, cx)? + // .to_proto(), + // ) + // }); - // 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(), - // } - // } + // cx.notify(); - // fn handle_update_followers( - // &mut self, - // leader_id: PeerId, - // message: proto::UpdateFollowers, - // _cx: &mut ViewContext, - // ) { - // self.leader_updates_tx - // .unbounded_send((leader_id, message)) - // .ok(); - // } + // 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(), + // } + } - // async fn process_leader_update( - // this: &WeakViewHandle, - // leader_id: PeerId, - // update: proto::UpdateFollowers, - // cx: &mut AsyncAppContext, - // ) -> Result<()> { - // match update.variant.ok_or_else(|| anyhow!("invalid update"))? { - // proto::update_followers::Variant::UpdateActiveView(update_active_view) => { - // this.update(cx, |this, _| { - // for (_, state) in &mut this.follower_states { - // if state.leader_id == leader_id { - // state.active_view_id = - // if let Some(active_view_id) = update_active_view.id.clone() { - // Some(ViewId::from_proto(active_view_id)?) - // } else { - // None - // }; - // } - // } - // anyhow::Ok(()) - // })??; - // } - // proto::update_followers::Variant::UpdateView(update_view) => { - // let variant = update_view - // .variant - // .ok_or_else(|| anyhow!("missing update view variant"))?; - // let id = update_view - // .id - // .ok_or_else(|| anyhow!("missing update view id"))?; - // let mut tasks = Vec::new(); - // this.update(cx, |this, cx| { - // let project = this.project.clone(); - // for (_, state) in &mut this.follower_states { - // if state.leader_id == leader_id { - // let view_id = ViewId::from_proto(id.clone())?; - // if let Some(item) = state.items_by_leader_view_id.get(&view_id) { - // tasks.push(item.apply_update_proto(&project, variant.clone(), cx)); - // } - // } - // } - // anyhow::Ok(()) - // })??; - // try_join_all(tasks).await.log_err(); - // } - // proto::update_followers::Variant::CreateView(view) => { - // let panes = this.read_with(cx, |this, _| { - // this.follower_states - // .iter() - // .filter_map(|(pane, state)| (state.leader_id == leader_id).then_some(pane)) - // .cloned() - // .collect() - // })?; - // Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], cx).await?; - // } - // } - // this.update(cx, |this, cx| this.leader_updated(leader_id, cx))?; - // Ok(()) - // } + fn handle_update_followers( + &mut self, + leader_id: PeerId, + message: proto::UpdateFollowers, + _cx: &mut ViewContext, + ) { + self.leader_updates_tx + .unbounded_send((leader_id, message)) + .ok(); + } - // async fn add_views_from_leader( - // this: WeakViewHandle, - // leader_id: PeerId, - // panes: Vec>, - // views: Vec, - // cx: &mut AsyncAppContext, - // ) -> Result<()> { - // let this = this - // .upgrade(cx) - // .ok_or_else(|| anyhow!("workspace dropped"))?; + async fn process_leader_update( + this: &WeakView, + leader_id: PeerId, + update: proto::UpdateFollowers, + cx: &mut AsyncWindowContext, + ) -> 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.update(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(()) + } - // let item_builders = cx.update(|cx| { - // cx.default_global::() - // .values() - // .map(|b| b.0) - // .collect::>() - // }); + async fn add_views_from_leader( + this: WeakView, + leader_id: PeerId, + panes: Vec>, + views: Vec, + cx: &mut AsyncWindowContext, + ) -> Result<()> { + let this = this.upgrade().context("workspace dropped")?; - // 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()); - // } - // } - // } + let item_builders = cx.update(|_, cx| { + cx.default_global::() + .values() + .map(|b| b.0) + .collect::>() + })?; - // item_tasks_by_pane.insert(pane, (item_tasks, leader_view_ids)); - // } + 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()); + } + } + } - // 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); - // } + item_tasks_by_pane.insert(pane, (item_tasks, leader_view_ids)); + } - // Some(()) - // }); - // } - // Ok(()) - // } + 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); + } - // 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), - // }; - // } - // } + Some(()) + })?; + } + Ok(()) + } - // 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_active_view_for_followers(&mut self, cx: &mut ViewContext) { + 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), + }; + } + } - // 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) - // }) - // } + 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, + ); + } + } - // pub fn leader_for_pane(&self, pane: &View) -> Option { - // self.follower_states.get(pane).map(|state| state.leader_id) - // } + fn update_followers( + &self, + project_only: bool, + update: proto::update_followers::Variant, + cx: &mut WindowContext, + ) -> Option<()> { + let project_id = if project_only { + self.project.read(cx).remote_id() + } else { + None + }; + self.app_state().workspace_store.update(cx, |store, cx| { + store.update_followers(project_id, update, cx) + }) + } - // fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext) -> Option<()> { - // cx.notify(); + pub fn leader_for_pane(&self, pane: &View) -> Option { + self.follower_states.get(pane).map(|state| state.leader_id) + } - // 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(); + fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext) -> Option<()> { + cx.notify(); - // 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; - // } - // }; + 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(); - // 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))); - // } - // } + let leader_in_this_app; + let leader_in_this_project; + match participant.location { + call2::ParticipantLocation::SharedProject { project_id } => { + leader_in_this_app = true; + leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id(); + } + call2::ParticipantLocation::UnsharedProject => { + leader_in_this_app = true; + leader_in_this_project = false; + } + call2::ParticipantLocation::External => { + leader_in_this_app = false; + leader_in_this_project = false; + } + }; - // 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) - // }); - // } + 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; + } + // todo!() + // if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) { + // items_to_activate.push((pane.clone(), Box::new(shared_screen))); + // } + } - // if pane_was_focused { - // pane.update(cx, |pane, cx| pane.focus_active_item(cx)); - // } - // } + 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) + }); + } - // None - // } + if pane_was_focused { + pane.update(cx, |pane, cx| pane.focus_active_item(cx)); + } + } + + None + } // fn shared_screen_for_peer( // &self, @@ -3149,95 +3140,98 @@ impl Workspace { // } // } - // Some(cx.add_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx))) + // Some(cx.build_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx))) // } - // pub fn on_window_activation_changed(&mut self, active: bool, cx: &mut ViewContext) { - // if active { - // self.update_active_view_for_followers(cx); - // cx.background() - // .spawn(persistence::DB.update_timestamp(self.database_id())) - // .detach(); - // } else { - // for pane in &self.panes { - // pane.update(cx, |pane, cx| { - // if let Some(item) = pane.active_item() { - // item.workspace_deactivated(cx); - // } - // if matches!( - // settings::get::(cx).autosave, - // AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange - // ) { - // for item in pane.items() { - // Pane::autosave_item(item.as_ref(), self.project.clone(), cx) - // .detach_and_log_err(cx); - // } - // } - // }); - // } - // } - // } + pub fn on_window_activation_changed(&mut self, cx: &mut ViewContext) { + if cx.is_window_active() { + self.update_active_view_for_followers(cx); + cx.background_executor() + .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!( + WorkspaceSettings::get_global(cx).autosave, + AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange + ) { + for item in pane.items() { + Pane::autosave_item(item.as_ref(), self.project.clone(), cx) + .detach_and_log_err(cx); + } + } + }); + } + } + } - // fn active_call(&self) -> Option<&ModelHandle> { - // self.active_call.as_ref().map(|(call, _)| call) - // } + fn active_call(&self) -> Option<&Model> { + self.active_call.as_ref().map(|(call, _)| call) + } - // fn on_active_call_event( - // &mut self, - // _: ModelHandle, - // event: &call::room::Event, - // cx: &mut ViewContext, - // ) { - // match event { - // call::room::Event::ParticipantLocationChanged { participant_id } - // | call::room::Event::RemoteVideoTracksChanged { participant_id } => { - // self.leader_updated(*participant_id, cx); - // } - // _ => {} - // } - // } + fn on_active_call_event( + &mut self, + _: Model, + event: &call2::room::Event, + cx: &mut ViewContext, + ) { + match event { + call2::room::Event::ParticipantLocationChanged { participant_id } + | call2::room::Event::RemoteVideoTracksChanged { participant_id } => { + self.leader_updated(*participant_id, cx); + } + _ => {} + } + } - // pub fn database_id(&self) -> WorkspaceId { - // self.database_id - // } + pub fn database_id(&self) -> WorkspaceId { + self.database_id + } - // fn location(&self, cx: &AppContext) -> Option { - // let project = self.project().read(cx); + fn location(&self, cx: &AppContext) -> Option { + let project = self.project().read(cx); - // if project.is_local() { - // Some( - // project - // .visible_worktrees(cx) - // .map(|worktree| worktree.read(cx).abs_path()) - // .collect::>() - // .into(), - // ) - // } else { - // None - // } - // } + if project.is_local() { + Some( + project + .visible_worktrees(cx) + .map(|worktree| worktree.read(cx).abs_path()) + .collect::>() + .into(), + ) + } else { + None + } + } - // fn remove_panes(&mut self, member: Member, cx: &mut ViewContext) { - // match member { - // Member::Axis(PaneAxis { members, .. }) => { - // for child in members.iter() { - // self.remove_panes(child.clone(), cx) - // } - // } - // Member::Pane(pane) => { - // self.force_remove_pane(&pane, cx); - // } - // } - // } + fn remove_panes(&mut self, member: Member, cx: &mut ViewContext) { + match member { + Member::Axis(PaneAxis { members, .. }) => { + for child in members.iter() { + self.remove_panes(child.clone(), cx) + } + } + Member::Pane(pane) => { + self.force_remove_pane(&pane, cx); + } + } + } - // fn force_remove_pane(&mut self, pane: &View, cx: &mut ViewContext) { - // self.panes.retain(|p| p != pane); - // cx.focus(self.panes.last().unwrap()); - // if self.last_active_center_pane == Some(pane.downgrade()) { - // self.last_active_center_pane = None; - // } - // cx.notify(); - // } + fn force_remove_pane(&mut self, pane: &View, cx: &mut ViewContext) { + self.panes.retain(|p| p != pane); + if true { + todo!() + // cx.focus(self.panes.last().unwrap()); + } + if self.last_active_center_pane == Some(pane.downgrade()) { + self.last_active_center_pane = None; + } + cx.notify(); + } // fn schedule_serialize(&mut self, cx: &mut ViewContext) { // self._schedule_serialize = Some(cx.spawn(|this, cx| async move { @@ -3247,263 +3241,262 @@ impl Workspace { // })); // } - // fn serialize_workspace(&self, cx: &ViewContext) { - // fn serialize_pane_handle( - // pane_handle: &View, - // cx: &AppContext, - // ) -> SerializedPane { - // let (items, active) = { - // let pane = pane_handle.read(cx); - // let active_item_id = pane.active_item().map(|item| item.id()); - // ( - // pane.items() - // .filter_map(|item_handle| { - // Some(SerializedItem { - // kind: Arc::from(item_handle.serialized_item_kind()?), - // item_id: item_handle.id(), - // active: Some(item_handle.id()) == active_item_id, - // }) - // }) - // .collect::>(), - // pane.has_focus(), - // ) - // }; + fn serialize_workspace(&self, cx: &mut ViewContext) { + fn serialize_pane_handle(pane_handle: &View, 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().as_u64() as usize, + active: Some(item_handle.id()) == active_item_id, + }) + }) + .collect::>(), + pane.has_focus(), + ) + }; - // SerializedPane::new(items, active) - // } + SerializedPane::new(items, active) + } - // fn build_serialized_pane_group( - // pane_group: &Member, - // cx: &AppContext, - // ) -> SerializedPaneGroup { - // match pane_group { - // Member::Axis(PaneAxis { - // axis, - // members, - // flexes, - // bounding_boxes: _, - // }) => SerializedPaneGroup::Group { - // axis: *axis, - // children: members - // .iter() - // .map(|member| build_serialized_pane_group(member, cx)) - // .collect::>(), - // flexes: Some(flexes.borrow().clone()), - // }, - // Member::Pane(pane_handle) => { - // SerializedPaneGroup::Pane(serialize_pane_handle(&pane_handle, cx)) - // } - // } - // } + fn build_serialized_pane_group( + pane_group: &Member, + cx: &AppContext, + ) -> SerializedPaneGroup { + match pane_group { + Member::Axis(PaneAxis { + axis, + members, + flexes, + bounding_boxes: _, + }) => SerializedPaneGroup::Group { + axis: *axis, + children: members + .iter() + .map(|member| build_serialized_pane_group(member, cx)) + .collect::>(), + flexes: Some(flexes.lock().clone()), + }, + Member::Pane(pane_handle) => { + SerializedPaneGroup::Pane(serialize_pane_handle(&pane_handle, cx)) + } + } + } - // fn build_serialized_docks(this: &Workspace, cx: &ViewContext) -> DockStructure { - // let left_dock = this.left_dock.read(cx); - // let left_visible = left_dock.is_open(); - // let left_active_panel = left_dock.visible_panel().and_then(|panel| { - // Some( - // cx.view_ui_name(panel.as_any().window(), panel.id())? - // .to_string(), - // ) - // }); - // let left_dock_zoom = left_dock - // .visible_panel() - // .map(|panel| panel.is_zoomed(cx)) - // .unwrap_or(false); + fn build_serialized_docks( + this: &Workspace, + cx: &mut ViewContext, + ) -> DockStructure { + let left_dock = this.left_dock.read(cx); + let left_visible = left_dock.is_open(); + let left_active_panel = left_dock + .visible_panel() + .and_then(|panel| Some(panel.persistent_name(cx).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 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(panel.persistent_name(cx).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); + 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(panel.persistent_name(cx).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, - // }, - // } - // } + 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); + 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, - // }; + 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(); - // } - // } - // } + cx.spawn(|_, _| persistence::DB.save_workspace(serialized_workspace)) + .detach(); + } + } + } - // pub(crate) fn load_workspace( - // workspace: WeakViewHandle, - // serialized_workspace: SerializedWorkspace, - // paths_to_open: Vec>, - // cx: &mut AppContext, - // ) -> Task>>>> { - // cx.spawn(|mut cx| async move { - // let (project, old_center_pane) = workspace.read_with(&cx, |workspace, _| { - // ( - // workspace.project().clone(), - // workspace.last_active_center_pane.clone(), - // ) - // })?; + pub(crate) fn load_workspace( + serialized_workspace: SerializedWorkspace, + paths_to_open: Vec>, + cx: &mut ViewContext, + ) -> Task>>>> { + cx.spawn(|workspace, mut cx| async move { + let (project, old_center_pane) = workspace.update(&mut 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 center_group = None; + let mut center_items = None; - // 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::>() - // }); + // 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.clone(), + &mut cx, + ) + .await + { + center_items = Some(items); + center_group = Some((group, active_pane)) + } - // 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::>(); + let mut items_by_project_path = cx.update(|_, 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::>() + })?; - // // 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); + 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::>(); - // // Swap workspace center group - // workspace.center = PaneGroup::with_root(center_group); + // 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); - // // Change the focus to the workspace first so that we retrigger focus in on the pane. - // cx.focus_self(); + // Swap workspace center group + workspace.center = PaneGroup::with_root(center_group); - // 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() - // } - // } + // Change the focus to the workspace first so that we retrigger focus in on the pane. + // todo!() + // cx.focus_self(); + // if let Some(active_pane) = active_pane { + // cx.focus(&active_pane); + // } else { + // cx.focus(workspace.panes.last().unwrap()); + // } + } else { + // todo!() + // let old_center_handle = old_center_pane.and_then(|weak| weak.upgrade()); + // 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)); + 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 { + // todo!() + // 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); - // } - // } + if docks.right.visible && docks.right.zoom { + // todo!() + // 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)); + dock.active_panel() + .map(|panel| panel.set_zoomed(docks.bottom.zoom, cx)); - // if docks.bottom.visible && docks.bottom.zoom { - // cx.focus_self() - // } - // }); + if docks.bottom.visible && docks.bottom.zoom { + // todo!() + // cx.focus_self() + } + }); - // cx.notify(); - // })?; + 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))?; + // Serialize ourself to make sure our timestamps and any pane / item changes are replicated + workspace.update(&mut cx, |workspace, cx| workspace.serialize_workspace(cx))?; - // Ok(opened_items) - // }) - // } + Ok(opened_items) + }) + } // #[cfg(any(test, feature = "test-support"))] // pub fn test_new(project: ModelHandle, cx: &mut ViewContext) -> Self { @@ -3556,209 +3549,372 @@ impl Workspace { // ) // } // } - - // fn window_bounds_env_override(cx: &AsyncAppContext) -> Option { - // ZED_WINDOW_POSITION - // .zip(*ZED_WINDOW_SIZE) - // .map(|(position, size)| { - // WindowBounds::Fixed(RectF::new( - // cx.platform().screens()[0].bounds().origin() + position, - // size, - // )) - // }) - // } - - // async fn open_items( - // serialized_workspace: Option, - // workspace: &WeakViewHandle, - // mut project_paths_to_open: Vec<(PathBuf, Option)>, - // app_state: Arc, - // mut cx: AsyncAppContext, - // ) -> Result>>>> { - // let mut opened_items = Vec::with_capacity(project_paths_to_open.len()); - - // if let Some(serialized_workspace) = serialized_workspace { - // let workspace = workspace.clone(); - // let restored_items = cx - // .update(|cx| { - // Workspace::load_workspace( - // workspace, - // serialized_workspace, - // project_paths_to_open - // .iter() - // .map(|(_, project_path)| project_path) - // .cloned() - // .collect(), - // cx, - // ) - // }) - // .await?; - - // let restored_project_paths = cx.read(|cx| { - // restored_items - // .iter() - // .filter_map(|item| item.as_ref()?.project_path(cx)) - // .collect::>() - // }); - - // for restored_item in restored_items { - // opened_items.push(restored_item.map(Ok)); - // } - - // project_paths_to_open - // .iter_mut() - // .for_each(|(_, project_path)| { - // if let Some(project_path_to_open) = project_path { - // if restored_project_paths.contains(project_path_to_open) { - // *project_path = None; - // } - // } - // }); - // } else { - // for _ in 0..project_paths_to_open.len() { - // opened_items.push(None); - // } - // } - // assert!(opened_items.len() == project_paths_to_open.len()); - - // let tasks = - // project_paths_to_open - // .into_iter() - // .enumerate() - // .map(|(i, (abs_path, project_path))| { - // let workspace = workspace.clone(); - // cx.spawn(|mut cx| { - // let fs = app_state.fs.clone(); - // async move { - // let file_project_path = project_path?; - // if fs.is_file(&abs_path).await { - // Some(( - // i, - // workspace - // .update(&mut cx, |workspace, cx| { - // workspace.open_path(file_project_path, None, true, cx) - // }) - // .log_err()? - // .await, - // )) - // } else { - // None - // } - // } - // }) - // }); - - // for maybe_opened_path in futures::future::join_all(tasks.into_iter()) - // .await - // .into_iter() - // { - // if let Some((i, path_open_result)) = maybe_opened_path { - // opened_items[i] = Some(path_open_result); - // } - // } - - // Ok(opened_items) - // } - - // fn notify_of_new_dock(workspace: &WeakViewHandle, cx: &mut AsyncAppContext) { - // const NEW_PANEL_BLOG_POST: &str = "https://zed.dev/blog/new-panel-system"; - // const NEW_DOCK_HINT_KEY: &str = "show_new_dock_key"; - // const MESSAGE_ID: usize = 2; - - // if workspace - // .read_with(cx, |workspace, cx| { - // workspace.has_shown_notification_once::(MESSAGE_ID, cx) - // }) - // .unwrap_or(false) - // { - // return; - // } - - // if db::kvp::KEY_VALUE_STORE - // .read_kvp(NEW_DOCK_HINT_KEY) - // .ok() - // .flatten() - // .is_some() - // { - // if !workspace - // .read_with(cx, |workspace, cx| { - // workspace.has_shown_notification_once::(MESSAGE_ID, cx) - // }) - // .unwrap_or(false) - // { - // cx.update(|cx| { - // cx.update_global::(|tracker, _| { - // let entry = tracker - // .entry(TypeId::of::()) - // .or_default(); - // if !entry.contains(&MESSAGE_ID) { - // entry.push(MESSAGE_ID); - // } - // }); - // }); - // } - - // return; - // } - - // cx.spawn(|_| async move { - // db::kvp::KEY_VALUE_STORE - // .write_kvp(NEW_DOCK_HINT_KEY.to_string(), "seen".to_string()) - // .await - // .ok(); - // }) - // .detach(); - - // workspace - // .update(cx, |workspace, cx| { - // workspace.show_notification_once(2, cx, |cx| { - // cx.add_view(|_| { - // MessageNotification::new_element(|text, _| { - // Text::new( - // "Looking for the dock? Try ctrl-`!\nshift-escape now zooms your pane.", - // text, - // ) - // .with_custom_runs(vec![26..32, 34..46], |_, bounds, cx| { - // let code_span_background_color = settings::get::(cx) - // .theme - // .editor - // .document_highlight_read_background; - - // cx.scene().push_quad(gpui::Quad { - // bounds, - // background: Some(code_span_background_color), - // border: Default::default(), - // corner_radii: (2.0).into(), - // }) - // }) - // .into_any() - // }) - // .with_click_message("Read more about the new panel system") - // .on_click(|cx| cx.platform().open_url(NEW_PANEL_BLOG_POST)) - // }) - // }) - // }) - // .ok(); } -// fn notify_if_database_failed(workspace: &WeakViewHandle, cx: &mut AsyncAppContext) { -// const REPORT_ISSUE_URL: &str ="https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml"; +fn window_bounds_env_override(cx: &AsyncAppContext) -> Option { + let display_origin = cx + .update(|cx| Some(cx.displays().first()?.bounds().origin)) + .ok()??; + ZED_WINDOW_POSITION + .zip(*ZED_WINDOW_SIZE) + .map(|(position, size)| { + WindowBounds::Fixed(Bounds { + origin: display_origin + position, + size, + }) + }) +} + +fn open_items( + serialized_workspace: Option, + mut project_paths_to_open: Vec<(PathBuf, Option)>, + app_state: Arc, + cx: &mut ViewContext, +) -> impl 'static + Future>>>>> { + let restored_items = serialized_workspace.map(|serialized_workspace| { + Workspace::load_workspace( + serialized_workspace, + project_paths_to_open + .iter() + .map(|(_, project_path)| project_path) + .cloned() + .collect(), + cx, + ) + }); + + cx.spawn(|workspace, mut cx| async move { + let mut opened_items = Vec::with_capacity(project_paths_to_open.len()); + + if let Some(restored_items) = restored_items { + let restored_items = restored_items.await?; + + let restored_project_paths = restored_items + .iter() + .filter_map(|item| { + cx.update(|_, cx| item.as_ref()?.project_path(cx)) + .ok() + .flatten() + }) + .collect::>(); + + for restored_item in restored_items { + opened_items.push(restored_item.map(Ok)); + } + + project_paths_to_open + .iter_mut() + .for_each(|(_, project_path)| { + if let Some(project_path_to_open) = project_path { + if restored_project_paths.contains(project_path_to_open) { + *project_path = None; + } + } + }); + } else { + for _ in 0..project_paths_to_open.len() { + opened_items.push(None); + } + } + assert!(opened_items.len() == project_paths_to_open.len()); + + let tasks = + project_paths_to_open + .into_iter() + .enumerate() + .map(|(i, (abs_path, project_path))| { + let workspace = workspace.clone(); + cx.spawn(|mut cx| { + let fs = app_state.fs.clone(); + async move { + let file_project_path = project_path?; + if fs.is_file(&abs_path).await { + Some(( + i, + workspace + .update(&mut cx, |workspace, cx| { + workspace.open_path(file_project_path, None, true, cx) + }) + .log_err()? + .await, + )) + } else { + None + } + } + }) + }); + + let tasks = tasks.collect::>(); + + let tasks = futures::future::join_all(tasks.into_iter()); + for maybe_opened_path in tasks.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: &WeakView, cx: &mut AsyncAppContext) { +// const NEW_PANEL_BLOG_POST: &str = "https://zed.dev/blog/new-panel-system"; +// const NEW_DOCK_HINT_KEY: &str = "show_new_dock_key"; +// const MESSAGE_ID: usize = 2; + +// if workspace +// .read_with(cx, |workspace, cx| { +// workspace.has_shown_notification_once::(MESSAGE_ID, cx) +// }) +// .unwrap_or(false) +// { +// return; +// } + +// if db::kvp::KEY_VALUE_STORE +// .read_kvp(NEW_DOCK_HINT_KEY) +// .ok() +// .flatten() +// .is_some() +// { +// if !workspace +// .read_with(cx, |workspace, cx| { +// workspace.has_shown_notification_once::(MESSAGE_ID, cx) +// }) +// .unwrap_or(false) +// { +// cx.update(|cx| { +// cx.update_global::(|tracker, _| { +// let entry = tracker +// .entry(TypeId::of::()) +// .or_default(); +// if !entry.contains(&MESSAGE_ID) { +// entry.push(MESSAGE_ID); +// } +// }); +// }); +// } + +// return; +// } + +// cx.spawn(|_| async move { +// db::kvp::KEY_VALUE_STORE +// .write_kvp(NEW_DOCK_HINT_KEY.to_string(), "seen".to_string()) +// .await +// .ok(); +// }) +// .detach(); // workspace // .update(cx, |workspace, cx| { -// 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(); -// } +// workspace.show_notification_once(2, cx, |cx| { +// cx.build_view(|_| { +// MessageNotification::new_element(|text, _| { +// Text::new( +// "Looking for the dock? Try ctrl-`!\nshift-escape now zooms your pane.", +// text, +// ) +// .with_custom_runs(vec![26..32, 34..46], |_, bounds, cx| { +// let code_span_background_color = settings::get::(cx) +// .theme +// .editor +// .document_highlight_read_background; +// cx.scene().push_quad(gpui::Quad { +// bounds, +// background: Some(code_span_background_color), +// border: Default::default(), +// corner_radii: (2.0).into(), +// }) +// }) +// .into_any() +// }) +// .with_click_message("Read more about the new panel system") +// .on_click(|cx| cx.platform().open_url(NEW_PANEL_BLOG_POST)) +// }) +// }) +// }) +// .ok(); + +fn notify_if_database_failed(_workspace: WindowHandle, _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"; + + // todo!() + // 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.build_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 EventEmitter for Workspace { + type Event = Event; +} + +impl Render for Workspace { + type Element = Div; + + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + div() + .relative() + .size_full() + .flex() + .flex_col() + .font("Zed Sans") + .gap_0() + .justify_start() + .items_start() + .text_color(cx.theme().colors().text) + .bg(cx.theme().colors().background) + .child(self.render_titlebar(cx)) + .child( + div() + .flex_1() + .w_full() + .flex() + .flex_row() + .overflow_hidden() + .border_t() + .border_b() + .border_color(cx.theme().colors().border) + // .children( + // Some( + // Panel::new("project-panel-outer", cx) + // .side(PanelSide::Left) + // .child(ProjectPanel::new("project-panel-inner")), + // ) + // .filter(|_| self.is_project_panel_open()), + // ) + // .children( + // Some( + // Panel::new("collab-panel-outer", cx) + // .child(CollabPanel::new("collab-panel-inner")) + // .side(PanelSide::Left), + // ) + // .filter(|_| self.is_collab_panel_open()), + // ) + // .child(NotificationToast::new( + // "maxbrunsfeld has requested to add you as a contact.".into(), + // )) + .child( + div().flex().flex_col().flex_1().h_full().child( + div().flex().flex_1().child(self.center.render( + &self.project, + &self.follower_states, + self.active_call(), + &self.active_pane, + self.zoomed.as_ref(), + &self.app_state, + cx, + )), + ), // .children( + // Some( + // Panel::new("terminal-panel", cx) + // .child(Terminal::new()) + // .allowed_sides(PanelAllowedSides::BottomOnly) + // .side(PanelSide::Bottom), + // ) + // .filter(|_| self.is_terminal_open()), + // ), + ), // .children( + // Some( + // Panel::new("chat-panel-outer", cx) + // .side(PanelSide::Right) + // .child(ChatPanel::new("chat-panel-inner").messages(vec![ + // ChatMessage::new( + // "osiewicz".to_string(), + // "is this thing on?".to_string(), + // DateTime::parse_from_rfc3339("2023-09-27T15:40:52.707Z") + // .unwrap() + // .naive_local(), + // ), + // ChatMessage::new( + // "maxdeviant".to_string(), + // "Reading you loud and clear!".to_string(), + // DateTime::parse_from_rfc3339("2023-09-28T15:40:52.707Z") + // .unwrap() + // .naive_local(), + // ), + // ])), + // ) + // .filter(|_| self.is_chat_panel_open()), + // ) + // .children( + // Some( + // Panel::new("notifications-panel-outer", cx) + // .side(PanelSide::Right) + // .child(NotificationsPanel::new("notifications-panel-inner")), + // ) + // .filter(|_| self.is_notifications_panel_open()), + // ) + // .children( + // Some( + // Panel::new("assistant-panel-outer", cx) + // .child(AssistantPanel::new("assistant-panel-inner")), + // ) + // .filter(|_| self.is_assistant_panel_open()), + // ), + ) + .child(self.status_bar.clone()) + // .when(self.debug.show_toast, |this| { + // this.child(Toast::new(ToastOrigin::Bottom).child(Label::new("A toast"))) + // }) + // .children( + // Some( + // div() + // .absolute() + // .top(px(50.)) + // .left(px(640.)) + // .z_index(8) + // .child(LanguageSelector::new("language-selector")), + // ) + // .filter(|_| self.is_language_selector_open()), + // ) + .z_index(8) + // Debug + .child( + div() + .flex() + .flex_col() + .z_index(9) + .absolute() + .top_20() + .left_1_4() + .w_40() + .gap_2(), // .when(self.show_debug, |this| { + // this.child(Button::::new("Toggle User Settings").on_click( + // Arc::new(|workspace, cx| workspace.debug_toggle_user_settings(cx)), + // )) + // .child( + // Button::::new("Toggle Toasts").on_click(Arc::new( + // |workspace, cx| workspace.debug_toggle_toast(cx), + // )), + // ) + // .child( + // Button::::new("Toggle Livestream").on_click(Arc::new( + // |workspace, cx| workspace.debug_toggle_livestream(cx), + // )), + // ) + // }) + // .child( + // Button::::new("Toggle Debug") + // .on_click(Arc::new(|workspace, cx| workspace.toggle_debug(cx))), + // ), + ) + } +} + +// todo!() // impl Entity for Workspace { // type Event = Event; @@ -3909,168 +4065,165 @@ impl Workspace { // } // } -// impl WorkspaceStore { -// pub fn new(client: Arc, cx: &mut ModelContext) -> Self { -// Self { -// workspaces: Default::default(), -// followers: Default::default(), -// _subscriptions: vec![ -// client.add_request_handler(cx.handle(), Self::handle_follow), -// client.add_message_handler(cx.handle(), Self::handle_unfollow), -// client.add_message_handler(cx.handle(), Self::handle_update_followers), -// ], -// client, -// } -// } +impl WorkspaceStore { + pub fn new(client: Arc, _cx: &mut ModelContext) -> Self { + Self { + workspaces: Default::default(), + followers: Default::default(), + _subscriptions: vec![], + // client.add_request_handler(cx.weak_model(), Self::handle_follow), + // client.add_message_handler(cx.weak_model(), Self::handle_unfollow), + // client.add_message_handler(cx.weak_model(), Self::handle_update_followers), + // ], + client, + } + } -// pub fn update_followers( -// &self, -// project_id: Option, -// update: proto::update_followers::Variant, -// cx: &AppContext, -// ) -> Option<()> { -// if !cx.has_global::>() { -// return None; -// } + pub fn update_followers( + &self, + project_id: Option, + update: proto::update_followers::Variant, + cx: &AppContext, + ) -> Option<()> { + if !cx.has_global::>() { + return None; + } -// let room_id = ActiveCall::global(cx).read(cx).room()?.read(cx).id(); -// let follower_ids: Vec<_> = self -// .followers -// .iter() -// .filter_map(|follower| { -// if follower.project_id == project_id || project_id.is_none() { -// Some(follower.peer_id.into()) -// } else { -// None -// } -// }) -// .collect(); -// if follower_ids.is_empty() { -// return None; -// } -// self.client -// .send(proto::UpdateFollowers { -// room_id, -// project_id, -// follower_ids, -// variant: Some(update), -// }) -// .log_err() -// } + let room_id = ActiveCall::global(cx).read(cx).room()?.read(cx).id(); + let follower_ids: Vec<_> = self + .followers + .iter() + .filter_map(|follower| { + if follower.project_id == project_id || project_id.is_none() { + Some(follower.peer_id.into()) + } else { + None + } + }) + .collect(); + if follower_ids.is_empty() { + return None; + } + self.client + .send(proto::UpdateFollowers { + room_id, + project_id, + follower_ids, + variant: Some(update), + }) + .log_err() + } -// async fn handle_follow( -// this: ModelHandle, -// envelope: TypedEnvelope, -// _: Arc, -// mut cx: AsyncAppContext, -// ) -> Result { -// this.update(&mut cx, |this, cx| { -// let follower = Follower { -// project_id: envelope.payload.project_id, -// peer_id: envelope.original_sender_id()?, -// }; -// let active_project = ActiveCall::global(cx) -// .read(cx) -// .location() -// .map(|project| project.id()); + pub async fn handle_follow( + this: Model, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + this.update(&mut cx, |this, cx| { + let follower = Follower { + project_id: envelope.payload.project_id, + peer_id: envelope.original_sender_id()?, + }; + let active_project = ActiveCall::global(cx).read(cx).location().cloned(); -// let mut response = proto::FollowResponse::default(); -// for workspace in &this.workspaces { -// let Some(workspace) = workspace.upgrade(cx) else { -// continue; -// }; + let mut response = proto::FollowResponse::default(); + for workspace in &this.workspaces { + workspace + .update(cx, |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); + } -// 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.downgrade()) == active_project + { + response.active_view_id = Some(active_view_id); + } + } + }) + .ok(); + } -// 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); + } -// if let Err(ix) = this.followers.binary_search(&follower) { -// this.followers.insert(ix, follower); -// } + Ok(response) + })? + } -// Ok(response) -// }) -// } + async fn handle_unfollow( + model: Model, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + model.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_unfollow( -// this: ModelHandle, -// envelope: TypedEnvelope, -// _: Arc, -// mut cx: AsyncAppContext, -// ) -> Result<()> { -// this.update(&mut cx, |this, _| { -// let follower = Follower { -// project_id: envelope.payload.project_id, -// peer_id: envelope.original_sender_id()?, -// }; -// if let Ok(ix) = this.followers.binary_search(&follower) { -// this.followers.remove(ix); -// } -// Ok(()) -// }) -// } + async fn handle_update_followers( + _this: Model, + _envelope: TypedEnvelope, + _: Arc, + mut _cx: AsyncWindowContext, + ) -> Result<()> { + // let leader_id = envelope.original_sender_id()?; + // let update = envelope.payload; -// async fn handle_update_followers( -// this: ModelHandle, -// envelope: TypedEnvelope, -// _: Arc, -// mut cx: AsyncAppContext, -// ) -> Result<()> { -// let leader_id = envelope.original_sender_id()?; -// let update = envelope.payload; -// this.update(&mut cx, |this, cx| { -// for workspace in &this.workspaces { -// let Some(workspace) = workspace.upgrade(cx) else { -// continue; -// }; -// workspace.update(cx.as_mut(), |workspace, cx| { -// let project_id = workspace.project.read(cx).remote_id(); -// if update.project_id != project_id && update.project_id.is_some() { -// return; -// } -// workspace.handle_update_followers(leader_id, update.clone(), cx); -// }); -// } -// Ok(()) -// }) -// } -// } + // this.update(&mut cx, |this, cx| { + // for workspace in &this.workspaces { + // let Some(workspace) = workspace.upgrade() else { + // continue; + // }; + // workspace.update(cx, |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(()) + // })? + todo!() + } +} // impl Entity for WorkspaceStore { // type Event = (); // } -// impl ViewId { -// pub(crate) fn from_proto(message: proto::ViewId) -> Result { -// Ok(Self { -// creator: message -// .creator -// .ok_or_else(|| anyhow!("creator is missing"))?, -// id: message.id, -// }) -// } +impl ViewId { + pub(crate) fn from_proto(message: proto::ViewId) -> Result { + Ok(Self { + creator: message + .creator + .ok_or_else(|| anyhow!("creator is missing"))?, + id: message.id, + }) + } -// pub(crate) fn to_proto(&self) -> proto::ViewId { -// proto::ViewId { -// creator: Some(self.creator), -// id: self.id, -// } -// } -// } + pub(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; @@ -4099,45 +4252,41 @@ impl Workspace { // } // } -// pub struct WorkspaceCreated(pub WeakViewHandle); +// pub struct WorkspaceCreated(pub WeakView); -pub async fn activate_workspace_for_project( - cx: &mut AsyncAppContext, +pub fn activate_workspace_for_project( + cx: &mut AppContext, predicate: impl Fn(&Project, &AppContext) -> bool + Send + 'static, ) -> Option> { - cx.run_on_main(move |cx| { - for window in cx.windows() { - let Some(workspace) = window.downcast::() else { - continue; - }; + for window in cx.windows() { + let Some(workspace) = window.downcast::() else { + continue; + }; - let predicate = cx - .update_window_root(&workspace, |workspace, cx| { - let project = workspace.project.read(cx); - if predicate(project, cx) { - cx.activate_window(); - true - } else { - false - } - }) - .log_err() - .unwrap_or(false); + let predicate = workspace + .update(cx, |workspace, cx| { + let project = workspace.project.read(cx); + if predicate(project, cx) { + cx.activate_window(); + true + } else { + false + } + }) + .log_err() + .unwrap_or(false); - if predicate { - return Some(workspace); - } + if predicate { + return Some(workspace); } + } - None - }) - .ok()? - .await + None } -// pub async fn last_opened_workspace_paths() -> Option { -// DB.last_workspace().await.log_err().flatten() -// } +pub async fn last_opened_workspace_paths() -> Option { + DB.last_workspace().await.log_err().flatten() +} // async fn join_channel_internal( // channel_id: u64, @@ -4321,27 +4470,6 @@ pub async fn activate_workspace_for_project( // None // } -use client2::{ - proto::{self, PeerId, ViewId}, - Client, UserStore, -}; -use collections::{HashMap, HashSet}; -use gpui2::{ - AnyHandle, AnyView, AppContext, AsyncAppContext, DisplayId, Handle, MainThread, Task, View, - ViewContext, WeakHandle, WeakView, WindowBounds, WindowHandle, WindowOptions, -}; -use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem}; -use language2::LanguageRegistry; -use node_runtime::NodeRuntime; -use project2::{Project, ProjectEntryId, ProjectPath, Worktree}; -use std::{ - any::TypeId, - path::{Path, PathBuf}, - sync::Arc, - time::Duration, -}; -use util::ResultExt; - #[allow(clippy::type_complexity)] pub fn open_paths( abs_paths: &[PathBuf], @@ -4356,50 +4484,48 @@ pub fn open_paths( > { 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) - }) - .await; - + // Open paths in existing workspace if possible + let existing = activate_workspace_for_project(cx, { + let abs_paths = abs_paths.clone(); + move |project, cx| project.contains_paths(&abs_paths, cx) + }); + cx.spawn(move |mut cx| async move { if let Some(existing) = existing { - Ok(( - existing.clone(), - cx.update_window_root(&existing, |workspace, cx| { - workspace.open_paths(abs_paths, true, cx) - })? - .await, - )) - } else { + // // Ok(( + // existing.clone(), + // cx.update_window_root(&existing, |workspace, cx| { + // workspace.open_paths(abs_paths, true, cx) + // })? + // .await, + // )) todo!() - // Ok(cx - // .update(|cx| { - // Workspace::new_local(abs_paths, app_state.clone(), requesting_window, cx) - // }) - // .await) + } else { + cx.update(move |cx| { + Workspace::new_local(abs_paths, app_state.clone(), requesting_window, cx) + })? + .await } }) } -// pub fn open_new( -// app_state: &Arc, -// cx: &mut AppContext, -// init: impl FnOnce(&mut Workspace, &mut ViewContext) + 'static, -// ) -> Task<()> { -// let task = Workspace::new_local(Vec::new(), app_state.clone(), None, cx); -// cx.spawn(|mut cx| async move { -// let (workspace, opened_paths) = task.await; - -// workspace -// .update(&mut cx, |workspace, cx| { -// if opened_paths.is_empty() { -// init(workspace, cx) -// } -// }) -// .log_err(); -// }) -// } +pub fn open_new( + app_state: &Arc, + cx: &mut AppContext, + init: impl FnOnce(&mut Workspace, &mut ViewContext) + 'static + Send, +) -> Task<()> { + let task = Workspace::new_local(Vec::new(), app_state.clone(), None, cx); + cx.spawn(|mut cx| async move { + if let Some((workspace, opened_paths)) = task.await.log_err() { + 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, @@ -4569,12 +4695,19 @@ pub fn open_paths( // .detach_and_log_err(cx); // } -// fn parse_pixel_position_env_var(value: &str) -> Option { -// let mut parts = value.split(','); -// let width: usize = parts.next()?.parse().ok()?; -// let height: usize = parts.next()?.parse().ok()?; -// Some(vec2f(width as f32, height as f32)) -// } +fn parse_pixel_position_env_var(value: &str) -> Option> { + let mut parts = value.split(','); + let x: usize = parts.next()?.parse().ok()?; + let y: usize = parts.next()?.parse().ok()?; + Some(point((x as f64).into(), (y as f64).into())) +} + +fn parse_pixel_size_env_var(value: &str) -> Option> { + let mut parts = value.split(','); + let width: usize = parts.next()?.parse().ok()?; + let height: usize = parts.next()?.parse().ok()?; + Some(size((width as f64).into(), (height as f64).into())) +} // #[cfg(test)] // mod tests { @@ -4600,7 +4733,7 @@ pub fn open_paths( // let workspace = window.root(cx); // // Adding an item with no ambiguity renders the tab without detail. -// let item1 = window.add_view(cx, |_| { +// let item1 = window.build_view(cx, |_| { // let mut item = TestItem::new(); // item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]); // item @@ -4612,7 +4745,7 @@ pub fn open_paths( // // Adding an item that creates ambiguity increases the level of detail on // // both tabs. -// let item2 = window.add_view(cx, |_| { +// let item2 = window.build_view(cx, |_| { // let mut item = TestItem::new(); // item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]); // item @@ -4626,7 +4759,7 @@ pub fn open_paths( // // 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 item3 = window.build_view(cx, |_| { // let mut item = TestItem::new(); // item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]); // item @@ -4668,10 +4801,10 @@ pub fn open_paths( // project.worktrees(cx).next().unwrap().read(cx).id() // }); -// let item1 = window.add_view(cx, |cx| { +// let item1 = window.build_view(cx, |cx| { // TestItem::new().with_project_items(&[TestProjectItem::new(1, "one.txt", cx)]) // }); -// let item2 = window.add_view(cx, |cx| { +// let item2 = window.build_view(cx, |cx| { // TestItem::new().with_project_items(&[TestProjectItem::new(2, "two.txt", cx)]) // }); @@ -4744,15 +4877,15 @@ pub fn open_paths( // let workspace = window.root(cx); // // When there are no dirty items, there's nothing to do. -// let item1 = window.add_view(cx, |_| TestItem::new()); +// let item1 = window.build_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| { +// let item2 = window.build_view(cx, |_| TestItem::new().with_dirty(true)); +// let item3 = window.build_view(cx, |cx| { // TestItem::new() // .with_dirty(true) // .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]) @@ -4781,24 +4914,24 @@ pub fn open_paths( // let window = cx.add_window(|cx| Workspace::test_new(project, cx)); // let workspace = window.root(cx); -// let item1 = window.add_view(cx, |cx| { +// let item1 = window.build_view(cx, |cx| { // TestItem::new() // .with_dirty(true) // .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]) // }); -// let item2 = window.add_view(cx, |cx| { +// let item2 = window.build_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| { +// let item3 = window.build_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| { +// let item4 = window.build_view(cx, |cx| { // TestItem::new() // .with_dirty(true) // .with_project_items(&[TestProjectItem::new_untitled(cx)]) @@ -4892,7 +5025,7 @@ pub fn open_paths( // // workspace items with multiple project entries. // let single_entry_items = (0..=4) // .map(|project_entry_id| { -// window.add_view(cx, |cx| { +// window.build_view(cx, |cx| { // TestItem::new() // .with_dirty(true) // .with_project_items(&[TestProjectItem::new( @@ -4903,7 +5036,7 @@ pub fn open_paths( // }) // }) // .collect::>(); -// let item_2_3 = window.add_view(cx, |cx| { +// let item_2_3 = window.build_view(cx, |cx| { // TestItem::new() // .with_dirty(true) // .with_singleton(false) @@ -4912,7 +5045,7 @@ pub fn open_paths( // single_entry_items[3].read(cx).project_items[0].clone(), // ]) // }); -// let item_3_4 = window.add_view(cx, |cx| { +// let item_3_4 = window.build_view(cx, |cx| { // TestItem::new() // .with_dirty(true) // .with_singleton(false) @@ -4999,7 +5132,7 @@ pub fn open_paths( // let workspace = window.root(cx); // let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); -// let item = window.add_view(cx, |cx| { +// let item = window.build_view(cx, |cx| { // TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]) // }); // let item_id = item.id(); @@ -5119,7 +5252,7 @@ pub fn open_paths( // let window = cx.add_window(|cx| Workspace::test_new(project, cx)); // let workspace = window.root(cx); -// let item = window.add_view(cx, |cx| { +// let item = window.build_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()); @@ -5174,7 +5307,7 @@ pub fn open_paths( // let workspace = window.root(cx); // let panel = workspace.update(cx, |workspace, cx| { -// let panel = cx.add_view(|_| TestPanel::new(DockPosition::Right)); +// let panel = cx.build_view(|_| TestPanel::new(DockPosition::Right)); // workspace.add_panel(panel.clone(), cx); // workspace @@ -5186,7 +5319,7 @@ pub fn open_paths( // let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); // pane.update(cx, |pane, cx| { -// let item = cx.add_view(|_| TestItem::new()); +// let item = cx.build_view(|_| TestItem::new()); // pane.add_item(Box::new(item), true, true, None, cx); // }); @@ -5323,12 +5456,12 @@ pub fn open_paths( // 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)); +// let panel_1 = cx.build_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)); +// let panel_2 = cx.build_view(|_| TestPanel::new(DockPosition::Right)); // workspace.add_panel(panel_2.clone(), cx); // workspace // .right_dock() @@ -5476,7 +5609,7 @@ pub fn open_paths( // // 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); +// let focus_receiver = window.build_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())); diff --git a/crates/workspace2/src/workspace_settings.rs b/crates/workspace2/src/workspace_settings.rs new file mode 100644 index 0000000000..4b93b705a3 --- /dev/null +++ b/crates/workspace2/src/workspace_settings.rs @@ -0,0 +1,56 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use settings2::Settings; + +#[derive(Deserialize)] +pub struct WorkspaceSettings { + pub active_pane_magnification: f32, + pub confirm_quit: bool, + pub show_call_status_icon: bool, + pub autosave: AutosaveSetting, +} + +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] +pub struct WorkspaceSettingsContent { + pub active_pane_magnification: Option, + pub confirm_quit: Option, + pub show_call_status_icon: Option, + pub autosave: Option, +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum AutosaveSetting { + Off, + AfterDelay { milliseconds: u64 }, + OnFocusChange, + OnWindowChange, +} + +#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] +pub struct GitSettings { + pub git_gutter: Option, + pub gutter_debounce: Option, +} + +#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum GitGutterSetting { + #[default] + TrackedFiles, + Hide, +} + +impl Settings for WorkspaceSettings { + const KEY: Option<&'static str> = None; + + type FileContent = WorkspaceSettingsContent; + + fn load( + default_value: &Self::FileContent, + user_values: &[&Self::FileContent], + _: &mut gpui2::AppContext, + ) -> anyhow::Result { + Self::load_via_json_merge(default_value, user_values) + } +} diff --git a/crates/zed2/Cargo.toml b/crates/zed2/Cargo.toml index 3e07b4a76f..71963ff30b 100644 --- a/crates/zed2/Cargo.toml +++ b/crates/zed2/Cargo.toml @@ -70,7 +70,7 @@ theme = { package = "theme2", path = "../theme2" } util = { path = "../util" } # semantic_index = { path = "../semantic_index" } # vim = { path = "../vim" } -# workspace = { path = "../workspace" } +workspace2 = { path = "../workspace2" } # welcome = { path = "../welcome" } # zed-actions = {path = "../zed-actions"} anyhow.workspace = true diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index 78e94c591c..f8b77fe9df 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -1,3 +1,6 @@ +#![allow(unused_variables, dead_code, unused_mut)] +// todo!() this is to make transition easier. + // Allow binary to be called Zed for a nice application menu when running executable directly #![allow(non_snake_case)] @@ -12,7 +15,7 @@ use client::UserStore; use db::kvp::KEY_VALUE_STORE; use fs::RealFs; use futures::{channel::mpsc, SinkExt, StreamExt}; -use gpui::{App, AppContext, AsyncAppContext, Context, SemanticVersion, Task}; +use gpui::{Action, App, AppContext, AsyncAppContext, Context, SemanticVersion, Task}; use isahc::{prelude::Configurable, Request}; use language::LanguageRegistry; use log::LevelFilter; @@ -40,13 +43,15 @@ use std::{ time::{SystemTime, UNIX_EPOCH}, }; use util::{ + async_maybe, channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL}, http::{self, HttpClient}, paths, ResultExt, }; use uuid::Uuid; -use zed2::languages; -use zed2::{ensure_only_instance, AppState, Assets, IsOnlyInstance}; +use workspace2::{AppState, WorkspaceStore}; +use zed2::{build_window_options, initialize_workspace, languages}; +use zed2::{ensure_only_instance, Assets, IsOnlyInstance}; mod open_listener; @@ -125,7 +130,7 @@ fn main() { language::init(cx); languages::init(languages.clone(), node_runtime.clone(), cx); let user_store = cx.build_model(|cx| UserStore::new(client.clone(), http.clone(), cx)); - // let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx)); + let workspace_store = cx.build_model(|cx| WorkspaceStore::new(client.clone(), cx)); cx.set_global(client.clone()); @@ -170,26 +175,23 @@ fn main() { // client.telemetry().start(installation_id, session_id, cx); - // todo!("app_state") - let app_state = Arc::new(AppState { client, user_store }); - // let app_state = Arc::new(AppState { - // languages, - // client: client.clone(), - // user_store, - // fs, - // build_window_options, - // initialize_workspace, - // background_actions, - // workspace_store, - // node_runtime, - // }); - // cx.set_global(Arc::downgrade(&app_state)); + let app_state = Arc::new(AppState { + languages, + client: client.clone(), + user_store, + fs, + build_window_options, + initialize_workspace, + // background_actions: todo!("ask Mikayla"), + workspace_store, + node_runtime, + }); + cx.set_global(Arc::downgrade(&app_state)); // audio::init(Assets, cx); // auto_update::init(http.clone(), client::ZED_SERVER_URL.clone(), cx); - // todo!("workspace") - // workspace::init(app_state.clone(), cx); + workspace2::init(app_state.clone(), cx); // recent_projects::init(cx); // journal2::init(app_state.clone(), cx); @@ -320,22 +322,30 @@ async fn installation_id() -> Result { } } -async fn restore_or_create_workspace(_app_state: &Arc, mut _cx: AsyncAppContext) { - todo!("workspace") - // if let Some(location) = workspace::last_opened_workspace_paths().await { - // cx.update(|cx| workspace::open_paths(location.paths().as_ref(), app_state, None, cx)) - // .await - // .log_err(); - // } else if matches!(KEY_VALUE_STORE.read_kvp(FIRST_OPEN), Ok(None)) { - // cx.update(|cx| show_welcome_experience(app_state, cx)); - // } else { - // cx.update(|cx| { - // workspace::open_new(app_state, cx, |workspace, cx| { - // Editor::new_file(workspace, &Default::default(), cx) - // }) - // .detach(); - // }); - // } +async fn restore_or_create_workspace(app_state: &Arc, mut cx: AsyncAppContext) { + async_maybe!({ + if let Some(location) = workspace2::last_opened_workspace_paths().await { + cx.update(|cx| workspace2::open_paths(location.paths().as_ref(), app_state, None, cx))? + .await + .log_err(); + } else if matches!(KEY_VALUE_STORE.read_kvp("******* THIS IS A BAD KEY PLEASE UNCOMMENT BELOW TO FIX THIS VERY LONG LINE *******"), Ok(None)) { + // todo!(welcome) + //} else if matches!(KEY_VALUE_STORE.read_kvp(FIRST_OPEN), Ok(None)) { + //todo!() + // cx.update(|cx| show_welcome_experience(app_state, cx)); + } else { + cx.update(|cx| { + workspace2::open_new(app_state, cx, |workspace, cx| { + // todo!(editor) + // Editor::new_file(workspace, &Default::default(), cx) + }) + .detach(); + })?; + } + anyhow::Ok(()) + }) + .await + .log_err(); } fn init_paths() { @@ -919,11 +929,13 @@ async fn handle_cli_connection( } } -// pub fn background_actions() -> &'static [(&'static str, &'static dyn Action)] { -// &[ -// ("Go to file", &file_finder::Toggle), -// ("Open command palette", &command_palette::Toggle), -// ("Open recent projects", &recent_projects::OpenRecent), -// ("Change your settings", &zed_actions::OpenSettings), -// ] -// } +pub fn background_actions() -> &'static [(&'static str, &'static dyn Action)] { + // &[ + // ("Go to file", &file_finder::Toggle), + // ("Open command palette", &command_palette::Toggle), + // ("Open recent projects", &recent_projects::OpenRecent), + // ("Change your settings", &zed_actions::OpenSettings), + // ] + // todo!() + &[] +} diff --git a/crates/zed2/src/zed2.rs b/crates/zed2/src/zed2.rs index ea8bdd9f1c..04778f29dd 100644 --- a/crates/zed2/src/zed2.rs +++ b/crates/zed2/src/zed2.rs @@ -1,11 +1,17 @@ +#![allow(unused_variables, dead_code, unused_mut)] +// todo!() this is to make transition easier. + mod assets; pub mod languages; mod only_instance; mod open_listener; pub use assets::*; -use client::{Client, UserStore}; -use gpui::{AsyncAppContext, Model}; +use collections::HashMap; +use gpui::{ + point, px, AppContext, AsyncAppContext, AsyncWindowContext, Point, Task, TitlebarOptions, + WeakView, WindowBounds, WindowKind, WindowOptions, +}; pub use only_instance::*; pub use open_listener::*; @@ -14,8 +20,14 @@ 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}; +use uuid::Uuid; +use workspace2::{AppState, Workspace}; pub fn connect_to_cli( server_name: &str, @@ -46,163 +58,350 @@ pub fn connect_to_cli( Ok((async_request_rx, response_tx)) } -pub struct AppState { - pub client: Arc, - pub user_store: Model, -} - pub async fn handle_cli_connection( - (mut requests, _responses): (mpsc::Receiver, IpcSender), - _app_state: Arc, - mut _cx: AsyncAppContext, + (mut requests, responses): (mpsc::Receiver, IpcSender), + app_state: Arc, + mut cx: AsyncAppContext, ) { if let Some(request) = requests.next().await { match request { - CliRequest::Open { paths: _, wait: _ } => { - // let mut caret_positions = HashMap::new(); + CliRequest::Open { paths, wait } => { + let mut caret_positions = HashMap::default(); - // let paths = if paths.is_empty() { - // todo!() - // workspace::last_opened_workspace_paths() - // .await - // .map(|location| location.paths().to_vec()) - // .unwrap_or_default() - // } else { - // paths - // .into_iter() - // .filter_map(|path_with_position_string| { - // let path_with_position = PathLikeWithPosition::parse_str( - // &path_with_position_string, - // |path_str| { - // Ok::<_, std::convert::Infallible>( - // Path::new(path_str).to_path_buf(), - // ) - // }, - // ) - // .expect("Infallible"); - // let path = path_with_position.path_like; - // if let Some(row) = path_with_position.row { - // if path.is_file() { - // let row = row.saturating_sub(1); - // let col = - // path_with_position.column.unwrap_or(0).saturating_sub(1); - // caret_positions.insert(path.clone(), Point::new(row, col)); - // } - // } - // Some(path) - // }) - // .collect() - // }; + let paths = if paths.is_empty() { + todo!() + // workspace::last_opened_workspace_paths() + // .await + // .map(|location| location.paths().to_vec()) + // .unwrap_or_default() + } else { + paths + .into_iter() + .filter_map(|path_with_position_string| { + let path_with_position = PathLikeWithPosition::parse_str( + &path_with_position_string, + |path_str| { + Ok::<_, std::convert::Infallible>( + Path::new(path_str).to_path_buf(), + ) + }, + ) + .expect("Infallible"); + let path = path_with_position.path_like; + if let Some(row) = path_with_position.row { + if path.is_file() { + let row = row.saturating_sub(1); + let col = + path_with_position.column.unwrap_or(0).saturating_sub(1); + caret_positions.insert(path.clone(), Point::new(row, col)); + } + } + Some(path) + }) + .collect::>() + }; - // let mut errored = false; - // todo!("workspace") - // match cx - // .update(|cx| workspace::open_paths(&paths, &app_state, None, cx)) - // .await - // { - // Ok((workspace, items)) => { - // let mut item_release_futures = Vec::new(); + let mut errored = false; - // for (item, path) in items.into_iter().zip(&paths) { - // match item { - // Some(Ok(item)) => { - // if let Some(point) = caret_positions.remove(path) { - // if let Some(active_editor) = item.downcast::() { - // active_editor - // .downgrade() - // .update(&mut cx, |editor, cx| { - // let snapshot = - // editor.snapshot(cx).display_snapshot; - // let point = snapshot - // .buffer_snapshot - // .clip_point(point, Bias::Left); - // editor.change_selections( - // Some(Autoscroll::center()), - // cx, - // |s| s.select_ranges([point..point]), - // ); - // }) - // .log_err(); - // } - // } + if let Some(open_paths_task) = cx + .update(|cx| workspace2::open_paths(&paths, &app_state, None, cx)) + .log_err() + { + match open_paths_task.await { + Ok((workspace, items)) => { + let mut item_release_futures = Vec::new(); - // let released = oneshot::channel(); - // cx.update(|cx| { - // item.on_release( - // cx, - // Box::new(move |_| { - // let _ = released.0.send(()); - // }), - // ) - // .detach(); - // }); - // item_release_futures.push(released.1); - // } - // Some(Err(err)) => { - // responses - // .send(CliResponse::Stderr { - // message: format!("error opening {:?}: {}", path, err), - // }) - // .log_err(); - // errored = true; - // } - // None => {} - // } - // } + for (item, path) in items.into_iter().zip(&paths) { + match item { + Some(Ok(mut item)) => { + if let Some(point) = caret_positions.remove(path) { + todo!() + // if let Some(active_editor) = item.downcast::() { + // active_editor + // .downgrade() + // .update(&mut cx, |editor, cx| { + // let snapshot = + // editor.snapshot(cx).display_snapshot; + // let point = snapshot + // .buffer_snapshot + // .clip_point(point, Bias::Left); + // editor.change_selections( + // Some(Autoscroll::center()), + // cx, + // |s| s.select_ranges([point..point]), + // ); + // }) + // .log_err(); + // } + } - // if wait { - // let background = cx.background(); - // let wait = async move { - // if paths.is_empty() { - // let (done_tx, done_rx) = oneshot::channel(); - // if let Some(workspace) = workspace.upgrade(&cx) { - // let _subscription = cx.update(|cx| { - // cx.observe_release(&workspace, move |_, _| { - // let _ = done_tx.send(()); - // }) - // }); - // drop(workspace); - // let _ = done_rx.await; - // } - // } else { - // let _ = - // futures::future::try_join_all(item_release_futures).await; - // }; - // } - // .fuse(); - // futures::pin_mut!(wait); + let released = oneshot::channel(); + cx.update(move |cx| { + item.on_release( + cx, + Box::new(move |_| { + let _ = released.0.send(()); + }), + ) + .detach(); + }) + .ok(); + item_release_futures.push(released.1); + } + Some(Err(err)) => { + responses + .send(CliResponse::Stderr { + message: format!( + "error opening {:?}: {}", + path, err + ), + }) + .log_err(); + errored = true; + } + None => {} + } + } - // loop { - // // Repeatedly check if CLI is still open to avoid wasting resources - // // waiting for files or workspaces to close. - // let mut timer = background.timer(Duration::from_secs(1)).fuse(); - // futures::select_biased! { - // _ = wait => break, - // _ = timer => { - // if responses.send(CliResponse::Ping).is_err() { - // break; - // } - // } - // } - // } - // } - // } - // Err(error) => { - // errored = true; - // responses - // .send(CliResponse::Stderr { - // message: format!("error opening {:?}: {}", paths, error), - // }) - // .log_err(); - // } - // } + if wait { + let executor = cx.background_executor().clone(); + let wait = async move { + if paths.is_empty() { + let (done_tx, done_rx) = oneshot::channel(); + let _subscription = + workspace.update(&mut cx, move |_, cx| { + cx.on_release(|_, _| { + let _ = done_tx.send(()); + }) + }); + let _ = done_rx.await; + } else { + let _ = futures::future::try_join_all(item_release_futures) + .await; + }; + } + .fuse(); + futures::pin_mut!(wait); - // responses - // .send(CliResponse::Exit { - // status: i32::from(errored), - // }) - // .log_err(); + loop { + // Repeatedly check if CLI is still open to avoid wasting resources + // waiting for files or workspaces to close. + let mut timer = executor.timer(Duration::from_secs(1)).fuse(); + futures::select_biased! { + _ = wait => break, + _ = timer => { + if responses.send(CliResponse::Ping).is_err() { + break; + } + } + } + } + } + } + Err(error) => { + errored = true; + responses + .send(CliResponse::Stderr { + message: format!("error opening {:?}: {}", paths, error), + }) + .log_err(); + } + } + + responses + .send(CliResponse::Exit { + status: i32::from(errored), + }) + .log_err(); + } } } } } + +pub fn build_window_options( + bounds: Option, + display_uuid: Option, + cx: &mut AppContext, +) -> WindowOptions { + let bounds = bounds.unwrap_or(WindowBounds::Maximized); + let display = display_uuid.and_then(|uuid| { + cx.displays() + .into_iter() + .find(|display| display.uuid().ok() == Some(uuid)) + }); + + WindowOptions { + bounds, + titlebar: Some(TitlebarOptions { + title: None, + appears_transparent: true, + traffic_light_position: Some(point(px(8.), px(8.))), + }), + center: false, + focus: false, + show: false, + kind: WindowKind::Normal, + is_movable: false, + display_id: display.map(|display| display.id()), + } +} + +pub fn initialize_workspace( + workspace_handle: WeakView, + was_deserialized: bool, + app_state: Arc, + cx: AsyncWindowContext, +) -> Task> { + cx.spawn(|mut cx| async move { + workspace_handle.update(&mut cx, |workspace, cx| { + let workspace_handle = cx.view(); + cx.subscribe(&workspace_handle, { + move |workspace, _, event, cx| { + if let workspace2::Event::PaneAdded(pane) = event { + pane.update(cx, |pane, cx| { + // todo!() + // pane.toolbar().update(cx, |toolbar, cx| { + // let breadcrumbs = cx.add_view(|_| Breadcrumbs::new(workspace)); + // toolbar.add_item(breadcrumbs, cx); + // let buffer_search_bar = cx.add_view(BufferSearchBar::new); + // toolbar.add_item(buffer_search_bar.clone(), cx); + // let quick_action_bar = cx.add_view(|_| { + // QuickActionBar::new(buffer_search_bar, workspace) + // }); + // toolbar.add_item(quick_action_bar, cx); + // let diagnostic_editor_controls = + // cx.add_view(|_| diagnostics2::ToolbarControls::new()); + // toolbar.add_item(diagnostic_editor_controls, cx); + // let project_search_bar = cx.add_view(|_| ProjectSearchBar::new()); + // toolbar.add_item(project_search_bar, cx); + // let submit_feedback_button = + // cx.add_view(|_| SubmitFeedbackButton::new()); + // toolbar.add_item(submit_feedback_button, cx); + // let feedback_info_text = cx.add_view(|_| FeedbackInfoText::new()); + // toolbar.add_item(feedback_info_text, cx); + // let lsp_log_item = + // cx.add_view(|_| language_tools::LspLogToolbarItemView::new()); + // toolbar.add_item(lsp_log_item, cx); + // let syntax_tree_item = cx + // .add_view(|_| language_tools::SyntaxTreeToolbarItemView::new()); + // toolbar.add_item(syntax_tree_item, cx); + // }) + }); + } + } + }) + .detach(); + + // cx.emit(workspace2::Event::PaneAdded( + // workspace.active_pane().clone(), + // )); + + // let collab_titlebar_item = + // cx.add_view(|cx| CollabTitlebarItem::new(workspace, &workspace_handle, cx)); + // workspace.set_titlebar_item(collab_titlebar_item.into_any(), cx); + + // let copilot = + // cx.add_view(|cx| copilot_button::CopilotButton::new(app_state.fs.clone(), cx)); + // let diagnostic_summary = + // cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx)); + // let activity_indicator = activity_indicator::ActivityIndicator::new( + // workspace, + // app_state.languages.clone(), + // cx, + // ); + // let active_buffer_language = + // cx.add_view(|_| language_selector::ActiveBufferLanguage::new(workspace)); + // let vim_mode_indicator = cx.add_view(|cx| vim::ModeIndicator::new(cx)); + // let feedback_button = cx.add_view(|_| { + // feedback::deploy_feedback_button::DeployFeedbackButton::new(workspace) + // }); + // let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new()); + // workspace.status_bar().update(cx, |status_bar, cx| { + // status_bar.add_left_item(diagnostic_summary, cx); + // status_bar.add_left_item(activity_indicator, cx); + + // status_bar.add_right_item(feedback_button, cx); + // status_bar.add_right_item(copilot, cx); + // status_bar.add_right_item(active_buffer_language, cx); + // status_bar.add_right_item(vim_mode_indicator, cx); + // status_bar.add_right_item(cursor_position, cx); + // }); + + // auto_update::notify_of_any_new_update(cx.weak_handle(), cx); + + // vim::observe_keystrokes(cx); + + // cx.on_window_should_close(|workspace, cx| { + // if let Some(task) = workspace.close(&Default::default(), cx) { + // task.detach_and_log_err(cx); + // } + // false + // }); + // })?; + + // let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone()); + // let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone()); + // let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone()); + // let channels_panel = + // collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone()); + // let chat_panel = + // collab_ui::chat_panel::ChatPanel::load(workspace_handle.clone(), cx.clone()); + // let notification_panel = collab_ui::notification_panel::NotificationPanel::load( + // workspace_handle.clone(), + // cx.clone(), + // ); + // let ( + // project_panel, + // terminal_panel, + // assistant_panel, + // channels_panel, + // chat_panel, + // notification_panel, + // ) = futures::try_join!( + // project_panel, + // terminal_panel, + // assistant_panel, + // channels_panel, + // chat_panel, + // notification_panel, + // )?; + // workspace_handle.update(&mut cx, |workspace, cx| { + // let project_panel_position = project_panel.position(cx); + // workspace.add_panel_with_extra_event_handler( + // project_panel, + // cx, + // |workspace, _, event, cx| match event { + // project_panel::Event::NewSearchInDirectory { dir_entry } => { + // search::ProjectSearchView::new_search_in_directory(workspace, dir_entry, cx) + // } + // project_panel::Event::ActivatePanel => { + // workspace.focus_panel::(cx); + // } + // _ => {} + // }, + // ); + // workspace.add_panel(terminal_panel, cx); + // workspace.add_panel(assistant_panel, cx); + // workspace.add_panel(channels_panel, cx); + // workspace.add_panel(chat_panel, cx); + // workspace.add_panel(notification_panel, cx); + + // if !was_deserialized + // && workspace + // .project() + // .read(cx) + // .visible_worktrees(cx) + // .any(|tree| { + // tree.read(cx) + // .root_entry() + // .map_or(false, |entry| entry.is_dir()) + // }) + // { + // workspace.toggle_dock(project_panel_position, cx); + // } + // cx.focus_self(); + })?; + Ok(()) + }) +}