This commit is contained in:
Antonio Scandurra 2023-10-26 12:27:20 +02:00
parent 06c22206af
commit c8b5b085f4
22 changed files with 14373 additions and 170 deletions

38
Cargo.lock generated
View File

@ -10549,6 +10549,43 @@ dependencies = [
"uuid 1.4.1",
]
[[package]]
name = "workspace2"
version = "0.1.0"
dependencies = [
"anyhow",
"async-recursion 1.0.5",
"bincode",
"call2",
"client2",
"collections",
"db2",
"env_logger 0.9.3",
"fs2",
"futures 0.3.28",
"gpui2",
"indoc",
"install_cli2",
"itertools 0.10.5",
"language2",
"lazy_static",
"log",
"node_runtime",
"parking_lot 0.11.2",
"postage",
"project2",
"schemars",
"serde",
"serde_derive",
"serde_json",
"settings2",
"smallvec",
"terminal2",
"theme2",
"util",
"uuid 1.4.1",
]
[[package]]
name = "ws2_32-sys"
version = "0.2.1"
@ -10857,6 +10894,7 @@ dependencies = [
"urlencoding",
"util",
"uuid 1.4.1",
"workspace2",
]
[[package]]

View File

@ -94,7 +94,7 @@ members = [
"crates/semantic_index",
"crates/vim",
"crates/vcs_menu",
"crates/workspace",
"crates/workspace2",
"crates/welcome",
"crates/xtask",
"crates/zed",

View File

@ -607,6 +607,20 @@ impl AppContext {
self.globals_by_type.insert(global_type, lease.global);
}
pub fn observe_release<E: 'static>(
&mut self,
handle: &Handle<E>,
mut on_release: impl FnMut(&mut E, &mut AppContext) + Send + Sync + 'static,
) -> Subscription {
self.release_listeners.insert(
handle.entity_id,
Box::new(move |entity, cx| {
let entity = entity.downcast_mut().expect("invalid entity type");
on_release(entity, cx)
}),
)
}
pub(crate) fn push_text_style(&mut self, text_style: TextStyleRefinement) {
self.text_style_stack.push(text_style);
}

View File

@ -115,15 +115,11 @@ impl<'a, T: 'static> ModelContext<'a, T> {
T: Any + Send + Sync,
{
let this = self.weak_handle();
self.app.release_listeners.insert(
handle.entity_id,
Box::new(move |entity, cx| {
let entity = entity.downcast_mut().expect("invalid entity type");
if let Some(this) = this.upgrade() {
this.update(cx, |this, cx| on_release(this, entity, cx));
}
}),
)
self.app.observe_release(handle, move |entity, cx| {
if let Some(this) = this.upgrade() {
this.update(cx, |this, cx| on_release(this, entity, cx));
}
})
}
pub fn observe_global<G: 'static>(

View File

@ -1,7 +1,7 @@
use crate::{
px, size, Action, AnyBox, AnyDrag, AnyView, AppContext, AsyncWindowContext, AvailableSpace,
Bounds, BoxShadow, Context, Corners, DevicePixels, DispatchContext, DisplayId, ExternalPaths,
Edges, Effect, Element, EntityId, EventEmitter, FileDropEvent, FocusEvent, FontId,
Bounds, BoxShadow, Context, Corners, DevicePixels, DispatchContext, DisplayId, Edges, Effect,
Element, EntityId, EventEmitter, ExternalPaths, FileDropEvent, FocusEvent, FontId,
GlobalElementId, GlyphId, Handle, Hsla, ImageData, InputEvent, IsZero, KeyListener, KeyMatch,
KeyMatcher, Keystroke, LayoutId, MainThread, MainThreadOnly, Modifiers, MonochromeSprite,
MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Path, Pixels, PlatformAtlas,
@ -1517,22 +1517,14 @@ impl<'a, 'w, V: 'static> ViewContext<'a, 'w, V> {
&mut self,
handle: &Handle<T>,
mut on_release: impl FnMut(&mut V, &mut T, &mut ViewContext<'_, '_, V>) + Send + Sync + 'static,
) -> Subscription
where
V: Any + Send + Sync,
{
) -> Subscription {
let this = self.handle();
let window_handle = self.window.handle;
self.app.release_listeners.insert(
handle.entity_id,
Box::new(move |entity, cx| {
let entity = entity.downcast_mut().expect("invalid entity type");
// todo!("are we okay with silently swallowing the error?")
let _ = cx.update_window(window_handle.id, |cx| {
this.update(cx, |this, cx| on_release(this, entity, cx))
});
}),
)
self.app.observe_release(handle, move |entity, cx| {
let _ = cx.update_window(window_handle.id, |cx| {
this.update(cx, |this, cx| on_release(this, entity, cx))
});
})
}
pub fn notify(&mut self) {

View File

@ -0,0 +1,65 @@
[package]
name = "workspace2"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/workspace2.rs"
doctest = false
[features]
test-support = [
"call2/test-support",
"client2/test-support",
"project2/test-support",
"settings2/test-support",
"gpui2/test-support",
"fs2/test-support"
]
[dependencies]
db2 = { path = "../db2" }
call2 = { path = "../call2" }
client2 = { path = "../client2" }
collections = { path = "../collections" }
# context_menu = { path = "../context_menu" }
fs2 = { path = "../fs2" }
gpui2 = { path = "../gpui2" }
install_cli2 = { path = "../install_cli2" }
language2 = { path = "../language2" }
#menu = { path = "../menu" }
node_runtime = { path = "../node_runtime" }
project2 = { path = "../project2" }
settings2 = { path = "../settings2" }
terminal2 = { path = "../terminal2" }
theme2 = { path = "../theme2" }
util = { path = "../util" }
async-recursion = "1.0.0"
itertools = "0.10"
bincode = "1.2.1"
anyhow.workspace = true
futures.workspace = true
lazy_static.workspace = true
log.workspace = true
parking_lot.workspace = true
postage.workspace = true
schemars.workspace = true
serde.workspace = true
serde_derive.workspace = true
serde_json.workspace = true
smallvec.workspace = true
uuid.workspace = true
[dev-dependencies]
call2 = { path = "../call2", features = ["test-support"] }
client2 = { path = "../client2", features = ["test-support"] }
gpui2 = { path = "../gpui2", features = ["test-support"] }
project2 = { path = "../project2", features = ["test-support"] }
settings2 = { path = "../settings2", features = ["test-support"] }
fs2 = { path = "../fs2", features = ["test-support"] }
db2 = { path = "../db2", features = ["test-support"] }
indoc.workspace = true
env_logger.workspace = true

View File

@ -0,0 +1,744 @@
use crate::{StatusItemView, Workspace, WorkspaceBounds};
use gpui2::{
elements::*, platform::CursorStyle, platform::MouseButton, Action, AnyViewHandle, AppContext,
Axis, Entity, Subscription, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::rc::Rc;
use theme2::ThemeSettings;
pub trait Panel: View {
fn position(&self, cx: &WindowContext) -> DockPosition;
fn position_is_valid(&self, position: DockPosition) -> bool;
fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>);
fn size(&self, cx: &WindowContext) -> f32;
fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>);
fn icon_path(&self, cx: &WindowContext) -> Option<&'static str>;
fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>);
fn icon_label(&self, _: &WindowContext) -> Option<String> {
None
}
fn should_change_position_on_event(_: &Self::Event) -> bool;
fn should_zoom_in_on_event(_: &Self::Event) -> bool {
false
}
fn should_zoom_out_on_event(_: &Self::Event) -> bool {
false
}
fn is_zoomed(&self, _cx: &WindowContext) -> bool {
false
}
fn set_zoomed(&mut self, _zoomed: bool, _cx: &mut ViewContext<Self>) {}
fn set_active(&mut self, _active: bool, _cx: &mut ViewContext<Self>) {}
fn should_activate_on_event(_: &Self::Event) -> bool {
false
}
fn should_close_on_event(_: &Self::Event) -> bool {
false
}
fn has_focus(&self, cx: &WindowContext) -> bool;
fn is_focus_event(_: &Self::Event) -> bool;
}
pub trait PanelHandle {
fn id(&self) -> usize;
fn position(&self, cx: &WindowContext) -> DockPosition;
fn position_is_valid(&self, position: DockPosition, cx: &WindowContext) -> bool;
fn set_position(&self, position: DockPosition, cx: &mut WindowContext);
fn is_zoomed(&self, cx: &WindowContext) -> bool;
fn set_zoomed(&self, zoomed: bool, cx: &mut WindowContext);
fn set_active(&self, active: bool, cx: &mut WindowContext);
fn size(&self, cx: &WindowContext) -> f32;
fn set_size(&self, size: Option<f32>, cx: &mut WindowContext);
fn icon_path(&self, cx: &WindowContext) -> Option<&'static str>;
fn icon_tooltip(&self, cx: &WindowContext) -> (String, Option<Box<dyn Action>>);
fn icon_label(&self, cx: &WindowContext) -> Option<String>;
fn has_focus(&self, cx: &WindowContext) -> bool;
fn as_any(&self) -> &AnyViewHandle;
}
impl<T> PanelHandle for ViewHandle<T>
where
T: Panel,
{
fn id(&self) -> usize {
self.id()
}
fn position(&self, cx: &WindowContext) -> DockPosition {
self.read(cx).position(cx)
}
fn position_is_valid(&self, position: DockPosition, cx: &WindowContext) -> bool {
self.read(cx).position_is_valid(position)
}
fn set_position(&self, position: DockPosition, cx: &mut WindowContext) {
self.update(cx, |this, cx| this.set_position(position, cx))
}
fn size(&self, cx: &WindowContext) -> f32 {
self.read(cx).size(cx)
}
fn set_size(&self, size: Option<f32>, cx: &mut WindowContext) {
self.update(cx, |this, cx| this.set_size(size, cx))
}
fn is_zoomed(&self, cx: &WindowContext) -> bool {
self.read(cx).is_zoomed(cx)
}
fn set_zoomed(&self, zoomed: bool, cx: &mut WindowContext) {
self.update(cx, |this, cx| this.set_zoomed(zoomed, cx))
}
fn set_active(&self, active: bool, cx: &mut WindowContext) {
self.update(cx, |this, cx| this.set_active(active, cx))
}
fn icon_path(&self, cx: &WindowContext) -> Option<&'static str> {
self.read(cx).icon_path(cx)
}
fn icon_tooltip(&self, cx: &WindowContext) -> (String, Option<Box<dyn Action>>) {
self.read(cx).icon_tooltip()
}
fn icon_label(&self, cx: &WindowContext) -> Option<String> {
self.read(cx).icon_label(cx)
}
fn has_focus(&self, cx: &WindowContext) -> bool {
self.read(cx).has_focus(cx)
}
fn as_any(&self) -> &AnyViewHandle {
self
}
}
impl From<&dyn PanelHandle> for AnyViewHandle {
fn from(val: &dyn PanelHandle) -> Self {
val.as_any().clone()
}
}
pub struct Dock {
position: DockPosition,
panel_entries: Vec<PanelEntry>,
is_open: bool,
active_panel_index: usize,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum DockPosition {
Left,
Bottom,
Right,
}
impl DockPosition {
fn to_label(&self) -> &'static str {
match self {
Self::Left => "left",
Self::Bottom => "bottom",
Self::Right => "right",
}
}
fn to_resize_handle_side(self) -> HandleSide {
match self {
Self::Left => HandleSide::Right,
Self::Bottom => HandleSide::Top,
Self::Right => HandleSide::Left,
}
}
pub fn axis(&self) -> Axis {
match self {
Self::Left | Self::Right => Axis::Horizontal,
Self::Bottom => Axis::Vertical,
}
}
}
struct PanelEntry {
panel: Rc<dyn PanelHandle>,
context_menu: ViewHandle<ContextMenu>,
_subscriptions: [Subscription; 2],
}
pub struct PanelButtons {
dock: ViewHandle<Dock>,
workspace: WeakViewHandle<Workspace>,
}
impl Dock {
pub fn new(position: DockPosition) -> Self {
Self {
position,
panel_entries: Default::default(),
active_panel_index: 0,
is_open: false,
}
}
pub fn position(&self) -> DockPosition {
self.position
}
pub fn is_open(&self) -> bool {
self.is_open
}
pub fn has_focus(&self, cx: &WindowContext) -> bool {
self.visible_panel()
.map_or(false, |panel| panel.has_focus(cx))
}
pub fn panel<T: Panel>(&self) -> Option<ViewHandle<T>> {
self.panel_entries
.iter()
.find_map(|entry| entry.panel.as_any().clone().downcast())
}
pub fn panel_index_for_type<T: Panel>(&self) -> Option<usize> {
self.panel_entries
.iter()
.position(|entry| entry.panel.as_any().is::<T>())
}
pub fn panel_index_for_ui_name(&self, ui_name: &str, cx: &AppContext) -> Option<usize> {
self.panel_entries.iter().position(|entry| {
let panel = entry.panel.as_any();
cx.view_ui_name(panel.window(), panel.id()) == Some(ui_name)
})
}
pub fn active_panel_index(&self) -> usize {
self.active_panel_index
}
pub(crate) fn set_open(&mut self, open: bool, cx: &mut ViewContext<Self>) {
if open != self.is_open {
self.is_open = open;
if let Some(active_panel) = self.panel_entries.get(self.active_panel_index) {
active_panel.panel.set_active(open, cx);
}
cx.notify();
}
}
pub fn set_panel_zoomed(
&mut self,
panel: &AnyViewHandle,
zoomed: bool,
cx: &mut ViewContext<Self>,
) {
for entry in &mut self.panel_entries {
if entry.panel.as_any() == panel {
if zoomed != entry.panel.is_zoomed(cx) {
entry.panel.set_zoomed(zoomed, cx);
}
} else if entry.panel.is_zoomed(cx) {
entry.panel.set_zoomed(false, cx);
}
}
cx.notify();
}
pub fn zoom_out(&mut self, cx: &mut ViewContext<Self>) {
for entry in &mut self.panel_entries {
if entry.panel.is_zoomed(cx) {
entry.panel.set_zoomed(false, cx);
}
}
}
pub(crate) fn add_panel<T: Panel>(&mut self, panel: ViewHandle<T>, cx: &mut ViewContext<Self>) {
let subscriptions = [
cx.observe(&panel, |_, _, cx| cx.notify()),
cx.subscribe(&panel, |this, panel, event, cx| {
if T::should_activate_on_event(event) {
if let Some(ix) = this
.panel_entries
.iter()
.position(|entry| entry.panel.id() == panel.id())
{
this.set_open(true, cx);
this.activate_panel(ix, cx);
cx.focus(&panel);
}
} else if T::should_close_on_event(event)
&& this.visible_panel().map_or(false, |p| p.id() == panel.id())
{
this.set_open(false, cx);
}
}),
];
let dock_view_id = cx.view_id();
self.panel_entries.push(PanelEntry {
panel: Rc::new(panel),
context_menu: cx.add_view(|cx| {
let mut menu = ContextMenu::new(dock_view_id, cx);
menu.set_position_mode(OverlayPositionMode::Local);
menu
}),
_subscriptions: subscriptions,
});
cx.notify()
}
pub fn remove_panel<T: Panel>(&mut self, panel: &ViewHandle<T>, cx: &mut ViewContext<Self>) {
if let Some(panel_ix) = self
.panel_entries
.iter()
.position(|entry| entry.panel.id() == panel.id())
{
if panel_ix == self.active_panel_index {
self.active_panel_index = 0;
self.set_open(false, cx);
} else if panel_ix < self.active_panel_index {
self.active_panel_index -= 1;
}
self.panel_entries.remove(panel_ix);
cx.notify();
}
}
pub fn panels_len(&self) -> usize {
self.panel_entries.len()
}
pub fn activate_panel(&mut self, panel_ix: usize, cx: &mut ViewContext<Self>) {
if panel_ix != self.active_panel_index {
if let Some(active_panel) = self.panel_entries.get(self.active_panel_index) {
active_panel.panel.set_active(false, cx);
}
self.active_panel_index = panel_ix;
if let Some(active_panel) = self.panel_entries.get(self.active_panel_index) {
active_panel.panel.set_active(true, cx);
}
cx.notify();
}
}
pub fn visible_panel(&self) -> Option<&Rc<dyn PanelHandle>> {
let entry = self.visible_entry()?;
Some(&entry.panel)
}
pub fn active_panel(&self) -> Option<&Rc<dyn PanelHandle>> {
Some(&self.panel_entries.get(self.active_panel_index)?.panel)
}
fn visible_entry(&self) -> Option<&PanelEntry> {
if self.is_open {
self.panel_entries.get(self.active_panel_index)
} else {
None
}
}
pub fn zoomed_panel(&self, cx: &WindowContext) -> Option<Rc<dyn PanelHandle>> {
let entry = self.visible_entry()?;
if entry.panel.is_zoomed(cx) {
Some(entry.panel.clone())
} else {
None
}
}
pub fn panel_size(&self, panel: &dyn PanelHandle, cx: &WindowContext) -> Option<f32> {
self.panel_entries
.iter()
.find(|entry| entry.panel.id() == panel.id())
.map(|entry| entry.panel.size(cx))
}
pub fn active_panel_size(&self, cx: &WindowContext) -> Option<f32> {
if self.is_open {
self.panel_entries
.get(self.active_panel_index)
.map(|entry| entry.panel.size(cx))
} else {
None
}
}
pub fn resize_active_panel(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
if let Some(entry) = self.panel_entries.get_mut(self.active_panel_index) {
entry.panel.set_size(size, cx);
cx.notify();
}
}
pub fn render_placeholder(&self, cx: &WindowContext) -> AnyElement<Workspace> {
if let Some(active_entry) = self.visible_entry() {
Empty::new()
.into_any()
.contained()
.with_style(self.style(cx))
.resizable::<WorkspaceBounds>(
self.position.to_resize_handle_side(),
active_entry.panel.size(cx),
|_, _, _| {},
)
.into_any()
} else {
Empty::new().into_any()
}
}
fn style(&self, cx: &WindowContext) -> ContainerStyle {
let theme = &settings::get::<ThemeSettings>(cx).theme;
let style = match self.position {
DockPosition::Left => theme.workspace.dock.left,
DockPosition::Bottom => theme.workspace.dock.bottom,
DockPosition::Right => theme.workspace.dock.right,
};
style
}
}
impl Entity for Dock {
type Event = ();
}
impl View for Dock {
fn ui_name() -> &'static str {
"Dock"
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
if let Some(active_entry) = self.visible_entry() {
let style = self.style(cx);
ChildView::new(active_entry.panel.as_any(), cx)
.contained()
.with_style(style)
.resizable::<WorkspaceBounds>(
self.position.to_resize_handle_side(),
active_entry.panel.size(cx),
|dock: &mut Self, size, cx| dock.resize_active_panel(size, cx),
)
.into_any()
} else {
Empty::new().into_any()
}
}
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
if cx.is_self_focused() {
if let Some(active_entry) = self.visible_entry() {
cx.focus(active_entry.panel.as_any());
} else {
cx.focus_parent();
}
}
}
}
impl PanelButtons {
pub fn new(
dock: ViewHandle<Dock>,
workspace: WeakViewHandle<Workspace>,
cx: &mut ViewContext<Self>,
) -> Self {
cx.observe(&dock, |_, _, cx| cx.notify()).detach();
Self { dock, workspace }
}
}
impl Entity for PanelButtons {
type Event = ();
}
impl View for PanelButtons {
fn ui_name() -> &'static str {
"PanelButtons"
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let theme = &settings::get::<ThemeSettings>(cx).theme;
let tooltip_style = theme.tooltip.clone();
let theme = &theme.workspace.status_bar.panel_buttons;
let button_style = theme.button.clone();
let dock = self.dock.read(cx);
let active_ix = dock.active_panel_index;
let is_open = dock.is_open;
let dock_position = dock.position;
let group_style = match dock_position {
DockPosition::Left => theme.group_left,
DockPosition::Bottom => theme.group_bottom,
DockPosition::Right => theme.group_right,
};
let menu_corner = match dock_position {
DockPosition::Left => AnchorCorner::BottomLeft,
DockPosition::Bottom | DockPosition::Right => AnchorCorner::BottomRight,
};
let panels = dock
.panel_entries
.iter()
.map(|item| (item.panel.clone(), item.context_menu.clone()))
.collect::<Vec<_>>();
Flex::row()
.with_children(panels.into_iter().enumerate().filter_map(
|(panel_ix, (view, context_menu))| {
let icon_path = view.icon_path(cx)?;
let is_active = is_open && panel_ix == active_ix;
let (tooltip, tooltip_action) = if is_active {
(
format!("Close {} dock", dock_position.to_label()),
Some(match dock_position {
DockPosition::Left => crate::ToggleLeftDock.boxed_clone(),
DockPosition::Bottom => crate::ToggleBottomDock.boxed_clone(),
DockPosition::Right => crate::ToggleRightDock.boxed_clone(),
}),
)
} else {
view.icon_tooltip(cx)
};
Some(
Stack::new()
.with_child(
MouseEventHandler::new::<Self, _>(panel_ix, cx, |state, cx| {
let style = button_style.in_state(is_active);
let style = style.style_for(state);
Flex::row()
.with_child(
Svg::new(icon_path)
.with_color(style.icon_color)
.constrained()
.with_width(style.icon_size)
.aligned(),
)
.with_children(if let Some(label) = view.icon_label(cx) {
Some(
Label::new(label, style.label.text.clone())
.contained()
.with_style(style.label.container)
.aligned(),
)
} else {
None
})
.constrained()
.with_height(style.icon_size)
.contained()
.with_style(style.container)
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, {
let tooltip_action =
tooltip_action.as_ref().map(|action| action.boxed_clone());
move |_, this, cx| {
if let Some(tooltip_action) = &tooltip_action {
let window = cx.window();
let view_id = this.workspace.id();
let tooltip_action = tooltip_action.boxed_clone();
cx.spawn(|_, mut cx| async move {
window.dispatch_action(
view_id,
&*tooltip_action,
&mut cx,
);
})
.detach();
}
}
})
.on_click(MouseButton::Right, {
let view = view.clone();
let menu = context_menu.clone();
move |_, _, cx| {
const POSITIONS: [DockPosition; 3] = [
DockPosition::Left,
DockPosition::Right,
DockPosition::Bottom,
];
menu.update(cx, |menu, cx| {
let items = POSITIONS
.into_iter()
.filter(|position| {
*position != dock_position
&& view.position_is_valid(*position, cx)
})
.map(|position| {
let view = view.clone();
ContextMenuItem::handler(
format!("Dock {}", position.to_label()),
move |cx| view.set_position(position, cx),
)
})
.collect();
menu.show(Default::default(), menu_corner, items, cx);
})
}
})
.with_tooltip::<Self>(
panel_ix,
tooltip,
tooltip_action,
tooltip_style.clone(),
cx,
),
)
.with_child(ChildView::new(&context_menu, cx)),
)
},
))
.contained()
.with_style(group_style)
.into_any()
}
}
impl StatusItemView for PanelButtons {
fn set_active_pane_item(
&mut self,
_: Option<&dyn crate::ItemHandle>,
_: &mut ViewContext<Self>,
) {
}
}
#[cfg(any(test, feature = "test-support"))]
pub mod test {
use super::*;
use gpui2::{ViewContext, WindowContext};
#[derive(Debug)]
pub enum TestPanelEvent {
PositionChanged,
Activated,
Closed,
ZoomIn,
ZoomOut,
Focus,
}
pub struct TestPanel {
pub position: DockPosition,
pub zoomed: bool,
pub active: bool,
pub has_focus: bool,
pub size: f32,
}
impl TestPanel {
pub fn new(position: DockPosition) -> Self {
Self {
position,
zoomed: false,
active: false,
has_focus: false,
size: 300.,
}
}
}
impl Entity for TestPanel {
type Event = TestPanelEvent;
}
impl View for TestPanel {
fn ui_name() -> &'static str {
"TestPanel"
}
fn render(&mut self, _: &mut ViewContext<'_, '_, Self>) -> AnyElement<Self> {
Empty::new().into_any()
}
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
self.has_focus = true;
cx.emit(TestPanelEvent::Focus);
}
fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
self.has_focus = false;
}
}
impl Panel for TestPanel {
fn position(&self, _: &gpui::WindowContext) -> super::DockPosition {
self.position
}
fn position_is_valid(&self, _: super::DockPosition) -> bool {
true
}
fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
self.position = position;
cx.emit(TestPanelEvent::PositionChanged);
}
fn is_zoomed(&self, _: &WindowContext) -> bool {
self.zoomed
}
fn set_zoomed(&mut self, zoomed: bool, _cx: &mut ViewContext<Self>) {
self.zoomed = zoomed;
}
fn set_active(&mut self, active: bool, _cx: &mut ViewContext<Self>) {
self.active = active;
}
fn size(&self, _: &WindowContext) -> f32 {
self.size
}
fn set_size(&mut self, size: Option<f32>, _: &mut ViewContext<Self>) {
self.size = size.unwrap_or(300.);
}
fn icon_path(&self, _: &WindowContext) -> Option<&'static str> {
Some("icons/test_panel.svg")
}
fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>) {
("Test Panel".into(), None)
}
fn should_change_position_on_event(event: &Self::Event) -> bool {
matches!(event, TestPanelEvent::PositionChanged)
}
fn should_zoom_in_on_event(event: &Self::Event) -> bool {
matches!(event, TestPanelEvent::ZoomIn)
}
fn should_zoom_out_on_event(event: &Self::Event) -> bool {
matches!(event, TestPanelEvent::ZoomOut)
}
fn should_activate_on_event(event: &Self::Event) -> bool {
matches!(event, TestPanelEvent::Activated)
}
fn should_close_on_event(event: &Self::Event) -> bool {
matches!(event, TestPanelEvent::Closed)
}
fn has_focus(&self, _cx: &WindowContext) -> bool {
self.has_focus
}
fn is_focus_event(event: &Self::Event) -> bool {
matches!(event, TestPanelEvent::Focus)
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,400 @@
use crate::{Toast, Workspace};
use collections::HashMap;
use gpui2::{AnyViewHandle, AppContext, Entity, View, ViewContext, ViewHandle};
use std::{any::TypeId, ops::DerefMut};
pub fn init(cx: &mut AppContext) {
cx.set_global(NotificationTracker::new());
simple_message_notification::init(cx);
}
pub trait Notification: View {
fn should_dismiss_notification_on_event(&self, event: &<Self as Entity>::Event) -> bool;
}
pub trait NotificationHandle {
fn id(&self) -> usize;
fn as_any(&self) -> &AnyViewHandle;
}
impl<T: Notification> NotificationHandle for ViewHandle<T> {
fn id(&self) -> usize {
self.id()
}
fn as_any(&self) -> &AnyViewHandle {
self
}
}
impl From<&dyn NotificationHandle> for AnyViewHandle {
fn from(val: &dyn NotificationHandle) -> Self {
val.as_any().clone()
}
}
pub(crate) struct NotificationTracker {
notifications_sent: HashMap<TypeId, Vec<usize>>,
}
impl std::ops::Deref for NotificationTracker {
type Target = HashMap<TypeId, Vec<usize>>;
fn deref(&self) -> &Self::Target {
&self.notifications_sent
}
}
impl DerefMut for NotificationTracker {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.notifications_sent
}
}
impl NotificationTracker {
fn new() -> Self {
Self {
notifications_sent: Default::default(),
}
}
}
impl Workspace {
pub fn has_shown_notification_once<V: Notification>(
&self,
id: usize,
cx: &ViewContext<Self>,
) -> bool {
cx.global::<NotificationTracker>()
.get(&TypeId::of::<V>())
.map(|ids| ids.contains(&id))
.unwrap_or(false)
}
pub fn show_notification_once<V: Notification>(
&mut self,
id: usize,
cx: &mut ViewContext<Self>,
build_notification: impl FnOnce(&mut ViewContext<Self>) -> ViewHandle<V>,
) {
if !self.has_shown_notification_once::<V>(id, cx) {
cx.update_global::<NotificationTracker, _, _>(|tracker, _| {
let entry = tracker.entry(TypeId::of::<V>()).or_default();
entry.push(id);
});
self.show_notification::<V>(id, cx, build_notification)
}
}
pub fn show_notification<V: Notification>(
&mut self,
id: usize,
cx: &mut ViewContext<Self>,
build_notification: impl FnOnce(&mut ViewContext<Self>) -> ViewHandle<V>,
) {
let type_id = TypeId::of::<V>();
if self
.notifications
.iter()
.all(|(existing_type_id, existing_id, _)| {
(*existing_type_id, *existing_id) != (type_id, id)
})
{
let notification = build_notification(cx);
cx.subscribe(&notification, move |this, handle, event, cx| {
if handle.read(cx).should_dismiss_notification_on_event(event) {
this.dismiss_notification_internal(type_id, id, cx);
}
})
.detach();
self.notifications
.push((type_id, id, Box::new(notification)));
cx.notify();
}
}
pub fn dismiss_notification<V: Notification>(&mut self, id: usize, cx: &mut ViewContext<Self>) {
let type_id = TypeId::of::<V>();
self.dismiss_notification_internal(type_id, id, cx)
}
pub fn show_toast(&mut self, toast: Toast, cx: &mut ViewContext<Self>) {
self.dismiss_notification::<simple_message_notification::MessageNotification>(toast.id, cx);
self.show_notification(toast.id, cx, |cx| {
cx.add_view(|_cx| match toast.on_click.as_ref() {
Some((click_msg, on_click)) => {
let on_click = on_click.clone();
simple_message_notification::MessageNotification::new(toast.msg.clone())
.with_click_message(click_msg.clone())
.on_click(move |cx| on_click(cx))
}
None => simple_message_notification::MessageNotification::new(toast.msg.clone()),
})
})
}
pub fn dismiss_toast(&mut self, id: usize, cx: &mut ViewContext<Self>) {
self.dismiss_notification::<simple_message_notification::MessageNotification>(id, cx);
}
fn dismiss_notification_internal(
&mut self,
type_id: TypeId,
id: usize,
cx: &mut ViewContext<Self>,
) {
self.notifications
.retain(|(existing_type_id, existing_id, _)| {
if (*existing_type_id, *existing_id) == (type_id, id) {
cx.notify();
false
} else {
true
}
});
}
}
pub mod simple_message_notification {
use super::Notification;
use crate::Workspace;
use gpui2::{
actions,
elements::{Flex, MouseEventHandler, Padding, ParentElement, Svg, Text},
fonts::TextStyle,
impl_actions,
platform::{CursorStyle, MouseButton},
AnyElement, AppContext, Element, Entity, View, ViewContext,
};
use menu::Cancel;
use serde::Deserialize;
use std::{borrow::Cow, sync::Arc};
actions!(message_notifications, [CancelMessageNotification]);
#[derive(Clone, Default, Deserialize, PartialEq)]
pub struct OsOpen(pub Cow<'static, str>);
impl OsOpen {
pub fn new<I: Into<Cow<'static, str>>>(url: I) -> Self {
OsOpen(url.into())
}
}
impl_actions!(message_notifications, [OsOpen]);
pub fn init(cx: &mut AppContext) {
cx.add_action(MessageNotification::dismiss);
cx.add_action(
|_workspace: &mut Workspace, open_action: &OsOpen, cx: &mut ViewContext<Workspace>| {
cx.platform().open_url(open_action.0.as_ref());
},
)
}
enum NotificationMessage {
Text(Cow<'static, str>),
Element(fn(TextStyle, &AppContext) -> AnyElement<MessageNotification>),
}
pub struct MessageNotification {
message: NotificationMessage,
on_click: Option<Arc<dyn Fn(&mut ViewContext<Self>)>>,
click_message: Option<Cow<'static, str>>,
}
pub enum MessageNotificationEvent {
Dismiss,
}
impl Entity for MessageNotification {
type Event = MessageNotificationEvent;
}
impl MessageNotification {
pub fn new<S>(message: S) -> MessageNotification
where
S: Into<Cow<'static, str>>,
{
Self {
message: NotificationMessage::Text(message.into()),
on_click: None,
click_message: None,
}
}
pub fn new_element(
message: fn(TextStyle, &AppContext) -> AnyElement<MessageNotification>,
) -> MessageNotification {
Self {
message: NotificationMessage::Element(message),
on_click: None,
click_message: None,
}
}
pub fn with_click_message<S>(mut self, message: S) -> Self
where
S: Into<Cow<'static, str>>,
{
self.click_message = Some(message.into());
self
}
pub fn on_click<F>(mut self, on_click: F) -> Self
where
F: 'static + Fn(&mut ViewContext<Self>),
{
self.on_click = Some(Arc::new(on_click));
self
}
pub fn dismiss(&mut self, _: &CancelMessageNotification, cx: &mut ViewContext<Self>) {
cx.emit(MessageNotificationEvent::Dismiss);
}
}
impl View for MessageNotification {
fn ui_name() -> &'static str {
"MessageNotification"
}
fn render(&mut self, cx: &mut gpui2::ViewContext<Self>) -> gpui::AnyElement<Self> {
let theme = theme2::current(cx).clone();
let theme = &theme.simple_message_notification;
enum MessageNotificationTag {}
let click_message = self.click_message.clone();
let message = match &self.message {
NotificationMessage::Text(text) => {
Text::new(text.to_owned(), theme.message.text.clone()).into_any()
}
NotificationMessage::Element(e) => e(theme.message.text.clone(), cx),
};
let on_click = self.on_click.clone();
let has_click_action = on_click.is_some();
Flex::column()
.with_child(
Flex::row()
.with_child(
message
.contained()
.with_style(theme.message.container)
.aligned()
.top()
.left()
.flex(1., true),
)
.with_child(
MouseEventHandler::new::<Cancel, _>(0, cx, |state, _| {
let style = theme.dismiss_button.style_for(state);
Svg::new("icons/x.svg")
.with_color(style.color)
.constrained()
.with_width(style.icon_width)
.aligned()
.contained()
.with_style(style.container)
.constrained()
.with_width(style.button_width)
.with_height(style.button_width)
})
.with_padding(Padding::uniform(5.))
.on_click(MouseButton::Left, move |_, this, cx| {
this.dismiss(&Default::default(), cx);
})
.with_cursor_style(CursorStyle::PointingHand)
.aligned()
.constrained()
.with_height(cx.font_cache().line_height(theme.message.text.font_size))
.aligned()
.top()
.flex_float(),
),
)
.with_children({
click_message
.map(|click_message| {
MouseEventHandler::new::<MessageNotificationTag, _>(
0,
cx,
|state, _| {
let style = theme.action_message.style_for(state);
Flex::row()
.with_child(
Text::new(click_message, style.text.clone())
.contained()
.with_style(style.container),
)
.contained()
},
)
.on_click(MouseButton::Left, move |_, this, cx| {
if let Some(on_click) = on_click.as_ref() {
on_click(cx);
this.dismiss(&Default::default(), cx);
}
})
// Since we're not using a proper overlay, we have to capture these extra events
.on_down(MouseButton::Left, |_, _, _| {})
.on_up(MouseButton::Left, |_, _, _| {})
.with_cursor_style(if has_click_action {
CursorStyle::PointingHand
} else {
CursorStyle::Arrow
})
})
.into_iter()
})
.into_any()
}
}
impl Notification for MessageNotification {
fn should_dismiss_notification_on_event(&self, event: &<Self as Entity>::Event) -> bool {
match event {
MessageNotificationEvent::Dismiss => true,
}
}
}
}
pub trait NotifyResultExt {
type Ok;
fn notify_err(
self,
workspace: &mut Workspace,
cx: &mut ViewContext<Workspace>,
) -> Option<Self::Ok>;
}
impl<T, E> NotifyResultExt for Result<T, E>
where
E: std::fmt::Debug,
{
type Ok = T;
fn notify_err(self, workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Option<T> {
match self {
Ok(value) => Some(value),
Err(err) => {
workspace.show_notification(0, cx, |cx| {
cx.add_view(|_cx| {
simple_message_notification::MessageNotification::new(format!(
"Error: {:?}",
err,
))
})
});
None
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,239 @@
use super::DraggedItem;
use crate::{Pane, SplitDirection, Workspace};
use gpui2::{
color::Color,
elements::{Canvas, MouseEventHandler, ParentElement, Stack},
geometry::{rect::RectF, vector::Vector2F},
platform::MouseButton,
scene::MouseUp,
AppContext, Element, EventContext, MouseState, Quad, ViewContext, WeakViewHandle,
};
use project2::ProjectEntryId;
pub fn dragged_item_receiver<Tag, D, F>(
pane: &Pane,
region_id: usize,
drop_index: usize,
allow_same_pane: bool,
split_margin: Option<f32>,
cx: &mut ViewContext<Pane>,
render_child: F,
) -> MouseEventHandler<Pane>
where
Tag: 'static,
D: Element<Pane>,
F: FnOnce(&mut MouseState, &mut ViewContext<Pane>) -> D,
{
let drag_and_drop = cx.global::<DragAndDrop<Workspace>>();
let drag_position = if (pane.can_drop)(drag_and_drop, cx) {
drag_and_drop
.currently_dragged::<DraggedItem>(cx.window())
.map(|(drag_position, _)| drag_position)
.or_else(|| {
drag_and_drop
.currently_dragged::<ProjectEntryId>(cx.window())
.map(|(drag_position, _)| drag_position)
})
} else {
None
};
let mut handler = MouseEventHandler::above::<Tag, _>(region_id, cx, |state, cx| {
// Observing hovered will cause a render when the mouse enters regardless
// of if mouse position was accessed before
let drag_position = if state.dragging() {
drag_position
} else {
None
};
Stack::new()
.with_child(render_child(state, cx))
.with_children(drag_position.map(|drag_position| {
Canvas::new(move |bounds, _, _, cx| {
if bounds.contains_point(drag_position) {
let overlay_region = split_margin
.and_then(|split_margin| {
drop_split_direction(drag_position, bounds, split_margin)
.map(|dir| (dir, split_margin))
})
.map(|(dir, margin)| dir.along_edge(bounds, margin))
.unwrap_or(bounds);
cx.scene().push_stacking_context(None, None);
let background = overlay_color(cx);
cx.scene().push_quad(Quad {
bounds: overlay_region,
background: Some(background),
border: Default::default(),
corner_radii: Default::default(),
});
cx.scene().pop_stacking_context();
}
})
}))
});
if drag_position.is_some() {
handler = handler
.on_up(MouseButton::Left, {
move |event, pane, cx| {
let workspace = pane.workspace.clone();
let pane = cx.weak_handle();
handle_dropped_item(
event,
workspace,
&pane,
drop_index,
allow_same_pane,
split_margin,
cx,
);
cx.notify();
}
})
.on_move(|_, _, cx| {
let drag_and_drop = cx.global::<DragAndDrop<Workspace>>();
if drag_and_drop
.currently_dragged::<DraggedItem>(cx.window())
.is_some()
|| drag_and_drop
.currently_dragged::<ProjectEntryId>(cx.window())
.is_some()
{
cx.notify();
} else {
cx.propagate_event();
}
})
}
handler
}
pub fn handle_dropped_item<V: 'static>(
event: MouseUp,
workspace: WeakViewHandle<Workspace>,
pane: &WeakViewHandle<Pane>,
index: usize,
allow_same_pane: bool,
split_margin: Option<f32>,
cx: &mut EventContext<V>,
) {
enum Action {
Move(WeakViewHandle<Pane>, usize),
Open(ProjectEntryId),
}
let drag_and_drop = cx.global::<DragAndDrop<Workspace>>();
let action = if let Some((_, dragged_item)) =
drag_and_drop.currently_dragged::<DraggedItem>(cx.window())
{
Action::Move(dragged_item.pane.clone(), dragged_item.handle.id())
} else if let Some((_, project_entry)) =
drag_and_drop.currently_dragged::<ProjectEntryId>(cx.window())
{
Action::Open(*project_entry)
} else {
cx.propagate_event();
return;
};
if let Some(split_direction) =
split_margin.and_then(|margin| drop_split_direction(event.position, event.region, margin))
{
let pane_to_split = pane.clone();
match action {
Action::Move(from, item_id_to_move) => {
cx.window_context().defer(move |cx| {
if let Some(workspace) = workspace.upgrade(cx) {
workspace.update(cx, |workspace, cx| {
workspace.split_pane_with_item(
pane_to_split,
split_direction,
from,
item_id_to_move,
cx,
);
})
}
});
}
Action::Open(project_entry) => {
cx.window_context().defer(move |cx| {
if let Some(workspace) = workspace.upgrade(cx) {
workspace.update(cx, |workspace, cx| {
if let Some(task) = workspace.split_pane_with_project_entry(
pane_to_split,
split_direction,
project_entry,
cx,
) {
task.detach_and_log_err(cx);
}
})
}
});
}
};
} else {
match action {
Action::Move(from, item_id) => {
if pane != &from || allow_same_pane {
let pane = pane.clone();
cx.window_context().defer(move |cx| {
if let Some(((workspace, from), to)) = workspace
.upgrade(cx)
.zip(from.upgrade(cx))
.zip(pane.upgrade(cx))
{
workspace.update(cx, |workspace, cx| {
workspace.move_item(from, to, item_id, index, cx);
})
}
});
} else {
cx.propagate_event();
}
}
Action::Open(project_entry) => {
let pane = pane.clone();
cx.window_context().defer(move |cx| {
if let Some(workspace) = workspace.upgrade(cx) {
workspace.update(cx, |workspace, cx| {
if let Some(path) =
workspace.project.read(cx).path_for_entry(project_entry, cx)
{
workspace
.open_path(path, Some(pane), true, cx)
.detach_and_log_err(cx);
}
});
}
});
}
}
}
}
fn drop_split_direction(
position: Vector2F,
region: RectF,
split_margin: f32,
) -> Option<SplitDirection> {
let mut min_direction = None;
let mut min_distance = split_margin;
for direction in SplitDirection::all() {
let edge_distance = (direction.edge(region) - direction.axis().component(position)).abs();
if edge_distance < min_distance {
min_direction = Some(direction);
min_distance = edge_distance;
}
}
min_direction
}
fn overlay_color(cx: &AppContext) -> Color {
theme2::current(cx).workspace.drop_target_overlay_color
}

View File

@ -0,0 +1,989 @@
use crate::{pane_group::element::PaneAxisElement, AppState, FollowerState, Pane, Workspace};
use anyhow::{anyhow, Result};
use call::{ActiveCall, ParticipantLocation};
use collections::HashMap;
use gpui::{
elements::*,
geometry::{rect::RectF, vector::Vector2F},
platform::{CursorStyle, MouseButton},
AnyViewHandle, Axis, ModelHandle, ViewContext, ViewHandle,
};
use project::Project;
use serde::Deserialize;
use std::{cell::RefCell, rc::Rc, sync::Arc};
use theme::Theme;
const HANDLE_HITBOX_SIZE: f32 = 4.0;
const HORIZONTAL_MIN_SIZE: f32 = 80.;
const VERTICAL_MIN_SIZE: f32 = 100.;
#[derive(Clone, Debug, PartialEq)]
pub struct PaneGroup {
pub(crate) root: Member,
}
impl PaneGroup {
pub(crate) fn with_root(root: Member) -> Self {
Self { root }
}
pub fn new(pane: ViewHandle<Pane>) -> Self {
Self {
root: Member::Pane(pane),
}
}
pub fn split(
&mut self,
old_pane: &ViewHandle<Pane>,
new_pane: &ViewHandle<Pane>,
direction: SplitDirection,
) -> Result<()> {
match &mut self.root {
Member::Pane(pane) => {
if pane == old_pane {
self.root = Member::new_axis(old_pane.clone(), new_pane.clone(), direction);
Ok(())
} else {
Err(anyhow!("Pane not found"))
}
}
Member::Axis(axis) => axis.split(old_pane, new_pane, direction),
}
}
pub fn bounding_box_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<RectF> {
match &self.root {
Member::Pane(_) => None,
Member::Axis(axis) => axis.bounding_box_for_pane(pane),
}
}
pub fn pane_at_pixel_position(&self, coordinate: Vector2F) -> Option<&ViewHandle<Pane>> {
match &self.root {
Member::Pane(pane) => Some(pane),
Member::Axis(axis) => axis.pane_at_pixel_position(coordinate),
}
}
/// Returns:
/// - Ok(true) if it found and removed a pane
/// - Ok(false) if it found but did not remove the pane
/// - Err(_) if it did not find the pane
pub fn remove(&mut self, pane: &ViewHandle<Pane>) -> Result<bool> {
match &mut self.root {
Member::Pane(_) => Ok(false),
Member::Axis(axis) => {
if let Some(last_pane) = axis.remove(pane)? {
self.root = last_pane;
}
Ok(true)
}
}
}
pub fn swap(&mut self, from: &ViewHandle<Pane>, to: &ViewHandle<Pane>) {
match &mut self.root {
Member::Pane(_) => {}
Member::Axis(axis) => axis.swap(from, to),
};
}
pub(crate) fn render(
&self,
project: &ModelHandle<Project>,
theme: &Theme,
follower_states: &HashMap<ViewHandle<Pane>, FollowerState>,
active_call: Option<&ModelHandle<ActiveCall>>,
active_pane: &ViewHandle<Pane>,
zoomed: Option<&AnyViewHandle>,
app_state: &Arc<AppState>,
cx: &mut ViewContext<Workspace>,
) -> AnyElement<Workspace> {
self.root.render(
project,
0,
theme,
follower_states,
active_call,
active_pane,
zoomed,
app_state,
cx,
)
}
pub(crate) fn panes(&self) -> Vec<&ViewHandle<Pane>> {
let mut panes = Vec::new();
self.root.collect_panes(&mut panes);
panes
}
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) enum Member {
Axis(PaneAxis),
Pane(ViewHandle<Pane>),
}
impl Member {
fn new_axis(
old_pane: ViewHandle<Pane>,
new_pane: ViewHandle<Pane>,
direction: SplitDirection,
) -> Self {
use Axis::*;
use SplitDirection::*;
let axis = match direction {
Up | Down => Vertical,
Left | Right => Horizontal,
};
let members = match direction {
Up | Left => vec![Member::Pane(new_pane), Member::Pane(old_pane)],
Down | Right => vec![Member::Pane(old_pane), Member::Pane(new_pane)],
};
Member::Axis(PaneAxis::new(axis, members))
}
fn contains(&self, needle: &ViewHandle<Pane>) -> bool {
match self {
Member::Axis(axis) => axis.members.iter().any(|member| member.contains(needle)),
Member::Pane(pane) => pane == needle,
}
}
pub fn render(
&self,
project: &ModelHandle<Project>,
basis: usize,
theme: &Theme,
follower_states: &HashMap<ViewHandle<Pane>, FollowerState>,
active_call: Option<&ModelHandle<ActiveCall>>,
active_pane: &ViewHandle<Pane>,
zoomed: Option<&AnyViewHandle>,
app_state: &Arc<AppState>,
cx: &mut ViewContext<Workspace>,
) -> AnyElement<Workspace> {
enum FollowIntoExternalProject {}
match self {
Member::Pane(pane) => {
let pane_element = if Some(&**pane) == zoomed {
Empty::new().into_any()
} else {
ChildView::new(pane, cx).into_any()
};
let leader = follower_states.get(pane).and_then(|state| {
let room = active_call?.read(cx).room()?.read(cx);
room.remote_participant_for_peer_id(state.leader_id)
});
let mut leader_border = Border::default();
let mut leader_status_box = None;
if let Some(leader) = &leader {
let leader_color = theme
.editor
.selection_style_for_room_participant(leader.participant_index.0)
.cursor;
leader_border = Border::all(theme.workspace.leader_border_width, leader_color);
leader_border
.color
.fade_out(1. - theme.workspace.leader_border_opacity);
leader_border.overlay = true;
leader_status_box = match leader.location {
ParticipantLocation::SharedProject {
project_id: leader_project_id,
} => {
if Some(leader_project_id) == project.read(cx).remote_id() {
None
} else {
let leader_user = leader.user.clone();
let leader_user_id = leader.user.id;
Some(
MouseEventHandler::new::<FollowIntoExternalProject, _>(
pane.id(),
cx,
|_, _| {
Label::new(
format!(
"Follow {} to their active project",
leader_user.github_login,
),
theme
.workspace
.external_location_message
.text
.clone(),
)
.contained()
.with_style(
theme.workspace.external_location_message.container,
)
},
)
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, this, cx| {
crate::join_remote_project(
leader_project_id,
leader_user_id,
this.app_state().clone(),
cx,
)
.detach_and_log_err(cx);
})
.aligned()
.bottom()
.right()
.into_any(),
)
}
}
ParticipantLocation::UnsharedProject => Some(
Label::new(
format!(
"{} is viewing an unshared Zed project",
leader.user.github_login
),
theme.workspace.external_location_message.text.clone(),
)
.contained()
.with_style(theme.workspace.external_location_message.container)
.aligned()
.bottom()
.right()
.into_any(),
),
ParticipantLocation::External => Some(
Label::new(
format!(
"{} is viewing a window outside of Zed",
leader.user.github_login
),
theme.workspace.external_location_message.text.clone(),
)
.contained()
.with_style(theme.workspace.external_location_message.container)
.aligned()
.bottom()
.right()
.into_any(),
),
};
}
Stack::new()
.with_child(pane_element.contained().with_border(leader_border))
.with_children(leader_status_box)
.into_any()
}
Member::Axis(axis) => axis.render(
project,
basis + 1,
theme,
follower_states,
active_call,
active_pane,
zoomed,
app_state,
cx,
),
}
}
fn collect_panes<'a>(&'a self, panes: &mut Vec<&'a ViewHandle<Pane>>) {
match self {
Member::Axis(axis) => {
for member in &axis.members {
member.collect_panes(panes);
}
}
Member::Pane(pane) => panes.push(pane),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct PaneAxis {
pub axis: Axis,
pub members: Vec<Member>,
pub flexes: Rc<RefCell<Vec<f32>>>,
pub bounding_boxes: Rc<RefCell<Vec<Option<RectF>>>>,
}
impl PaneAxis {
pub fn new(axis: Axis, members: Vec<Member>) -> Self {
let flexes = Rc::new(RefCell::new(vec![1.; members.len()]));
let bounding_boxes = Rc::new(RefCell::new(vec![None; members.len()]));
Self {
axis,
members,
flexes,
bounding_boxes,
}
}
pub fn load(axis: Axis, members: Vec<Member>, flexes: Option<Vec<f32>>) -> Self {
let flexes = flexes.unwrap_or_else(|| vec![1.; members.len()]);
debug_assert!(members.len() == flexes.len());
let flexes = Rc::new(RefCell::new(flexes));
let bounding_boxes = Rc::new(RefCell::new(vec![None; members.len()]));
Self {
axis,
members,
flexes,
bounding_boxes,
}
}
fn split(
&mut self,
old_pane: &ViewHandle<Pane>,
new_pane: &ViewHandle<Pane>,
direction: SplitDirection,
) -> Result<()> {
for (mut idx, member) in self.members.iter_mut().enumerate() {
match member {
Member::Axis(axis) => {
if axis.split(old_pane, new_pane, direction).is_ok() {
return Ok(());
}
}
Member::Pane(pane) => {
if pane == old_pane {
if direction.axis() == self.axis {
if direction.increasing() {
idx += 1;
}
self.members.insert(idx, Member::Pane(new_pane.clone()));
*self.flexes.borrow_mut() = vec![1.; self.members.len()];
} else {
*member =
Member::new_axis(old_pane.clone(), new_pane.clone(), direction);
}
return Ok(());
}
}
}
}
Err(anyhow!("Pane not found"))
}
fn remove(&mut self, pane_to_remove: &ViewHandle<Pane>) -> Result<Option<Member>> {
let mut found_pane = false;
let mut remove_member = None;
for (idx, member) in self.members.iter_mut().enumerate() {
match member {
Member::Axis(axis) => {
if let Ok(last_pane) = axis.remove(pane_to_remove) {
if let Some(last_pane) = last_pane {
*member = last_pane;
}
found_pane = true;
break;
}
}
Member::Pane(pane) => {
if pane == pane_to_remove {
found_pane = true;
remove_member = Some(idx);
break;
}
}
}
}
if found_pane {
if let Some(idx) = remove_member {
self.members.remove(idx);
*self.flexes.borrow_mut() = vec![1.; self.members.len()];
}
if self.members.len() == 1 {
let result = self.members.pop();
*self.flexes.borrow_mut() = vec![1.; self.members.len()];
Ok(result)
} else {
Ok(None)
}
} else {
Err(anyhow!("Pane not found"))
}
}
fn swap(&mut self, from: &ViewHandle<Pane>, to: &ViewHandle<Pane>) {
for member in self.members.iter_mut() {
match member {
Member::Axis(axis) => axis.swap(from, to),
Member::Pane(pane) => {
if pane == from {
*member = Member::Pane(to.clone());
} else if pane == to {
*member = Member::Pane(from.clone())
}
}
}
}
}
fn bounding_box_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<RectF> {
debug_assert!(self.members.len() == self.bounding_boxes.borrow().len());
for (idx, member) in self.members.iter().enumerate() {
match member {
Member::Pane(found) => {
if pane == found {
return self.bounding_boxes.borrow()[idx];
}
}
Member::Axis(axis) => {
if let Some(rect) = axis.bounding_box_for_pane(pane) {
return Some(rect);
}
}
}
}
None
}
fn pane_at_pixel_position(&self, coordinate: Vector2F) -> Option<&ViewHandle<Pane>> {
debug_assert!(self.members.len() == self.bounding_boxes.borrow().len());
let bounding_boxes = self.bounding_boxes.borrow();
for (idx, member) in self.members.iter().enumerate() {
if let Some(coordinates) = bounding_boxes[idx] {
if coordinates.contains_point(coordinate) {
return match member {
Member::Pane(found) => Some(found),
Member::Axis(axis) => axis.pane_at_pixel_position(coordinate),
};
}
}
}
None
}
fn render(
&self,
project: &ModelHandle<Project>,
basis: usize,
theme: &Theme,
follower_states: &HashMap<ViewHandle<Pane>, FollowerState>,
active_call: Option<&ModelHandle<ActiveCall>>,
active_pane: &ViewHandle<Pane>,
zoomed: Option<&AnyViewHandle>,
app_state: &Arc<AppState>,
cx: &mut ViewContext<Workspace>,
) -> AnyElement<Workspace> {
debug_assert!(self.members.len() == self.flexes.borrow().len());
let mut pane_axis = PaneAxisElement::new(
self.axis,
basis,
self.flexes.clone(),
self.bounding_boxes.clone(),
);
let mut active_pane_ix = None;
let mut members = self.members.iter().enumerate().peekable();
while let Some((ix, member)) = members.next() {
let last = members.peek().is_none();
if member.contains(active_pane) {
active_pane_ix = Some(ix);
}
let mut member = member.render(
project,
(basis + ix) * 10,
theme,
follower_states,
active_call,
active_pane,
zoomed,
app_state,
cx,
);
if !last {
let mut border = theme.workspace.pane_divider;
border.left = false;
border.right = false;
border.top = false;
border.bottom = false;
match self.axis {
Axis::Vertical => border.bottom = true,
Axis::Horizontal => border.right = true,
}
member = member.contained().with_border(border).into_any();
}
pane_axis = pane_axis.with_child(member.into_any());
}
pane_axis.set_active_pane(active_pane_ix);
pane_axis.into_any()
}
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
pub enum SplitDirection {
Up,
Down,
Left,
Right,
}
impl SplitDirection {
pub fn all() -> [Self; 4] {
[Self::Up, Self::Down, Self::Left, Self::Right]
}
pub fn edge(&self, rect: RectF) -> f32 {
match self {
Self::Up => rect.min_y(),
Self::Down => rect.max_y(),
Self::Left => rect.min_x(),
Self::Right => rect.max_x(),
}
}
// Returns a new rectangle which shares an edge in SplitDirection and has `size` along SplitDirection
pub fn along_edge(&self, rect: RectF, size: f32) -> RectF {
match self {
Self::Up => RectF::new(rect.origin(), Vector2F::new(rect.width(), size)),
Self::Down => RectF::new(
rect.lower_left() - Vector2F::new(0., size),
Vector2F::new(rect.width(), size),
),
Self::Left => RectF::new(rect.origin(), Vector2F::new(size, rect.height())),
Self::Right => RectF::new(
rect.upper_right() - Vector2F::new(size, 0.),
Vector2F::new(size, rect.height()),
),
}
}
pub fn axis(&self) -> Axis {
match self {
Self::Up | Self::Down => Axis::Vertical,
Self::Left | Self::Right => Axis::Horizontal,
}
}
pub fn increasing(&self) -> bool {
match self {
Self::Left | Self::Up => false,
Self::Down | Self::Right => true,
}
}
}
mod element {
use std::{cell::RefCell, iter::from_fn, ops::Range, rc::Rc};
use gpui::{
geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
},
json::{self, ToJson},
platform::{CursorStyle, MouseButton},
scene::MouseDrag,
AnyElement, Axis, CursorRegion, Element, EventContext, MouseRegion, RectFExt,
SizeConstraint, Vector2FExt, ViewContext,
};
use crate::{
pane_group::{HANDLE_HITBOX_SIZE, HORIZONTAL_MIN_SIZE, VERTICAL_MIN_SIZE},
Workspace, WorkspaceSettings,
};
pub struct PaneAxisElement {
axis: Axis,
basis: usize,
active_pane_ix: Option<usize>,
flexes: Rc<RefCell<Vec<f32>>>,
children: Vec<AnyElement<Workspace>>,
bounding_boxes: Rc<RefCell<Vec<Option<RectF>>>>,
}
impl PaneAxisElement {
pub fn new(
axis: Axis,
basis: usize,
flexes: Rc<RefCell<Vec<f32>>>,
bounding_boxes: Rc<RefCell<Vec<Option<RectF>>>>,
) -> Self {
Self {
axis,
basis,
flexes,
bounding_boxes,
active_pane_ix: None,
children: Default::default(),
}
}
pub fn set_active_pane(&mut self, active_pane_ix: Option<usize>) {
self.active_pane_ix = active_pane_ix;
}
fn layout_children(
&mut self,
active_pane_magnification: f32,
constraint: SizeConstraint,
remaining_space: &mut f32,
remaining_flex: &mut f32,
cross_axis_max: &mut f32,
view: &mut Workspace,
cx: &mut ViewContext<Workspace>,
) {
let flexes = self.flexes.borrow();
let cross_axis = self.axis.invert();
for (ix, child) in self.children.iter_mut().enumerate() {
let flex = if active_pane_magnification != 1. {
if let Some(active_pane_ix) = self.active_pane_ix {
if ix == active_pane_ix {
active_pane_magnification
} else {
1.
}
} else {
1.
}
} else {
flexes[ix]
};
let child_size = if *remaining_flex == 0.0 {
*remaining_space
} else {
let space_per_flex = *remaining_space / *remaining_flex;
space_per_flex * flex
};
let child_constraint = match self.axis {
Axis::Horizontal => SizeConstraint::new(
vec2f(child_size, constraint.min.y()),
vec2f(child_size, constraint.max.y()),
),
Axis::Vertical => SizeConstraint::new(
vec2f(constraint.min.x(), child_size),
vec2f(constraint.max.x(), child_size),
),
};
let child_size = child.layout(child_constraint, view, cx);
*remaining_space -= child_size.along(self.axis);
*remaining_flex -= flex;
*cross_axis_max = cross_axis_max.max(child_size.along(cross_axis));
}
}
fn handle_resize(
flexes: Rc<RefCell<Vec<f32>>>,
axis: Axis,
preceding_ix: usize,
child_start: Vector2F,
drag_bounds: RectF,
) -> impl Fn(MouseDrag, &mut Workspace, &mut EventContext<Workspace>) {
let size = move |ix, flexes: &[f32]| {
drag_bounds.length_along(axis) * (flexes[ix] / flexes.len() as f32)
};
move |drag, workspace: &mut Workspace, cx| {
if drag.end {
// TODO: Clear cascading resize state
return;
}
let min_size = match axis {
Axis::Horizontal => HORIZONTAL_MIN_SIZE,
Axis::Vertical => VERTICAL_MIN_SIZE,
};
let mut flexes = flexes.borrow_mut();
// Don't allow resizing to less than the minimum size, if elements are already too small
if min_size - 1. > size(preceding_ix, flexes.as_slice()) {
return;
}
let mut proposed_current_pixel_change = (drag.position - child_start).along(axis)
- size(preceding_ix, flexes.as_slice());
let flex_changes = |pixel_dx, target_ix, next: isize, flexes: &[f32]| {
let flex_change = pixel_dx / drag_bounds.length_along(axis);
let current_target_flex = flexes[target_ix] + flex_change;
let next_target_flex =
flexes[(target_ix as isize + next) as usize] - flex_change;
(current_target_flex, next_target_flex)
};
let mut successors = from_fn({
let forward = proposed_current_pixel_change > 0.;
let mut ix_offset = 0;
let len = flexes.len();
move || {
let result = if forward {
(preceding_ix + 1 + ix_offset < len).then(|| preceding_ix + ix_offset)
} else {
(preceding_ix as isize - ix_offset as isize >= 0)
.then(|| preceding_ix - ix_offset)
};
ix_offset += 1;
result
}
});
while proposed_current_pixel_change.abs() > 0. {
let Some(current_ix) = successors.next() else {
break;
};
let next_target_size = f32::max(
size(current_ix + 1, flexes.as_slice()) - proposed_current_pixel_change,
min_size,
);
let current_target_size = f32::max(
size(current_ix, flexes.as_slice())
+ size(current_ix + 1, flexes.as_slice())
- next_target_size,
min_size,
);
let current_pixel_change =
current_target_size - size(current_ix, flexes.as_slice());
let (current_target_flex, next_target_flex) =
flex_changes(current_pixel_change, current_ix, 1, flexes.as_slice());
flexes[current_ix] = current_target_flex;
flexes[current_ix + 1] = next_target_flex;
proposed_current_pixel_change -= current_pixel_change;
}
workspace.schedule_serialize(cx);
cx.notify();
}
}
}
impl Extend<AnyElement<Workspace>> for PaneAxisElement {
fn extend<T: IntoIterator<Item = AnyElement<Workspace>>>(&mut self, children: T) {
self.children.extend(children);
}
}
impl Element<Workspace> for PaneAxisElement {
type LayoutState = f32;
type PaintState = ();
fn layout(
&mut self,
constraint: SizeConstraint,
view: &mut Workspace,
cx: &mut ViewContext<Workspace>,
) -> (Vector2F, Self::LayoutState) {
debug_assert!(self.children.len() == self.flexes.borrow().len());
let active_pane_magnification =
settings::get::<WorkspaceSettings>(cx).active_pane_magnification;
let mut remaining_flex = 0.;
if active_pane_magnification != 1. {
let active_pane_flex = self
.active_pane_ix
.map(|_| active_pane_magnification)
.unwrap_or(1.);
remaining_flex += self.children.len() as f32 - 1. + active_pane_flex;
} else {
for flex in self.flexes.borrow().iter() {
remaining_flex += flex;
}
}
let mut cross_axis_max: f32 = 0.0;
let mut remaining_space = constraint.max_along(self.axis);
if remaining_space.is_infinite() {
panic!("flex contains flexible children but has an infinite constraint along the flex axis");
}
self.layout_children(
active_pane_magnification,
constraint,
&mut remaining_space,
&mut remaining_flex,
&mut cross_axis_max,
view,
cx,
);
let mut size = match self.axis {
Axis::Horizontal => vec2f(constraint.max.x() - remaining_space, cross_axis_max),
Axis::Vertical => vec2f(cross_axis_max, constraint.max.y() - remaining_space),
};
if constraint.min.x().is_finite() {
size.set_x(size.x().max(constraint.min.x()));
}
if constraint.min.y().is_finite() {
size.set_y(size.y().max(constraint.min.y()));
}
if size.x() > constraint.max.x() {
size.set_x(constraint.max.x());
}
if size.y() > constraint.max.y() {
size.set_y(constraint.max.y());
}
(size, remaining_space)
}
fn paint(
&mut self,
bounds: RectF,
visible_bounds: RectF,
remaining_space: &mut Self::LayoutState,
view: &mut Workspace,
cx: &mut ViewContext<Workspace>,
) -> Self::PaintState {
let can_resize = settings::get::<WorkspaceSettings>(cx).active_pane_magnification == 1.;
let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
let overflowing = *remaining_space < 0.;
if overflowing {
cx.scene().push_layer(Some(visible_bounds));
}
let mut child_origin = bounds.origin();
let mut bounding_boxes = self.bounding_boxes.borrow_mut();
bounding_boxes.clear();
let mut children_iter = self.children.iter_mut().enumerate().peekable();
while let Some((ix, child)) = children_iter.next() {
let child_start = child_origin.clone();
child.paint(child_origin, visible_bounds, view, cx);
bounding_boxes.push(Some(RectF::new(child_origin, child.size())));
match self.axis {
Axis::Horizontal => child_origin += vec2f(child.size().x(), 0.0),
Axis::Vertical => child_origin += vec2f(0.0, child.size().y()),
}
if can_resize && children_iter.peek().is_some() {
cx.scene().push_stacking_context(None, None);
let handle_origin = match self.axis {
Axis::Horizontal => child_origin - vec2f(HANDLE_HITBOX_SIZE / 2., 0.0),
Axis::Vertical => child_origin - vec2f(0.0, HANDLE_HITBOX_SIZE / 2.),
};
let handle_bounds = match self.axis {
Axis::Horizontal => RectF::new(
handle_origin,
vec2f(HANDLE_HITBOX_SIZE, visible_bounds.height()),
),
Axis::Vertical => RectF::new(
handle_origin,
vec2f(visible_bounds.width(), HANDLE_HITBOX_SIZE),
),
};
let style = match self.axis {
Axis::Horizontal => CursorStyle::ResizeLeftRight,
Axis::Vertical => CursorStyle::ResizeUpDown,
};
cx.scene().push_cursor_region(CursorRegion {
bounds: handle_bounds,
style,
});
enum ResizeHandle {}
let mut mouse_region = MouseRegion::new::<ResizeHandle>(
cx.view_id(),
self.basis + ix,
handle_bounds,
);
mouse_region = mouse_region
.on_drag(
MouseButton::Left,
Self::handle_resize(
self.flexes.clone(),
self.axis,
ix,
child_start,
visible_bounds.clone(),
),
)
.on_click(MouseButton::Left, {
let flexes = self.flexes.clone();
move |e, v: &mut Workspace, cx| {
if e.click_count >= 2 {
let mut borrow = flexes.borrow_mut();
*borrow = vec![1.; borrow.len()];
v.schedule_serialize(cx);
cx.notify();
}
}
});
cx.scene().push_mouse_region(mouse_region);
cx.scene().pop_stacking_context();
}
}
if overflowing {
cx.scene().pop_layer();
}
}
fn rect_for_text_range(
&self,
range_utf16: Range<usize>,
_: RectF,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
view: &Workspace,
cx: &ViewContext<Workspace>,
) -> Option<RectF> {
self.children
.iter()
.find_map(|child| child.rect_for_text_range(range_utf16.clone(), view, cx))
}
fn debug(
&self,
bounds: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
view: &Workspace,
cx: &ViewContext<Workspace>,
) -> json::Value {
serde_json::json!({
"type": "PaneAxis",
"bounds": bounds.to_json(),
"axis": self.axis.to_json(),
"flexes": *self.flexes.borrow(),
"children": self.children.iter().map(|child| child.debug(view, cx)).collect::<Vec<json::Value>>()
})
}
}
}

View File

@ -0,0 +1,972 @@
#![allow(dead_code)]
pub mod model;
use std::path::Path;
use anyhow::{anyhow, bail, Context, Result};
use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql};
use gpui::{platform::WindowBounds, Axis};
use util::{unzip_option, ResultExt};
use uuid::Uuid;
use crate::WorkspaceId;
use model::{
GroupId, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
WorkspaceLocation,
};
use self::model::DockStructure;
define_connection! {
// Current schema shape using pseudo-rust syntax:
//
// workspaces(
// workspace_id: usize, // Primary key for workspaces
// workspace_location: Bincode<Vec<PathBuf>>,
// dock_visible: bool, // Deprecated
// dock_anchor: DockAnchor, // Deprecated
// dock_pane: Option<usize>, // Deprecated
// left_sidebar_open: boolean,
// timestamp: String, // UTC YYYY-MM-DD HH:MM:SS
// window_state: String, // WindowBounds Discriminant
// window_x: Option<f32>, // WindowBounds::Fixed RectF x
// window_y: Option<f32>, // WindowBounds::Fixed RectF y
// window_width: Option<f32>, // WindowBounds::Fixed RectF width
// window_height: Option<f32>, // WindowBounds::Fixed RectF height
// display: Option<Uuid>, // Display id
// )
//
// pane_groups(
// group_id: usize, // Primary key for pane_groups
// workspace_id: usize, // References workspaces table
// parent_group_id: Option<usize>, // None indicates that this is the root node
// position: Optiopn<usize>, // None indicates that this is the root node
// axis: Option<Axis>, // 'Vertical', 'Horizontal'
// flexes: Option<Vec<f32>>, // A JSON array of floats
// )
//
// panes(
// pane_id: usize, // Primary key for panes
// workspace_id: usize, // References workspaces table
// active: bool,
// )
//
// center_panes(
// pane_id: usize, // Primary key for center_panes
// parent_group_id: Option<usize>, // References pane_groups. If none, this is the root
// position: Option<usize>, // None indicates this is the root
// )
//
// CREATE TABLE items(
// item_id: usize, // This is the item's view id, so this is not unique
// workspace_id: usize, // References workspaces table
// pane_id: usize, // References panes table
// kind: String, // Indicates which view this connects to. This is the key in the item_deserializers global
// position: usize, // Position of the item in the parent pane. This is equivalent to panes' position column
// active: bool, // Indicates if this item is the active one in the pane
// )
pub static ref DB: WorkspaceDb<()> =
&[sql!(
CREATE TABLE workspaces(
workspace_id INTEGER PRIMARY KEY,
workspace_location BLOB UNIQUE,
dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed.
dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed.
dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed.
left_sidebar_open INTEGER, // Boolean
timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
FOREIGN KEY(dock_pane) REFERENCES panes(pane_id)
) STRICT;
CREATE TABLE pane_groups(
group_id INTEGER PRIMARY KEY,
workspace_id INTEGER NOT NULL,
parent_group_id INTEGER, // NULL indicates that this is a root node
position INTEGER, // NULL indicates that this is a root node
axis TEXT NOT NULL, // Enum: 'Vertical' / 'Horizontal'
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
ON DELETE CASCADE
ON UPDATE CASCADE,
FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
) STRICT;
CREATE TABLE panes(
pane_id INTEGER PRIMARY KEY,
workspace_id INTEGER NOT NULL,
active INTEGER NOT NULL, // Boolean
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
ON DELETE CASCADE
ON UPDATE CASCADE
) STRICT;
CREATE TABLE center_panes(
pane_id INTEGER PRIMARY KEY,
parent_group_id INTEGER, // NULL means that this is a root pane
position INTEGER, // NULL means that this is a root pane
FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
ON DELETE CASCADE,
FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
) STRICT;
CREATE TABLE items(
item_id INTEGER NOT NULL, // This is the item's view id, so this is not unique
workspace_id INTEGER NOT NULL,
pane_id INTEGER NOT NULL,
kind TEXT NOT NULL,
position INTEGER NOT NULL,
active INTEGER NOT NULL,
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
ON DELETE CASCADE
ON UPDATE CASCADE,
FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
ON DELETE CASCADE,
PRIMARY KEY(item_id, workspace_id)
) STRICT;
),
sql!(
ALTER TABLE workspaces ADD COLUMN window_state TEXT;
ALTER TABLE workspaces ADD COLUMN window_x REAL;
ALTER TABLE workspaces ADD COLUMN window_y REAL;
ALTER TABLE workspaces ADD COLUMN window_width REAL;
ALTER TABLE workspaces ADD COLUMN window_height REAL;
ALTER TABLE workspaces ADD COLUMN display BLOB;
),
// Drop foreign key constraint from workspaces.dock_pane to panes table.
sql!(
CREATE TABLE workspaces_2(
workspace_id INTEGER PRIMARY KEY,
workspace_location BLOB UNIQUE,
dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed.
dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed.
dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed.
left_sidebar_open INTEGER, // Boolean
timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
window_state TEXT,
window_x REAL,
window_y REAL,
window_width REAL,
window_height REAL,
display BLOB
) STRICT;
INSERT INTO workspaces_2 SELECT * FROM workspaces;
DROP TABLE workspaces;
ALTER TABLE workspaces_2 RENAME TO workspaces;
),
// Add panels related information
sql!(
ALTER TABLE workspaces ADD COLUMN left_dock_visible INTEGER; //bool
ALTER TABLE workspaces ADD COLUMN left_dock_active_panel TEXT;
ALTER TABLE workspaces ADD COLUMN right_dock_visible INTEGER; //bool
ALTER TABLE workspaces ADD COLUMN right_dock_active_panel TEXT;
ALTER TABLE workspaces ADD COLUMN bottom_dock_visible INTEGER; //bool
ALTER TABLE workspaces ADD COLUMN bottom_dock_active_panel TEXT;
),
// Add panel zoom persistence
sql!(
ALTER TABLE workspaces ADD COLUMN left_dock_zoom INTEGER; //bool
ALTER TABLE workspaces ADD COLUMN right_dock_zoom INTEGER; //bool
ALTER TABLE workspaces ADD COLUMN bottom_dock_zoom INTEGER; //bool
),
// Add pane group flex data
sql!(
ALTER TABLE pane_groups ADD COLUMN flexes TEXT;
)
];
}
impl WorkspaceDb {
/// Returns a serialized workspace for the given worktree_roots. If the passed array
/// is empty, the most recent workspace is returned instead. If no workspace for the
/// passed roots is stored, returns none.
pub fn workspace_for_roots<P: AsRef<Path>>(
&self,
worktree_roots: &[P],
) -> Option<SerializedWorkspace> {
let workspace_location: WorkspaceLocation = worktree_roots.into();
// Note that we re-assign the workspace_id here in case it's empty
// and we've grabbed the most recent workspace
let (workspace_id, workspace_location, bounds, display, docks): (
WorkspaceId,
WorkspaceLocation,
Option<WindowBounds>,
Option<Uuid>,
DockStructure,
) = self
.select_row_bound(sql! {
SELECT
workspace_id,
workspace_location,
window_state,
window_x,
window_y,
window_width,
window_height,
display,
left_dock_visible,
left_dock_active_panel,
left_dock_zoom,
right_dock_visible,
right_dock_active_panel,
right_dock_zoom,
bottom_dock_visible,
bottom_dock_active_panel,
bottom_dock_zoom
FROM workspaces
WHERE workspace_location = ?
})
.and_then(|mut prepared_statement| (prepared_statement)(&workspace_location))
.context("No workspaces found")
.warn_on_err()
.flatten()?;
Some(SerializedWorkspace {
id: workspace_id,
location: workspace_location.clone(),
center_group: self
.get_center_pane_group(workspace_id)
.context("Getting center group")
.log_err()?,
bounds,
display,
docks,
})
}
/// Saves a workspace using the worktree roots. Will garbage collect any workspaces
/// that used this workspace previously
pub async fn save_workspace(&self, workspace: SerializedWorkspace) {
self.write(move |conn| {
conn.with_savepoint("update_worktrees", || {
// Clear out panes and pane_groups
conn.exec_bound(sql!(
DELETE FROM pane_groups WHERE workspace_id = ?1;
DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id)
.expect("Clearing old panes");
conn.exec_bound(sql!(
DELETE FROM workspaces WHERE workspace_location = ? AND workspace_id != ?
))?((&workspace.location, workspace.id.clone()))
.context("clearing out old locations")?;
// Upsert
conn.exec_bound(sql!(
INSERT INTO workspaces(
workspace_id,
workspace_location,
left_dock_visible,
left_dock_active_panel,
left_dock_zoom,
right_dock_visible,
right_dock_active_panel,
right_dock_zoom,
bottom_dock_visible,
bottom_dock_active_panel,
bottom_dock_zoom,
timestamp
)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, CURRENT_TIMESTAMP)
ON CONFLICT DO
UPDATE SET
workspace_location = ?2,
left_dock_visible = ?3,
left_dock_active_panel = ?4,
left_dock_zoom = ?5,
right_dock_visible = ?6,
right_dock_active_panel = ?7,
right_dock_zoom = ?8,
bottom_dock_visible = ?9,
bottom_dock_active_panel = ?10,
bottom_dock_zoom = ?11,
timestamp = CURRENT_TIMESTAMP
))?((workspace.id, &workspace.location, workspace.docks))
.context("Updating workspace")?;
// Save center pane group
Self::save_pane_group(conn, workspace.id, &workspace.center_group, None)
.context("save pane group in save workspace")?;
Ok(())
})
.log_err();
})
.await;
}
query! {
pub async fn next_id() -> Result<WorkspaceId> {
INSERT INTO workspaces DEFAULT VALUES RETURNING workspace_id
}
}
query! {
fn recent_workspaces() -> Result<Vec<(WorkspaceId, WorkspaceLocation)>> {
SELECT workspace_id, workspace_location
FROM workspaces
WHERE workspace_location IS NOT NULL
ORDER BY timestamp DESC
}
}
query! {
async fn delete_stale_workspace(id: WorkspaceId) -> Result<()> {
DELETE FROM workspaces
WHERE workspace_id IS ?
}
}
// Returns the recent locations which are still valid on disk and deletes ones which no longer
// exist.
pub async fn recent_workspaces_on_disk(&self) -> Result<Vec<(WorkspaceId, WorkspaceLocation)>> {
let mut result = Vec::new();
let mut delete_tasks = Vec::new();
for (id, location) in self.recent_workspaces()? {
if location.paths().iter().all(|path| path.exists())
&& location.paths().iter().any(|path| path.is_dir())
{
result.push((id, location));
} else {
delete_tasks.push(self.delete_stale_workspace(id));
}
}
futures::future::join_all(delete_tasks).await;
Ok(result)
}
pub async fn last_workspace(&self) -> Result<Option<WorkspaceLocation>> {
Ok(self
.recent_workspaces_on_disk()
.await?
.into_iter()
.next()
.map(|(_, location)| location))
}
fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result<SerializedPaneGroup> {
Ok(self
.get_pane_group(workspace_id, None)?
.into_iter()
.next()
.unwrap_or_else(|| {
SerializedPaneGroup::Pane(SerializedPane {
active: true,
children: vec![],
})
}))
}
fn get_pane_group(
&self,
workspace_id: WorkspaceId,
group_id: Option<GroupId>,
) -> Result<Vec<SerializedPaneGroup>> {
type GroupKey = (Option<GroupId>, WorkspaceId);
type GroupOrPane = (
Option<GroupId>,
Option<Axis>,
Option<PaneId>,
Option<bool>,
Option<String>,
);
self.select_bound::<GroupKey, GroupOrPane>(sql!(
SELECT group_id, axis, pane_id, active, flexes
FROM (SELECT
group_id,
axis,
NULL as pane_id,
NULL as active,
position,
parent_group_id,
workspace_id,
flexes
FROM pane_groups
UNION
SELECT
NULL,
NULL,
center_panes.pane_id,
panes.active as active,
position,
parent_group_id,
panes.workspace_id as workspace_id,
NULL
FROM center_panes
JOIN panes ON center_panes.pane_id = panes.pane_id)
WHERE parent_group_id IS ? AND workspace_id = ?
ORDER BY position
))?((group_id, workspace_id))?
.into_iter()
.map(|(group_id, axis, pane_id, active, flexes)| {
if let Some((group_id, axis)) = group_id.zip(axis) {
let flexes = flexes
.map(|flexes| serde_json::from_str::<Vec<f32>>(&flexes))
.transpose()?;
Ok(SerializedPaneGroup::Group {
axis,
children: self.get_pane_group(workspace_id, Some(group_id))?,
flexes,
})
} else if let Some((pane_id, active)) = pane_id.zip(active) {
Ok(SerializedPaneGroup::Pane(SerializedPane::new(
self.get_items(pane_id)?,
active,
)))
} else {
bail!("Pane Group Child was neither a pane group or a pane");
}
})
// Filter out panes and pane groups which don't have any children or items
.filter(|pane_group| match pane_group {
Ok(SerializedPaneGroup::Group { children, .. }) => !children.is_empty(),
Ok(SerializedPaneGroup::Pane(pane)) => !pane.children.is_empty(),
_ => true,
})
.collect::<Result<_>>()
}
fn save_pane_group(
conn: &Connection,
workspace_id: WorkspaceId,
pane_group: &SerializedPaneGroup,
parent: Option<(GroupId, usize)>,
) -> Result<()> {
match pane_group {
SerializedPaneGroup::Group {
axis,
children,
flexes,
} => {
let (parent_id, position) = unzip_option(parent);
let flex_string = flexes
.as_ref()
.map(|flexes| serde_json::json!(flexes).to_string());
let group_id = conn.select_row_bound::<_, i64>(sql!(
INSERT INTO pane_groups(
workspace_id,
parent_group_id,
position,
axis,
flexes
)
VALUES (?, ?, ?, ?, ?)
RETURNING group_id
))?((
workspace_id,
parent_id,
position,
*axis,
flex_string,
))?
.ok_or_else(|| anyhow!("Couldn't retrieve group_id from inserted pane_group"))?;
for (position, group) in children.iter().enumerate() {
Self::save_pane_group(conn, workspace_id, group, Some((group_id, position)))?
}
Ok(())
}
SerializedPaneGroup::Pane(pane) => {
Self::save_pane(conn, workspace_id, &pane, parent)?;
Ok(())
}
}
}
fn save_pane(
conn: &Connection,
workspace_id: WorkspaceId,
pane: &SerializedPane,
parent: Option<(GroupId, usize)>,
) -> Result<PaneId> {
let pane_id = conn.select_row_bound::<_, i64>(sql!(
INSERT INTO panes(workspace_id, active)
VALUES (?, ?)
RETURNING pane_id
))?((workspace_id, pane.active))?
.ok_or_else(|| anyhow!("Could not retrieve inserted pane_id"))?;
let (parent_id, order) = unzip_option(parent);
conn.exec_bound(sql!(
INSERT INTO center_panes(pane_id, parent_group_id, position)
VALUES (?, ?, ?)
))?((pane_id, parent_id, order))?;
Self::save_items(conn, workspace_id, pane_id, &pane.children).context("Saving items")?;
Ok(pane_id)
}
fn get_items(&self, pane_id: PaneId) -> Result<Vec<SerializedItem>> {
Ok(self.select_bound(sql!(
SELECT kind, item_id, active FROM items
WHERE pane_id = ?
ORDER BY position
))?(pane_id)?)
}
fn save_items(
conn: &Connection,
workspace_id: WorkspaceId,
pane_id: PaneId,
items: &[SerializedItem],
) -> Result<()> {
let mut insert = conn.exec_bound(sql!(
INSERT INTO items(workspace_id, pane_id, position, kind, item_id, active) VALUES (?, ?, ?, ?, ?, ?)
)).context("Preparing insertion")?;
for (position, item) in items.iter().enumerate() {
insert((workspace_id, pane_id, position, item))?;
}
Ok(())
}
query! {
pub async fn update_timestamp(workspace_id: WorkspaceId) -> Result<()> {
UPDATE workspaces
SET timestamp = CURRENT_TIMESTAMP
WHERE workspace_id = ?
}
}
query! {
pub async fn set_window_bounds(workspace_id: WorkspaceId, bounds: WindowBounds, display: Uuid) -> Result<()> {
UPDATE workspaces
SET window_state = ?2,
window_x = ?3,
window_y = ?4,
window_width = ?5,
window_height = ?6,
display = ?7
WHERE workspace_id = ?1
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use db::open_test_db;
#[gpui::test]
async fn test_next_id_stability() {
env_logger::try_init().ok();
let db = WorkspaceDb(open_test_db("test_next_id_stability").await);
db.write(|conn| {
conn.migrate(
"test_table",
&[sql!(
CREATE TABLE test_table(
text TEXT,
workspace_id INTEGER,
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
ON DELETE CASCADE
) STRICT;
)],
)
.unwrap();
})
.await;
let id = db.next_id().await.unwrap();
// Assert the empty row got inserted
assert_eq!(
Some(id),
db.select_row_bound::<WorkspaceId, WorkspaceId>(sql!(
SELECT workspace_id FROM workspaces WHERE workspace_id = ?
))
.unwrap()(id)
.unwrap()
);
db.write(move |conn| {
conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
.unwrap()(("test-text-1", id))
.unwrap()
})
.await;
let test_text_1 = db
.select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
.unwrap()(1)
.unwrap()
.unwrap();
assert_eq!(test_text_1, "test-text-1");
}
#[gpui::test]
async fn test_workspace_id_stability() {
env_logger::try_init().ok();
let db = WorkspaceDb(open_test_db("test_workspace_id_stability").await);
db.write(|conn| {
conn.migrate(
"test_table",
&[sql!(
CREATE TABLE test_table(
text TEXT,
workspace_id INTEGER,
FOREIGN KEY(workspace_id)
REFERENCES workspaces(workspace_id)
ON DELETE CASCADE
) STRICT;)],
)
})
.await
.unwrap();
let mut workspace_1 = SerializedWorkspace {
id: 1,
location: (["/tmp", "/tmp2"]).into(),
center_group: Default::default(),
bounds: Default::default(),
display: Default::default(),
docks: Default::default(),
};
let workspace_2 = SerializedWorkspace {
id: 2,
location: (["/tmp"]).into(),
center_group: Default::default(),
bounds: Default::default(),
display: Default::default(),
docks: Default::default(),
};
db.save_workspace(workspace_1.clone()).await;
db.write(|conn| {
conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
.unwrap()(("test-text-1", 1))
.unwrap();
})
.await;
db.save_workspace(workspace_2.clone()).await;
db.write(|conn| {
conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
.unwrap()(("test-text-2", 2))
.unwrap();
})
.await;
workspace_1.location = (["/tmp", "/tmp3"]).into();
db.save_workspace(workspace_1.clone()).await;
db.save_workspace(workspace_1).await;
db.save_workspace(workspace_2).await;
let test_text_2 = db
.select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
.unwrap()(2)
.unwrap()
.unwrap();
assert_eq!(test_text_2, "test-text-2");
let test_text_1 = db
.select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
.unwrap()(1)
.unwrap()
.unwrap();
assert_eq!(test_text_1, "test-text-1");
}
fn group(axis: gpui::Axis, children: Vec<SerializedPaneGroup>) -> SerializedPaneGroup {
SerializedPaneGroup::Group {
axis,
flexes: None,
children,
}
}
#[gpui::test]
async fn test_full_workspace_serialization() {
env_logger::try_init().ok();
let db = WorkspaceDb(open_test_db("test_full_workspace_serialization").await);
// -----------------
// | 1,2 | 5,6 |
// | - - - | |
// | 3,4 | |
// -----------------
let center_group = group(
gpui::Axis::Horizontal,
vec![
group(
gpui::Axis::Vertical,
vec![
SerializedPaneGroup::Pane(SerializedPane::new(
vec![
SerializedItem::new("Terminal", 5, false),
SerializedItem::new("Terminal", 6, true),
],
false,
)),
SerializedPaneGroup::Pane(SerializedPane::new(
vec![
SerializedItem::new("Terminal", 7, true),
SerializedItem::new("Terminal", 8, false),
],
false,
)),
],
),
SerializedPaneGroup::Pane(SerializedPane::new(
vec![
SerializedItem::new("Terminal", 9, false),
SerializedItem::new("Terminal", 10, true),
],
false,
)),
],
);
let workspace = SerializedWorkspace {
id: 5,
location: (["/tmp", "/tmp2"]).into(),
center_group,
bounds: Default::default(),
display: Default::default(),
docks: Default::default(),
};
db.save_workspace(workspace.clone()).await;
let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
assert_eq!(workspace, round_trip_workspace.unwrap());
// Test guaranteed duplicate IDs
db.save_workspace(workspace.clone()).await;
db.save_workspace(workspace.clone()).await;
let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
assert_eq!(workspace, round_trip_workspace.unwrap());
}
#[gpui::test]
async fn test_workspace_assignment() {
env_logger::try_init().ok();
let db = WorkspaceDb(open_test_db("test_basic_functionality").await);
let workspace_1 = SerializedWorkspace {
id: 1,
location: (["/tmp", "/tmp2"]).into(),
center_group: Default::default(),
bounds: Default::default(),
display: Default::default(),
docks: Default::default(),
};
let mut workspace_2 = SerializedWorkspace {
id: 2,
location: (["/tmp"]).into(),
center_group: Default::default(),
bounds: Default::default(),
display: Default::default(),
docks: Default::default(),
};
db.save_workspace(workspace_1.clone()).await;
db.save_workspace(workspace_2.clone()).await;
// Test that paths are treated as a set
assert_eq!(
db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
workspace_1
);
assert_eq!(
db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
workspace_1
);
// Make sure that other keys work
assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
// Test 'mutate' case of updating a pre-existing id
workspace_2.location = (["/tmp", "/tmp2"]).into();
db.save_workspace(workspace_2.clone()).await;
assert_eq!(
db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
workspace_2
);
// Test other mechanism for mutating
let mut workspace_3 = SerializedWorkspace {
id: 3,
location: (&["/tmp", "/tmp2"]).into(),
center_group: Default::default(),
bounds: Default::default(),
display: Default::default(),
docks: Default::default(),
};
db.save_workspace(workspace_3.clone()).await;
assert_eq!(
db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
workspace_3
);
// Make sure that updating paths differently also works
workspace_3.location = (["/tmp3", "/tmp4", "/tmp2"]).into();
db.save_workspace(workspace_3.clone()).await;
assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
assert_eq!(
db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
.unwrap(),
workspace_3
);
}
use crate::persistence::model::SerializedWorkspace;
use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup};
fn default_workspace<P: AsRef<Path>>(
workspace_id: &[P],
center_group: &SerializedPaneGroup,
) -> SerializedWorkspace {
SerializedWorkspace {
id: 4,
location: workspace_id.into(),
center_group: center_group.clone(),
bounds: Default::default(),
display: Default::default(),
docks: Default::default(),
}
}
#[gpui::test]
async fn test_simple_split() {
env_logger::try_init().ok();
let db = WorkspaceDb(open_test_db("simple_split").await);
// -----------------
// | 1,2 | 5,6 |
// | - - - | |
// | 3,4 | |
// -----------------
let center_pane = group(
gpui::Axis::Horizontal,
vec![
group(
gpui::Axis::Vertical,
vec![
SerializedPaneGroup::Pane(SerializedPane::new(
vec![
SerializedItem::new("Terminal", 1, false),
SerializedItem::new("Terminal", 2, true),
],
false,
)),
SerializedPaneGroup::Pane(SerializedPane::new(
vec![
SerializedItem::new("Terminal", 4, false),
SerializedItem::new("Terminal", 3, true),
],
true,
)),
],
),
SerializedPaneGroup::Pane(SerializedPane::new(
vec![
SerializedItem::new("Terminal", 5, true),
SerializedItem::new("Terminal", 6, false),
],
false,
)),
],
);
let workspace = default_workspace(&["/tmp"], &center_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, &center_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);
}
}

View File

@ -0,0 +1,344 @@
use crate::{item::ItemHandle, ItemDeserializers, Member, Pane, PaneAxis, Workspace, WorkspaceId};
use anyhow::{Context, Result};
use async_recursion::async_recursion;
use db::sqlez::{
bindable::{Bind, Column, StaticColumnCount},
statement::Statement,
};
use gpui::{
platform::WindowBounds, AsyncAppContext, Axis, ModelHandle, Task, ViewHandle, WeakViewHandle,
};
use project::Project;
use std::{
path::{Path, PathBuf},
sync::Arc,
};
use util::ResultExt;
use uuid::Uuid;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkspaceLocation(Arc<Vec<PathBuf>>);
impl WorkspaceLocation {
pub fn paths(&self) -> Arc<Vec<PathBuf>> {
self.0.clone()
}
}
impl<P: AsRef<Path>, T: IntoIterator<Item = P>> From<T> for WorkspaceLocation {
fn from(iterator: T) -> Self {
let mut roots = iterator
.into_iter()
.map(|p| p.as_ref().to_path_buf())
.collect::<Vec<_>>();
roots.sort();
Self(Arc::new(roots))
}
}
impl StaticColumnCount for WorkspaceLocation {}
impl Bind for &WorkspaceLocation {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
bincode::serialize(&self.0)
.expect("Bincode serialization of paths should not fail")
.bind(statement, start_index)
}
}
impl Column for WorkspaceLocation {
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
let blob = statement.column_blob(start_index)?;
Ok((
WorkspaceLocation(bincode::deserialize(blob).context("Bincode failed")?),
start_index + 1,
))
}
}
#[derive(Debug, PartialEq, Clone)]
pub struct SerializedWorkspace {
pub id: WorkspaceId,
pub location: WorkspaceLocation,
pub center_group: SerializedPaneGroup,
pub bounds: Option<WindowBounds>,
pub display: Option<Uuid>,
pub docks: DockStructure,
}
#[derive(Debug, PartialEq, Clone, Default)]
pub struct DockStructure {
pub(crate) left: DockData,
pub(crate) right: DockData,
pub(crate) bottom: DockData,
}
impl Column for DockStructure {
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
let (left, next_index) = DockData::column(statement, start_index)?;
let (right, next_index) = DockData::column(statement, next_index)?;
let (bottom, next_index) = DockData::column(statement, next_index)?;
Ok((
DockStructure {
left,
right,
bottom,
},
next_index,
))
}
}
impl Bind for DockStructure {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
let next_index = statement.bind(&self.left, start_index)?;
let next_index = statement.bind(&self.right, next_index)?;
statement.bind(&self.bottom, next_index)
}
}
#[derive(Debug, PartialEq, Clone, Default)]
pub struct DockData {
pub(crate) visible: bool,
pub(crate) active_panel: Option<String>,
pub(crate) zoom: bool,
}
impl Column for DockData {
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
let (visible, next_index) = Option::<bool>::column(statement, start_index)?;
let (active_panel, next_index) = Option::<String>::column(statement, next_index)?;
let (zoom, next_index) = Option::<bool>::column(statement, next_index)?;
Ok((
DockData {
visible: visible.unwrap_or(false),
active_panel,
zoom: zoom.unwrap_or(false),
},
next_index,
))
}
}
impl Bind for DockData {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
let next_index = statement.bind(&self.visible, start_index)?;
let next_index = statement.bind(&self.active_panel, next_index)?;
statement.bind(&self.zoom, next_index)
}
}
#[derive(Debug, PartialEq, Clone)]
pub enum SerializedPaneGroup {
Group {
axis: Axis,
flexes: Option<Vec<f32>>,
children: Vec<SerializedPaneGroup>,
},
Pane(SerializedPane),
}
#[cfg(test)]
impl Default for SerializedPaneGroup {
fn default() -> Self {
Self::Pane(SerializedPane {
children: vec![SerializedItem::default()],
active: false,
})
}
}
impl SerializedPaneGroup {
#[async_recursion(?Send)]
pub(crate) async fn deserialize(
self,
project: &ModelHandle<Project>,
workspace_id: WorkspaceId,
workspace: &WeakViewHandle<Workspace>,
cx: &mut AsyncAppContext,
) -> Option<(
Member,
Option<ViewHandle<Pane>>,
Vec<Option<Box<dyn ItemHandle>>>,
)> {
match self {
SerializedPaneGroup::Group {
axis,
children,
flexes,
} => {
let mut current_active_pane = None;
let mut members = Vec::new();
let mut items = Vec::new();
for child in children {
if let Some((new_member, active_pane, new_items)) = child
.deserialize(project, workspace_id, workspace, cx)
.await
{
members.push(new_member);
items.extend(new_items);
current_active_pane = current_active_pane.or(active_pane);
}
}
if members.is_empty() {
return None;
}
if members.len() == 1 {
return Some((members.remove(0), current_active_pane, items));
}
Some((
Member::Axis(PaneAxis::load(axis, members, flexes)),
current_active_pane,
items,
))
}
SerializedPaneGroup::Pane(serialized_pane) => {
let pane = workspace
.update(cx, |workspace, cx| workspace.add_pane(cx).downgrade())
.log_err()?;
let active = serialized_pane.active;
let new_items = serialized_pane
.deserialize_to(project, &pane, workspace_id, workspace, cx)
.await
.log_err()?;
if pane
.read_with(cx, |pane, _| pane.items_len() != 0)
.log_err()?
{
let pane = pane.upgrade(cx)?;
Some((Member::Pane(pane.clone()), active.then(|| pane), new_items))
} else {
let pane = pane.upgrade(cx)?;
workspace
.update(cx, |workspace, cx| workspace.force_remove_pane(&pane, cx))
.log_err()?;
None
}
}
}
}
}
#[derive(Debug, PartialEq, Eq, Default, Clone)]
pub struct SerializedPane {
pub(crate) active: bool,
pub(crate) children: Vec<SerializedItem>,
}
impl SerializedPane {
pub fn new(children: Vec<SerializedItem>, active: bool) -> Self {
SerializedPane { children, active }
}
pub async fn deserialize_to(
&self,
project: &ModelHandle<Project>,
pane: &WeakViewHandle<Pane>,
workspace_id: WorkspaceId,
workspace: &WeakViewHandle<Workspace>,
cx: &mut AsyncAppContext,
) -> Result<Vec<Option<Box<dyn ItemHandle>>>> {
let mut items = Vec::new();
let mut active_item_index = None;
for (index, item) in self.children.iter().enumerate() {
let project = project.clone();
let item_handle = pane
.update(cx, |_, cx| {
if let Some(deserializer) = cx.global::<ItemDeserializers>().get(&item.kind) {
deserializer(project, workspace.clone(), workspace_id, item.item_id, cx)
} else {
Task::ready(Err(anyhow::anyhow!(
"Deserializer does not exist for item kind: {}",
item.kind
)))
}
})?
.await
.log_err();
items.push(item_handle.clone());
if let Some(item_handle) = item_handle {
pane.update(cx, |pane, cx| {
pane.add_item(item_handle.clone(), true, true, None, cx);
})?;
}
if item.active {
active_item_index = Some(index);
}
}
if let Some(active_item_index) = active_item_index {
pane.update(cx, |pane, cx| {
pane.activate_item(active_item_index, false, false, cx);
})?;
}
anyhow::Ok(items)
}
}
pub type GroupId = i64;
pub type PaneId = i64;
pub type ItemId = usize;
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct SerializedItem {
pub kind: Arc<str>,
pub item_id: ItemId,
pub active: bool,
}
impl SerializedItem {
pub fn new(kind: impl AsRef<str>, item_id: ItemId, active: bool) -> Self {
Self {
kind: Arc::from(kind.as_ref()),
item_id,
active,
}
}
}
#[cfg(test)]
impl Default for SerializedItem {
fn default() -> Self {
SerializedItem {
kind: Arc::from("Terminal"),
item_id: 100000,
active: false,
}
}
}
impl StaticColumnCount for SerializedItem {
fn column_count() -> usize {
3
}
}
impl Bind for &SerializedItem {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
let next_index = statement.bind(&self.kind, start_index)?;
let next_index = statement.bind(&self.item_id, next_index)?;
statement.bind(&self.active, next_index)
}
}
impl Column for SerializedItem {
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
let (kind, next_index) = Arc::<str>::column(statement, start_index)?;
let (item_id, next_index) = ItemId::column(statement, next_index)?;
let (active, next_index) = bool::column(statement, next_index)?;
Ok((
SerializedItem {
kind,
item_id,
active,
},
next_index,
))
}
}

View File

@ -0,0 +1,282 @@
use std::{any::Any, sync::Arc};
use gpui::{
AnyViewHandle, AnyWeakViewHandle, AppContext, Subscription, Task, ViewContext, ViewHandle,
WeakViewHandle, WindowContext,
};
use project::search::SearchQuery;
use crate::{item::WeakItemHandle, Item, ItemHandle};
#[derive(Debug)]
pub enum SearchEvent {
MatchesInvalidated,
ActiveMatchChanged,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Direction {
Prev,
Next,
}
#[derive(Clone, Copy, Debug, Default)]
pub struct SearchOptions {
pub case: bool,
pub word: bool,
pub regex: bool,
/// Specifies whether the item supports search & replace.
pub replacement: bool,
}
pub trait SearchableItem: Item {
type Match: Any + Sync + Send + Clone;
fn supported_options() -> SearchOptions {
SearchOptions {
case: true,
word: true,
regex: true,
replacement: true,
}
}
fn to_search_event(
&mut self,
event: &Self::Event,
cx: &mut ViewContext<Self>,
) -> Option<SearchEvent>;
fn clear_matches(&mut self, cx: &mut ViewContext<Self>);
fn update_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>);
fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String;
fn activate_match(
&mut self,
index: usize,
matches: Vec<Self::Match>,
cx: &mut ViewContext<Self>,
);
fn select_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>);
fn replace(&mut self, _: &Self::Match, _: &SearchQuery, _: &mut ViewContext<Self>);
fn match_index_for_direction(
&mut self,
matches: &Vec<Self::Match>,
current_index: usize,
direction: Direction,
count: usize,
_: &mut ViewContext<Self>,
) -> usize {
match direction {
Direction::Prev => {
let count = count % matches.len();
if current_index >= count {
current_index - count
} else {
matches.len() - (count - current_index)
}
}
Direction::Next => (current_index + count) % matches.len(),
}
}
fn find_matches(
&mut self,
query: Arc<SearchQuery>,
cx: &mut ViewContext<Self>,
) -> Task<Vec<Self::Match>>;
fn active_match_index(
&mut self,
matches: Vec<Self::Match>,
cx: &mut ViewContext<Self>,
) -> Option<usize>;
}
pub trait SearchableItemHandle: ItemHandle {
fn downgrade(&self) -> Box<dyn WeakSearchableItemHandle>;
fn boxed_clone(&self) -> Box<dyn SearchableItemHandle>;
fn supported_options(&self) -> SearchOptions;
fn subscribe_to_search_events(
&self,
cx: &mut WindowContext,
handler: Box<dyn Fn(SearchEvent, &mut WindowContext)>,
) -> Subscription;
fn clear_matches(&self, cx: &mut WindowContext);
fn update_matches(&self, matches: &Vec<Box<dyn Any + Send>>, cx: &mut WindowContext);
fn query_suggestion(&self, cx: &mut WindowContext) -> String;
fn activate_match(
&self,
index: usize,
matches: &Vec<Box<dyn Any + Send>>,
cx: &mut WindowContext,
);
fn select_matches(&self, matches: &Vec<Box<dyn Any + Send>>, cx: &mut WindowContext);
fn replace(&self, _: &Box<dyn Any + Send>, _: &SearchQuery, _: &mut WindowContext);
fn match_index_for_direction(
&self,
matches: &Vec<Box<dyn Any + Send>>,
current_index: usize,
direction: Direction,
count: usize,
cx: &mut WindowContext,
) -> usize;
fn find_matches(
&self,
query: Arc<SearchQuery>,
cx: &mut WindowContext,
) -> Task<Vec<Box<dyn Any + Send>>>;
fn active_match_index(
&self,
matches: &Vec<Box<dyn Any + Send>>,
cx: &mut WindowContext,
) -> Option<usize>;
}
impl<T: SearchableItem> SearchableItemHandle for ViewHandle<T> {
fn downgrade(&self) -> Box<dyn WeakSearchableItemHandle> {
Box::new(self.downgrade())
}
fn boxed_clone(&self) -> Box<dyn SearchableItemHandle> {
Box::new(self.clone())
}
fn supported_options(&self) -> SearchOptions {
T::supported_options()
}
fn subscribe_to_search_events(
&self,
cx: &mut WindowContext,
handler: Box<dyn Fn(SearchEvent, &mut WindowContext)>,
) -> Subscription {
cx.subscribe(self, move |handle, event, cx| {
let search_event = handle.update(cx, |handle, cx| handle.to_search_event(event, cx));
if let Some(search_event) = search_event {
handler(search_event, cx)
}
})
}
fn clear_matches(&self, cx: &mut WindowContext) {
self.update(cx, |this, cx| this.clear_matches(cx));
}
fn update_matches(&self, matches: &Vec<Box<dyn Any + Send>>, cx: &mut WindowContext) {
let matches = downcast_matches(matches);
self.update(cx, |this, cx| this.update_matches(matches, cx));
}
fn query_suggestion(&self, cx: &mut WindowContext) -> String {
self.update(cx, |this, cx| this.query_suggestion(cx))
}
fn activate_match(
&self,
index: usize,
matches: &Vec<Box<dyn Any + Send>>,
cx: &mut WindowContext,
) {
let matches = downcast_matches(matches);
self.update(cx, |this, cx| this.activate_match(index, matches, cx));
}
fn select_matches(&self, matches: &Vec<Box<dyn Any + Send>>, cx: &mut WindowContext) {
let matches = downcast_matches(matches);
self.update(cx, |this, cx| this.select_matches(matches, cx));
}
fn match_index_for_direction(
&self,
matches: &Vec<Box<dyn Any + Send>>,
current_index: usize,
direction: Direction,
count: usize,
cx: &mut WindowContext,
) -> usize {
let matches = downcast_matches(matches);
self.update(cx, |this, cx| {
this.match_index_for_direction(&matches, current_index, direction, count, cx)
})
}
fn find_matches(
&self,
query: Arc<SearchQuery>,
cx: &mut WindowContext,
) -> Task<Vec<Box<dyn Any + Send>>> {
let matches = self.update(cx, |this, cx| this.find_matches(query, cx));
cx.foreground().spawn(async {
let matches = matches.await;
matches
.into_iter()
.map::<Box<dyn Any + Send>, _>(|range| Box::new(range))
.collect()
})
}
fn active_match_index(
&self,
matches: &Vec<Box<dyn Any + Send>>,
cx: &mut WindowContext,
) -> Option<usize> {
let matches = downcast_matches(matches);
self.update(cx, |this, cx| this.active_match_index(matches, cx))
}
fn replace(&self, matches: &Box<dyn Any + Send>, query: &SearchQuery, cx: &mut WindowContext) {
let matches = matches.downcast_ref().unwrap();
self.update(cx, |this, cx| this.replace(matches, query, cx))
}
}
fn downcast_matches<T: Any + Clone>(matches: &Vec<Box<dyn Any + Send>>) -> Vec<T> {
matches
.iter()
.map(|range| range.downcast_ref::<T>().cloned())
.collect::<Option<Vec<_>>>()
.expect(
"SearchableItemHandle function called with vec of matches of a different type than expected",
)
}
impl From<Box<dyn SearchableItemHandle>> for AnyViewHandle {
fn from(this: Box<dyn SearchableItemHandle>) -> Self {
this.as_any().clone()
}
}
impl From<&Box<dyn SearchableItemHandle>> for AnyViewHandle {
fn from(this: &Box<dyn SearchableItemHandle>) -> Self {
this.as_any().clone()
}
}
impl PartialEq for Box<dyn SearchableItemHandle> {
fn eq(&self, other: &Self) -> bool {
self.id() == other.id() && self.window() == other.window()
}
}
impl Eq for Box<dyn SearchableItemHandle> {}
pub trait WeakSearchableItemHandle: WeakItemHandle {
fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn SearchableItemHandle>>;
fn into_any(self) -> AnyWeakViewHandle;
}
impl<T: SearchableItem> WeakSearchableItemHandle for WeakViewHandle<T> {
fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn SearchableItemHandle>> {
Some(Box::new(self.upgrade(cx)?))
}
fn into_any(self) -> AnyWeakViewHandle {
self.into_any()
}
}
impl PartialEq for Box<dyn WeakSearchableItemHandle> {
fn eq(&self, other: &Self) -> bool {
self.id() == other.id() && self.window() == other.window()
}
}
impl Eq for Box<dyn WeakSearchableItemHandle> {}
impl std::hash::Hash for Box<dyn WeakSearchableItemHandle> {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
(self.id(), self.window().id()).hash(state)
}
}

View File

@ -0,0 +1,151 @@
use crate::{
item::{Item, ItemEvent},
ItemNavHistory, WorkspaceId,
};
use anyhow::Result;
use call::participant::{Frame, RemoteVideoTrack};
use client::{proto::PeerId, User};
use futures::StreamExt;
use gpui::{
elements::*,
geometry::{rect::RectF, vector::vec2f},
platform::MouseButton,
AppContext, Entity, Task, View, ViewContext,
};
use smallvec::SmallVec;
use std::{
borrow::Cow,
sync::{Arc, Weak},
};
pub enum Event {
Close,
}
pub struct SharedScreen {
track: Weak<RemoteVideoTrack>,
frame: Option<Frame>,
pub peer_id: PeerId,
user: Arc<User>,
nav_history: Option<ItemNavHistory>,
_maintain_frame: Task<Result<()>>,
}
impl SharedScreen {
pub fn new(
track: &Arc<RemoteVideoTrack>,
peer_id: PeerId,
user: Arc<User>,
cx: &mut ViewContext<Self>,
) -> Self {
let mut frames = track.frames();
Self {
track: Arc::downgrade(track),
frame: None,
peer_id,
user,
nav_history: Default::default(),
_maintain_frame: cx.spawn(|this, mut cx| async move {
while let Some(frame) = frames.next().await {
this.update(&mut cx, |this, cx| {
this.frame = Some(frame);
cx.notify();
})?;
}
this.update(&mut cx, |_, cx| cx.emit(Event::Close))?;
Ok(())
}),
}
}
}
impl Entity for SharedScreen {
type Event = Event;
}
impl View for SharedScreen {
fn ui_name() -> &'static str {
"SharedScreen"
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
enum Focus {}
let frame = self.frame.clone();
MouseEventHandler::new::<Focus, _>(0, cx, |_, cx| {
Canvas::new(move |bounds, _, _, cx| {
if let Some(frame) = frame.clone() {
let size = constrain_size_preserving_aspect_ratio(
bounds.size(),
vec2f(frame.width() as f32, frame.height() as f32),
);
let origin = bounds.origin() + (bounds.size() / 2.) - size / 2.;
cx.scene().push_surface(gpui::platform::mac::Surface {
bounds: RectF::new(origin, size),
image_buffer: frame.image(),
});
}
})
.contained()
.with_style(theme::current(cx).shared_screen)
})
.on_down(MouseButton::Left, |_, _, cx| cx.focus_parent())
.into_any()
}
}
impl Item for SharedScreen {
fn tab_tooltip_text(&self, _: &AppContext) -> Option<Cow<str>> {
Some(format!("{}'s screen", self.user.github_login).into())
}
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
if let Some(nav_history) = self.nav_history.as_mut() {
nav_history.push::<()>(None, cx);
}
}
fn tab_content<V: 'static>(
&self,
_: Option<usize>,
style: &theme::Tab,
_: &AppContext,
) -> gpui::AnyElement<V> {
Flex::row()
.with_child(
Svg::new("icons/desktop.svg")
.with_color(style.label.text.color)
.constrained()
.with_width(style.type_icon_width)
.aligned()
.contained()
.with_margin_right(style.spacing),
)
.with_child(
Label::new(
format!("{}'s screen", self.user.github_login),
style.label.clone(),
)
.aligned(),
)
.into_any()
}
fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext<Self>) {
self.nav_history = Some(history);
}
fn clone_on_split(
&self,
_workspace_id: WorkspaceId,
cx: &mut ViewContext<Self>,
) -> Option<Self> {
let track = self.track.upgrade()?;
Some(Self::new(&track, self.peer_id, self.user.clone(), cx))
}
fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
match event {
Event::Close => smallvec::smallvec!(ItemEvent::CloseItem),
}
}
}

View File

@ -0,0 +1,271 @@
use std::ops::Range;
use crate::{ItemHandle, Pane};
use gpui::{
elements::*,
geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
},
json::{json, ToJson},
AnyElement, AnyViewHandle, Entity, SizeConstraint, Subscription, View, ViewContext, ViewHandle,
WindowContext,
};
pub trait StatusItemView: View {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn crate::ItemHandle>,
cx: &mut ViewContext<Self>,
);
}
trait StatusItemViewHandle {
fn as_any(&self) -> &AnyViewHandle;
fn set_active_pane_item(
&self,
active_pane_item: Option<&dyn ItemHandle>,
cx: &mut WindowContext,
);
fn ui_name(&self) -> &'static str;
}
pub struct StatusBar {
left_items: Vec<Box<dyn StatusItemViewHandle>>,
right_items: Vec<Box<dyn StatusItemViewHandle>>,
active_pane: ViewHandle<Pane>,
_observe_active_pane: Subscription,
}
impl Entity for StatusBar {
type Event = ();
}
impl View for StatusBar {
fn ui_name() -> &'static str {
"StatusBar"
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let theme = &theme::current(cx).workspace.status_bar;
StatusBarElement {
left: Flex::row()
.with_children(self.left_items.iter().map(|i| {
ChildView::new(i.as_any(), cx)
.aligned()
.contained()
.with_margin_right(theme.item_spacing)
}))
.into_any(),
right: Flex::row()
.with_children(self.right_items.iter().rev().map(|i| {
ChildView::new(i.as_any(), cx)
.aligned()
.contained()
.with_margin_left(theme.item_spacing)
}))
.into_any(),
}
.contained()
.with_style(theme.container)
.constrained()
.with_height(theme.height)
.into_any()
}
}
impl StatusBar {
pub fn new(active_pane: &ViewHandle<Pane>, cx: &mut ViewContext<Self>) -> Self {
let mut this = Self {
left_items: Default::default(),
right_items: Default::default(),
active_pane: active_pane.clone(),
_observe_active_pane: cx
.observe(active_pane, |this, _, cx| this.update_active_pane_item(cx)),
};
this.update_active_pane_item(cx);
this
}
pub fn add_left_item<T>(&mut self, item: ViewHandle<T>, cx: &mut ViewContext<Self>)
where
T: 'static + StatusItemView,
{
self.left_items.push(Box::new(item));
cx.notify();
}
pub fn item_of_type<T: StatusItemView>(&self) -> Option<ViewHandle<T>> {
self.left_items
.iter()
.chain(self.right_items.iter())
.find_map(|item| item.as_any().clone().downcast())
}
pub fn position_of_item<T>(&self) -> Option<usize>
where
T: StatusItemView,
{
for (index, item) in self.left_items.iter().enumerate() {
if item.as_ref().ui_name() == T::ui_name() {
return Some(index);
}
}
for (index, item) in self.right_items.iter().enumerate() {
if item.as_ref().ui_name() == T::ui_name() {
return Some(index + self.left_items.len());
}
}
return None;
}
pub fn insert_item_after<T>(
&mut self,
position: usize,
item: ViewHandle<T>,
cx: &mut ViewContext<Self>,
) where
T: 'static + StatusItemView,
{
if position < self.left_items.len() {
self.left_items.insert(position + 1, Box::new(item))
} else {
self.right_items
.insert(position + 1 - self.left_items.len(), Box::new(item))
}
cx.notify()
}
pub fn remove_item_at(&mut self, position: usize, cx: &mut ViewContext<Self>) {
if position < self.left_items.len() {
self.left_items.remove(position);
} else {
self.right_items.remove(position - self.left_items.len());
}
cx.notify();
}
pub fn add_right_item<T>(&mut self, item: ViewHandle<T>, cx: &mut ViewContext<Self>)
where
T: 'static + StatusItemView,
{
self.right_items.push(Box::new(item));
cx.notify();
}
pub fn set_active_pane(&mut self, active_pane: &ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
self.active_pane = active_pane.clone();
self._observe_active_pane =
cx.observe(active_pane, |this, _, cx| this.update_active_pane_item(cx));
self.update_active_pane_item(cx);
}
fn update_active_pane_item(&mut self, cx: &mut ViewContext<Self>) {
let active_pane_item = self.active_pane.read(cx).active_item();
for item in self.left_items.iter().chain(&self.right_items) {
item.set_active_pane_item(active_pane_item.as_deref(), cx);
}
}
}
impl<T: StatusItemView> StatusItemViewHandle for ViewHandle<T> {
fn as_any(&self) -> &AnyViewHandle {
self
}
fn set_active_pane_item(
&self,
active_pane_item: Option<&dyn ItemHandle>,
cx: &mut WindowContext,
) {
self.update(cx, |this, cx| {
this.set_active_pane_item(active_pane_item, cx)
});
}
fn ui_name(&self) -> &'static str {
T::ui_name()
}
}
impl From<&dyn StatusItemViewHandle> for AnyViewHandle {
fn from(val: &dyn StatusItemViewHandle) -> Self {
val.as_any().clone()
}
}
struct StatusBarElement {
left: AnyElement<StatusBar>,
right: AnyElement<StatusBar>,
}
impl Element<StatusBar> for StatusBarElement {
type LayoutState = ();
type PaintState = ();
fn layout(
&mut self,
mut constraint: SizeConstraint,
view: &mut StatusBar,
cx: &mut ViewContext<StatusBar>,
) -> (Vector2F, Self::LayoutState) {
let max_width = constraint.max.x();
constraint.min = vec2f(0., constraint.min.y());
let right_size = self.right.layout(constraint, view, cx);
let constraint = SizeConstraint::new(
vec2f(0., constraint.min.y()),
vec2f(max_width - right_size.x(), constraint.max.y()),
);
self.left.layout(constraint, view, cx);
(vec2f(max_width, right_size.y()), ())
}
fn paint(
&mut self,
bounds: RectF,
visible_bounds: RectF,
_: &mut Self::LayoutState,
view: &mut StatusBar,
cx: &mut ViewContext<StatusBar>,
) -> Self::PaintState {
let origin_y = bounds.upper_right().y();
let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
let left_origin = vec2f(bounds.lower_left().x(), origin_y);
self.left.paint(left_origin, visible_bounds, view, cx);
let right_origin = vec2f(bounds.upper_right().x() - self.right.size().x(), origin_y);
self.right.paint(right_origin, visible_bounds, view, cx);
}
fn rect_for_text_range(
&self,
_: Range<usize>,
_: RectF,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
_: &StatusBar,
_: &ViewContext<StatusBar>,
) -> Option<RectF> {
None
}
fn debug(
&self,
bounds: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
_: &StatusBar,
_: &ViewContext<StatusBar>,
) -> serde_json::Value {
json!({
"type": "StatusBarElement",
"bounds": bounds.to_json()
})
}
}

View File

@ -0,0 +1,301 @@
use crate::ItemHandle;
use gpui::{
elements::*, AnyElement, AnyViewHandle, AppContext, Entity, View, ViewContext, ViewHandle,
WindowContext,
};
pub trait ToolbarItemView: View {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn crate::ItemHandle>,
cx: &mut ViewContext<Self>,
) -> ToolbarItemLocation;
fn location_for_event(
&self,
_event: &Self::Event,
current_location: ToolbarItemLocation,
_cx: &AppContext,
) -> ToolbarItemLocation {
current_location
}
fn pane_focus_update(&mut self, _pane_focused: bool, _cx: &mut ViewContext<Self>) {}
/// Number of times toolbar's height will be repeated to get the effective height.
/// Useful when multiple rows one under each other are needed.
/// The rows have the same width and act as a whole when reacting to resizes and similar events.
fn row_count(&self, _cx: &ViewContext<Self>) -> usize {
1
}
}
trait ToolbarItemViewHandle {
fn id(&self) -> usize;
fn as_any(&self) -> &AnyViewHandle;
fn set_active_pane_item(
&self,
active_pane_item: Option<&dyn ItemHandle>,
cx: &mut WindowContext,
) -> ToolbarItemLocation;
fn focus_changed(&mut self, pane_focused: bool, cx: &mut WindowContext);
fn row_count(&self, cx: &WindowContext) -> usize;
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum ToolbarItemLocation {
Hidden,
PrimaryLeft { flex: Option<(f32, bool)> },
PrimaryRight { flex: Option<(f32, bool)> },
Secondary,
}
pub struct Toolbar {
active_item: Option<Box<dyn ItemHandle>>,
hidden: bool,
can_navigate: bool,
items: Vec<(Box<dyn ToolbarItemViewHandle>, ToolbarItemLocation)>,
}
impl Entity for Toolbar {
type Event = ();
}
impl View for Toolbar {
fn ui_name() -> &'static str {
"Toolbar"
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let theme = &theme::current(cx).workspace.toolbar;
let mut primary_left_items = Vec::new();
let mut primary_right_items = Vec::new();
let mut secondary_item = None;
let spacing = theme.item_spacing;
let mut primary_items_row_count = 1;
for (item, position) in &self.items {
match *position {
ToolbarItemLocation::Hidden => {}
ToolbarItemLocation::PrimaryLeft { flex } => {
primary_items_row_count = primary_items_row_count.max(item.row_count(cx));
let left_item = ChildView::new(item.as_any(), cx).aligned();
if let Some((flex, expanded)) = flex {
primary_left_items.push(left_item.flex(flex, expanded).into_any());
} else {
primary_left_items.push(left_item.into_any());
}
}
ToolbarItemLocation::PrimaryRight { flex } => {
primary_items_row_count = primary_items_row_count.max(item.row_count(cx));
let right_item = ChildView::new(item.as_any(), cx).aligned().flex_float();
if let Some((flex, expanded)) = flex {
primary_right_items.push(right_item.flex(flex, expanded).into_any());
} else {
primary_right_items.push(right_item.into_any());
}
}
ToolbarItemLocation::Secondary => {
secondary_item = Some(
ChildView::new(item.as_any(), cx)
.constrained()
.with_height(theme.height * item.row_count(cx) as f32)
.into_any(),
);
}
}
}
let container_style = theme.container;
let height = theme.height * primary_items_row_count as f32;
let mut primary_items = Flex::row().with_spacing(spacing);
primary_items.extend(primary_left_items);
primary_items.extend(primary_right_items);
let mut toolbar = Flex::column();
if !primary_items.is_empty() {
toolbar.add_child(primary_items.constrained().with_height(height));
}
if let Some(secondary_item) = secondary_item {
toolbar.add_child(secondary_item);
}
if toolbar.is_empty() {
toolbar.into_any_named("toolbar")
} else {
toolbar
.contained()
.with_style(container_style)
.into_any_named("toolbar")
}
}
}
// <<<<<<< HEAD
// =======
// #[allow(clippy::too_many_arguments)]
// fn nav_button<A: Action, F: 'static + Fn(&mut Toolbar, &mut ViewContext<Toolbar>)>(
// svg_path: &'static str,
// style: theme::Interactive<theme::IconButton>,
// nav_button_height: f32,
// tooltip_style: TooltipStyle,
// enabled: bool,
// spacing: f32,
// on_click: F,
// tooltip_action: A,
// action_name: &'static str,
// cx: &mut ViewContext<Toolbar>,
// ) -> AnyElement<Toolbar> {
// MouseEventHandler::new::<A, _>(0, cx, |state, _| {
// let style = if enabled {
// style.style_for(state)
// } else {
// style.disabled_style()
// };
// Svg::new(svg_path)
// .with_color(style.color)
// .constrained()
// .with_width(style.icon_width)
// .aligned()
// .contained()
// .with_style(style.container)
// .constrained()
// .with_width(style.button_width)
// .with_height(nav_button_height)
// .aligned()
// .top()
// })
// .with_cursor_style(if enabled {
// CursorStyle::PointingHand
// } else {
// CursorStyle::default()
// })
// .on_click(MouseButton::Left, move |_, toolbar, cx| {
// on_click(toolbar, cx)
// })
// .with_tooltip::<A>(
// 0,
// action_name,
// Some(Box::new(tooltip_action)),
// tooltip_style,
// cx,
// )
// .contained()
// .with_margin_right(spacing)
// .into_any_named("nav button")
// }
// >>>>>>> 139cbbfd3aebd0863a7d51b0c12d748764cf0b2e
impl Toolbar {
pub fn new() -> Self {
Self {
active_item: None,
items: Default::default(),
hidden: false,
can_navigate: true,
}
}
pub fn set_can_navigate(&mut self, can_navigate: bool, cx: &mut ViewContext<Self>) {
self.can_navigate = can_navigate;
cx.notify();
}
pub fn add_item<T>(&mut self, item: ViewHandle<T>, cx: &mut ViewContext<Self>)
where
T: 'static + ToolbarItemView,
{
let location = item.set_active_pane_item(self.active_item.as_deref(), cx);
cx.subscribe(&item, |this, item, event, cx| {
if let Some((_, current_location)) =
this.items.iter_mut().find(|(i, _)| i.id() == item.id())
{
let new_location = item
.read(cx)
.location_for_event(event, *current_location, cx);
if new_location != *current_location {
*current_location = new_location;
cx.notify();
}
}
})
.detach();
self.items.push((Box::new(item), location));
cx.notify();
}
pub fn set_active_item(&mut self, item: Option<&dyn ItemHandle>, cx: &mut ViewContext<Self>) {
self.active_item = item.map(|item| item.boxed_clone());
self.hidden = self
.active_item
.as_ref()
.map(|item| !item.show_toolbar(cx))
.unwrap_or(false);
for (toolbar_item, current_location) in self.items.iter_mut() {
let new_location = toolbar_item.set_active_pane_item(item, cx);
if new_location != *current_location {
*current_location = new_location;
cx.notify();
}
}
}
pub fn focus_changed(&mut self, focused: bool, cx: &mut ViewContext<Self>) {
for (toolbar_item, _) in self.items.iter_mut() {
toolbar_item.focus_changed(focused, cx);
}
}
pub fn item_of_type<T: ToolbarItemView>(&self) -> Option<ViewHandle<T>> {
self.items
.iter()
.find_map(|(item, _)| item.as_any().clone().downcast())
}
pub fn hidden(&self) -> bool {
self.hidden
}
}
impl<T: ToolbarItemView> ToolbarItemViewHandle for ViewHandle<T> {
fn id(&self) -> usize {
self.id()
}
fn as_any(&self) -> &AnyViewHandle {
self
}
fn set_active_pane_item(
&self,
active_pane_item: Option<&dyn ItemHandle>,
cx: &mut WindowContext,
) -> ToolbarItemLocation {
self.update(cx, |this, cx| {
this.set_active_pane_item(active_pane_item, cx)
})
}
fn focus_changed(&mut self, pane_focused: bool, cx: &mut WindowContext) {
self.update(cx, |this, cx| {
this.pane_focus_update(pane_focused, cx);
cx.notify();
});
}
fn row_count(&self, cx: &WindowContext) -> usize {
self.read_with(cx, |this, cx| this.row_count(cx))
}
}
impl From<&dyn ToolbarItemViewHandle> for AnyViewHandle {
fn from(val: &dyn ToolbarItemViewHandle) -> Self {
val.as_any().clone()
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,56 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Setting;
#[derive(Deserialize)]
pub struct WorkspaceSettings {
pub active_pane_magnification: f32,
pub confirm_quit: bool,
pub show_call_status_icon: bool,
pub autosave: AutosaveSetting,
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct WorkspaceSettingsContent {
pub active_pane_magnification: Option<f32>,
pub confirm_quit: Option<bool>,
pub show_call_status_icon: Option<bool>,
pub autosave: Option<AutosaveSetting>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum AutosaveSetting {
Off,
AfterDelay { milliseconds: u64 },
OnFocusChange,
OnWindowChange,
}
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
pub struct GitSettings {
pub git_gutter: Option<GitGutterSetting>,
pub gutter_debounce: Option<u64>,
}
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum GitGutterSetting {
#[default]
TrackedFiles,
Hide,
}
impl Setting for WorkspaceSettings {
const KEY: Option<&'static str> = None;
type FileContent = WorkspaceSettingsContent;
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
_: &gpui::AppContext,
) -> anyhow::Result<Self> {
Self::load_via_json_merge(default_value, user_values)
}
}

View File

@ -69,7 +69,7 @@ theme2 = { path = "../theme2" }
util = { path = "../util" }
# semantic_index = { path = "../semantic_index" }
# vim = { path = "../vim" }
# workspace = { path = "../workspace" }
workspace2 = { path = "../workspace2" }
# welcome = { path = "../welcome" }
# zed-actions = {path = "../zed-actions"}
anyhow.workspace = true

View File

@ -4,7 +4,8 @@ mod open_listener;
pub use assets::*;
use client2::{Client, UserStore};
use gpui2::{AsyncAppContext, Handle};
use collections::HashMap;
use gpui2::{AsyncAppContext, Handle, Point};
pub use only_instance::*;
pub use open_listener::*;
@ -13,8 +14,12 @@ use cli::{
ipc::{self, IpcSender},
CliRequest, CliResponse, IpcHandshake,
};
use futures::{channel::mpsc, SinkExt, StreamExt};
use std::{sync::Arc, thread};
use futures::{
channel::{mpsc, oneshot},
FutureExt, SinkExt, StreamExt,
};
use std::{path::Path, sync::Arc, thread, time::Duration};
use util::{paths::PathLikeWithPosition, ResultExt};
pub fn connect_to_cli(
server_name: &str,
@ -51,156 +56,157 @@ pub struct AppState {
}
pub async fn handle_cli_connection(
(mut requests, _responses): (mpsc::Receiver<CliRequest>, IpcSender<CliResponse>),
_app_state: Arc<AppState>,
mut _cx: AsyncAppContext,
(mut requests, responses): (mpsc::Receiver<CliRequest>, IpcSender<CliResponse>),
app_state: Arc<AppState>,
mut cx: AsyncAppContext,
) {
if let Some(request) = requests.next().await {
match request {
CliRequest::Open { paths: _, wait: _ } => {
// let mut caret_positions = HashMap::new();
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::<Editor>() {
// active_editor
// .downgrade()
// .update(&mut cx, |editor, cx| {
// let snapshot =
// editor.snapshot(cx).display_snapshot;
// let point = snapshot
// .buffer_snapshot
// .clip_point(point, Bias::Left);
// editor.change_selections(
// Some(Autoscroll::center()),
// cx,
// |s| s.select_ranges([point..point]),
// );
// })
// .log_err();
// }
// }
match cx
.update(|cx| workspace2::open_paths(&paths, &app_state, None, cx))
.await
{
Ok((workspace, items)) => {
let mut item_release_futures = Vec::new();
// let released = oneshot::channel();
// cx.update(|cx| {
// item.on_release(
// cx,
// Box::new(move |_| {
// let _ = released.0.send(());
// }),
// )
// .detach();
// });
// item_release_futures.push(released.1);
// }
// Some(Err(err)) => {
// responses
// .send(CliResponse::Stderr {
// message: format!("error opening {:?}: {}", path, err),
// })
// .log_err();
// errored = true;
// }
// None => {}
// }
// }
for (item, path) in items.into_iter().zip(&paths) {
match item {
Some(Ok(item)) => {
if let Some(point) = caret_positions.remove(path) {
todo!()
// if let Some(active_editor) = item.downcast::<Editor>() {
// active_editor
// .downgrade()
// .update(&mut cx, |editor, cx| {
// let snapshot =
// editor.snapshot(cx).display_snapshot;
// let point = snapshot
// .buffer_snapshot
// .clip_point(point, Bias::Left);
// editor.change_selections(
// Some(Autoscroll::center()),
// cx,
// |s| s.select_ranges([point..point]),
// );
// })
// .log_err();
// }
}
// if wait {
// let background = cx.background();
// let wait = async move {
// if paths.is_empty() {
// let (done_tx, done_rx) = oneshot::channel();
// if let Some(workspace) = workspace.upgrade(&cx) {
// let _subscription = cx.update(|cx| {
// cx.observe_release(&workspace, move |_, _| {
// let _ = done_tx.send(());
// })
// });
// drop(workspace);
// let _ = done_rx.await;
// }
// } else {
// let _ =
// futures::future::try_join_all(item_release_futures).await;
// };
// }
// .fuse();
// futures::pin_mut!(wait);
let released = oneshot::channel();
cx.update(|cx| {
item.on_release(
cx,
Box::new(move |_| {
let _ = released.0.send(());
}),
)
.detach();
});
item_release_futures.push(released.1);
}
Some(Err(err)) => {
responses
.send(CliResponse::Stderr {
message: format!("error opening {:?}: {}", path, err),
})
.log_err();
errored = true;
}
None => {}
}
}
// loop {
// // Repeatedly check if CLI is still open to avoid wasting resources
// // waiting for files or workspaces to close.
// let mut timer = background.timer(Duration::from_secs(1)).fuse();
// futures::select_biased! {
// _ = wait => break,
// _ = timer => {
// if responses.send(CliResponse::Ping).is_err() {
// break;
// }
// }
// }
// }
// }
// }
// Err(error) => {
// errored = true;
// responses
// .send(CliResponse::Stderr {
// message: format!("error opening {:?}: {}", paths, error),
// })
// .log_err();
// }
// }
if wait {
let executor = cx.executor();
let wait = async move {
if paths.is_empty() {
let (done_tx, done_rx) = oneshot::channel();
if let Some(workspace) = workspace.upgrade(&cx) {
let _subscription = cx.update(|cx| {
cx.observe_release(&workspace, move |_, _| {
let _ = done_tx.send(());
})
});
drop(workspace);
let _ = done_rx.await;
}
} else {
let _ =
futures::future::try_join_all(item_release_futures).await;
};
}
.fuse();
futures::pin_mut!(wait);
// responses
// .send(CliResponse::Exit {
// status: i32::from(errored),
// })
// .log_err();
loop {
// Repeatedly check if CLI is still open to avoid wasting resources
// waiting for files or workspaces to close.
let mut timer = executor.timer(Duration::from_secs(1)).fuse();
futures::select_biased! {
_ = wait => break,
_ = timer => {
if responses.send(CliResponse::Ping).is_err() {
break;
}
}
}
}
}
}
Err(error) => {
errored = true;
responses
.send(CliResponse::Stderr {
message: format!("error opening {:?}: {}", paths, error),
})
.log_err();
}
}
responses
.send(CliResponse::Exit {
status: i32::from(errored),
})
.log_err();
}
}
}