zed/crates/workspace/src/workspace.rs
2023-07-20 11:11:47 -06:00

4945 lines
178 KiB
Rust
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

pub mod dock;
pub mod item;
pub mod notifications;
pub mod pane;
pub mod pane_group;
mod persistence;
pub mod searchable;
pub mod shared_screen;
mod status_bar;
mod toolbar;
mod workspace_settings;
use anyhow::{anyhow, Context, Result};
use call::ActiveCall;
use client::{
proto::{self, PeerId},
Client, TypedEnvelope, UserStore,
};
use collections::{hash_map, HashMap, HashSet};
use drag_and_drop::DragAndDrop;
use futures::{
channel::{mpsc, oneshot},
future::try_join_all,
FutureExt, StreamExt,
};
use gpui::{
actions,
elements::*,
geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
},
impl_actions,
platform::{
CursorStyle, MouseButton, PathPromptOptions, Platform, PromptLevel, WindowBounds,
WindowOptions,
},
AnyModelHandle, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity,
ModelContext, ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle,
WeakViewHandle, WindowContext,
};
use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem};
use itertools::Itertools;
use language::{LanguageRegistry, Rope};
use std::{
any::TypeId,
borrow::Cow,
cmp, env,
future::Future,
path::{Path, PathBuf},
rc::Rc,
str,
sync::{atomic::AtomicUsize, Arc},
time::Duration,
};
use crate::{
notifications::{simple_message_notification::MessageNotification, NotificationTracker},
persistence::model::{
DockData, DockStructure, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
},
};
use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle};
use lazy_static::lazy_static;
use notifications::{NotificationHandle, NotifyResultExt};
pub use pane::*;
pub use pane_group::*;
use persistence::{model::SerializedItem, DB};
pub use persistence::{
model::{ItemId, WorkspaceLocation},
WorkspaceDb, DB as WORKSPACE_DB,
};
use postage::prelude::Stream;
use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
use serde::Deserialize;
use shared_screen::SharedScreen;
use status_bar::StatusBar;
pub use status_bar::StatusItemView;
use theme::{Theme, ThemeSettings};
pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
use util::{async_iife, ResultExt};
pub use workspace_settings::{AutosaveSetting, GitGutterSetting, WorkspaceSettings};
lazy_static! {
static ref ZED_WINDOW_SIZE: Option<Vector2F> = env::var("ZED_WINDOW_SIZE")
.ok()
.as_deref()
.and_then(parse_pixel_position_env_var);
static ref ZED_WINDOW_POSITION: Option<Vector2F> = env::var("ZED_WINDOW_POSITION")
.ok()
.as_deref()
.and_then(parse_pixel_position_env_var);
}
pub trait Modal: View {
fn has_focus(&self) -> bool;
fn dismiss_on_event(event: &Self::Event) -> bool;
}
trait ModalHandle {
fn as_any(&self) -> &AnyViewHandle;
fn has_focus(&self, cx: &WindowContext) -> bool;
}
impl<T: Modal> ModalHandle for ViewHandle<T> {
fn as_any(&self) -> &AnyViewHandle {
self
}
fn has_focus(&self, cx: &WindowContext) -> bool {
self.read(cx).has_focus()
}
}
#[derive(Clone, PartialEq)]
pub struct RemoveWorktreeFromProject(pub WorktreeId);
actions!(
workspace,
[
Open,
NewFile,
NewWindow,
CloseWindow,
AddFolderToProject,
Unfollow,
Save,
SaveAs,
SaveAll,
ActivatePreviousPane,
ActivateNextPane,
FollowNextCollaborator,
NewTerminal,
NewCenterTerminal,
ToggleTerminalFocus,
NewSearch,
Feedback,
Restart,
Welcome,
ToggleZoom,
ToggleLeftDock,
ToggleRightDock,
ToggleBottomDock,
]
);
#[derive(Clone, PartialEq)]
pub struct OpenPaths {
pub paths: Vec<PathBuf>,
}
#[derive(Clone, Deserialize, PartialEq)]
pub struct ActivatePane(pub usize);
#[derive(Clone, Deserialize, PartialEq)]
pub struct ActivatePaneInDirection(pub SplitDirection);
#[derive(Deserialize)]
pub struct Toast {
id: usize,
msg: Cow<'static, str>,
#[serde(skip)]
on_click: Option<(Cow<'static, str>, Arc<dyn Fn(&mut WindowContext)>)>,
}
impl Toast {
pub fn new<I: Into<Cow<'static, str>>>(id: usize, msg: I) -> Self {
Toast {
id,
msg: msg.into(),
on_click: None,
}
}
pub fn on_click<F, M>(mut self, message: M, on_click: F) -> Self
where
M: Into<Cow<'static, str>>,
F: Fn(&mut WindowContext) + 'static,
{
self.on_click = Some((message.into(), Arc::new(on_click)));
self
}
}
impl PartialEq for Toast {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
&& self.msg == other.msg
&& self.on_click.is_some() == other.on_click.is_some()
}
}
impl Clone for Toast {
fn clone(&self) -> Self {
Toast {
id: self.id,
msg: self.msg.to_owned(),
on_click: self.on_click.clone(),
}
}
}
impl_actions!(workspace, [ActivatePane, ActivatePaneInDirection, Toast]);
pub type WorkspaceId = i64;
pub fn init_settings(cx: &mut AppContext) {
settings::register::<WorkspaceSettings>(cx);
settings::register::<item::ItemSettings>(cx);
}
pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
init_settings(cx);
pane::init(cx);
notifications::init(cx);
cx.add_global_action({
let app_state = Arc::downgrade(&app_state);
move |_: &Open, cx: &mut AppContext| {
let mut paths = cx.prompt_for_paths(PathPromptOptions {
files: true,
directories: true,
multiple: true,
});
if let Some(app_state) = app_state.upgrade() {
cx.spawn(move |mut cx| async move {
if let Some(paths) = paths.recv().await.flatten() {
cx.update(|cx| {
open_paths(&paths, &app_state, None, cx).detach_and_log_err(cx)
});
}
})
.detach();
}
}
});
cx.add_async_action(Workspace::open);
cx.add_async_action(Workspace::follow_next_collaborator);
cx.add_async_action(Workspace::close);
cx.add_global_action(Workspace::close_global);
cx.add_global_action(restart);
cx.add_async_action(Workspace::save_all);
cx.add_action(Workspace::add_folder_to_project);
cx.add_action(
|workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext<Workspace>| {
let pane = workspace.active_pane().clone();
workspace.unfollow(&pane, cx);
},
);
cx.add_action(
|workspace: &mut Workspace, _: &Save, cx: &mut ViewContext<Workspace>| {
workspace.save_active_item(false, cx).detach_and_log_err(cx);
},
);
cx.add_action(
|workspace: &mut Workspace, _: &SaveAs, cx: &mut ViewContext<Workspace>| {
workspace.save_active_item(true, cx).detach_and_log_err(cx);
},
);
cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| {
workspace.activate_previous_pane(cx)
});
cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| {
workspace.activate_next_pane(cx)
});
cx.add_action(
|workspace: &mut Workspace, action: &ActivatePaneInDirection, cx| {
workspace.activate_pane_in_direction(action.0, cx)
},
);
cx.add_action(|workspace: &mut Workspace, _: &ToggleLeftDock, cx| {
workspace.toggle_dock(DockPosition::Left, cx);
});
cx.add_action(|workspace: &mut Workspace, _: &ToggleRightDock, cx| {
workspace.toggle_dock(DockPosition::Right, cx);
});
cx.add_action(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| {
workspace.toggle_dock(DockPosition::Bottom, cx);
});
cx.add_action(Workspace::activate_pane_at_index);
cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
workspace.reopen_closed_item(cx).detach();
});
cx.add_action(|workspace: &mut Workspace, _: &GoBack, cx| {
workspace
.go_back(workspace.active_pane().downgrade(), cx)
.detach();
});
cx.add_action(|workspace: &mut Workspace, _: &GoForward, cx| {
workspace
.go_forward(workspace.active_pane().downgrade(), cx)
.detach();
});
cx.add_action(|_: &mut Workspace, _: &install_cli::Install, cx| {
cx.spawn(|workspace, mut cx| async move {
let err = install_cli::install_cli(&cx)
.await
.context("Failed to create CLI symlink");
workspace.update(&mut cx, |workspace, cx| {
if matches!(err, Err(_)) {
err.notify_err(workspace, cx);
} else {
workspace.show_notification(1, cx, |cx| {
cx.add_view(|_| {
MessageNotification::new("Successfully installed the `zed` binary")
})
});
}
})
})
.detach();
});
let client = &app_state.client;
client.add_view_request_handler(Workspace::handle_follow);
client.add_view_message_handler(Workspace::handle_unfollow);
client.add_view_message_handler(Workspace::handle_update_followers);
}
type ProjectItemBuilders = HashMap<
TypeId,
fn(ModelHandle<Project>, AnyModelHandle, &mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
>;
pub fn register_project_item<I: ProjectItem>(cx: &mut AppContext) {
cx.update_default_global(|builders: &mut ProjectItemBuilders, _| {
builders.insert(TypeId::of::<I::Item>(), |project, model, cx| {
let item = model.downcast::<I::Item>().unwrap();
Box::new(cx.add_view(|cx| I::for_project_item(project, item, cx)))
});
});
}
type FollowableItemBuilder = fn(
ViewHandle<Pane>,
ModelHandle<Project>,
ViewId,
&mut Option<proto::view::Variant>,
&mut AppContext,
) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>;
type FollowableItemBuilders = HashMap<
TypeId,
(
FollowableItemBuilder,
fn(&AnyViewHandle) -> Box<dyn FollowableItemHandle>,
),
>;
pub fn register_followable_item<I: FollowableItem>(cx: &mut AppContext) {
cx.update_default_global(|builders: &mut FollowableItemBuilders, _| {
builders.insert(
TypeId::of::<I>(),
(
|pane, project, id, state, cx| {
I::from_state_proto(pane, project, id, state, cx).map(|task| {
cx.foreground()
.spawn(async move { Ok(Box::new(task.await?) as Box<_>) })
})
},
|this| Box::new(this.clone().downcast::<I>().unwrap()),
),
);
});
}
type ItemDeserializers = HashMap<
Arc<str>,
fn(
ModelHandle<Project>,
WeakViewHandle<Workspace>,
WorkspaceId,
ItemId,
&mut ViewContext<Pane>,
) -> Task<Result<Box<dyn ItemHandle>>>,
>;
pub fn register_deserializable_item<I: Item>(cx: &mut AppContext) {
cx.update_default_global(|deserializers: &mut ItemDeserializers, _cx| {
if let Some(serialized_item_kind) = I::serialized_item_kind() {
deserializers.insert(
Arc::from(serialized_item_kind),
|project, workspace, workspace_id, item_id, cx| {
let task = I::deserialize(project, workspace, workspace_id, item_id, cx);
cx.foreground()
.spawn(async { Ok(Box::new(task.await?) as Box<_>) })
},
);
}
});
}
pub struct AppState {
pub languages: Arc<LanguageRegistry>,
pub client: Arc<client::Client>,
pub user_store: ModelHandle<client::UserStore>,
pub fs: Arc<dyn fs::Fs>,
pub build_window_options:
fn(Option<WindowBounds>, Option<uuid::Uuid>, &dyn Platform) -> WindowOptions<'static>,
pub initialize_workspace:
fn(WeakViewHandle<Workspace>, bool, Arc<AppState>, AsyncAppContext) -> Task<Result<()>>,
pub background_actions: BackgroundActions,
}
impl AppState {
#[cfg(any(test, feature = "test-support"))]
pub fn test(cx: &mut AppContext) -> Arc<Self> {
use settings::SettingsStore;
if !cx.has_global::<SettingsStore>() {
cx.set_global(SettingsStore::test(cx));
}
let fs = fs::FakeFs::new(cx.background().clone());
let languages = Arc::new(LanguageRegistry::test());
let http_client = util::http::FakeHttpClient::with_404_response();
let client = Client::new(http_client.clone(), cx);
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
theme::init((), cx);
client::init(&client, cx);
crate::init_settings(cx);
Arc::new(Self {
client,
fs,
languages,
user_store,
initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
build_window_options: |_, _, _| Default::default(),
background_actions: || &[],
})
}
}
struct DelayedDebouncedEditAction {
task: Option<Task<()>>,
cancel_channel: Option<oneshot::Sender<()>>,
}
impl DelayedDebouncedEditAction {
fn new() -> DelayedDebouncedEditAction {
DelayedDebouncedEditAction {
task: None,
cancel_channel: None,
}
}
fn fire_new<F>(&mut self, delay: Duration, cx: &mut ViewContext<Workspace>, func: F)
where
F: 'static + FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> Task<Result<()>>,
{
if let Some(channel) = self.cancel_channel.take() {
_ = channel.send(());
}
let (sender, mut receiver) = oneshot::channel::<()>();
self.cancel_channel = Some(sender);
let previous_task = self.task.take();
self.task = Some(cx.spawn(|workspace, mut cx| async move {
let mut timer = cx.background().timer(delay).fuse();
if let Some(previous_task) = previous_task {
previous_task.await;
}
futures::select_biased! {
_ = receiver => return,
_ = timer => {}
}
if let Some(result) = workspace
.update(&mut cx, |workspace, cx| (func)(workspace, cx))
.log_err()
{
result.await.log_err();
}
}));
}
}
pub enum Event {
PaneAdded(ViewHandle<Pane>),
ContactRequestedJoin(u64),
}
pub struct Workspace {
weak_self: WeakViewHandle<Self>,
remote_entity_subscription: Option<client::Subscription>,
modal: Option<ActiveModal>,
zoomed: Option<AnyWeakViewHandle>,
zoomed_position: Option<DockPosition>,
center: PaneGroup,
left_dock: ViewHandle<Dock>,
bottom_dock: ViewHandle<Dock>,
right_dock: ViewHandle<Dock>,
panes: Vec<ViewHandle<Pane>>,
panes_by_item: HashMap<usize, WeakViewHandle<Pane>>,
active_pane: ViewHandle<Pane>,
last_active_center_pane: Option<WeakViewHandle<Pane>>,
status_bar: ViewHandle<StatusBar>,
titlebar_item: Option<AnyViewHandle>,
notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
project: ModelHandle<Project>,
leader_state: LeaderState,
follower_states_by_leader: FollowerStatesByLeader,
last_leaders_by_pane: HashMap<WeakViewHandle<Pane>, PeerId>,
window_edited: bool,
active_call: Option<(ModelHandle<ActiveCall>, Vec<gpui::Subscription>)>,
leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
database_id: WorkspaceId,
app_state: Arc<AppState>,
subscriptions: Vec<Subscription>,
_apply_leader_updates: Task<Result<()>>,
_observe_current_user: Task<Result<()>>,
_schedule_serialize: Option<Task<()>>,
pane_history_timestamp: Arc<AtomicUsize>,
}
struct ActiveModal {
view: Box<dyn ModalHandle>,
previously_focused_view_id: Option<usize>,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct ViewId {
pub creator: PeerId,
pub id: u64,
}
#[derive(Default)]
struct LeaderState {
followers: HashSet<PeerId>,
}
type FollowerStatesByLeader = HashMap<PeerId, HashMap<ViewHandle<Pane>, FollowerState>>;
#[derive(Default)]
struct FollowerState {
active_view_id: Option<ViewId>,
items_by_leader_view_id: HashMap<ViewId, Box<dyn FollowableItemHandle>>,
}
impl Workspace {
pub fn new(
workspace_id: WorkspaceId,
project: ModelHandle<Project>,
app_state: Arc<AppState>,
cx: &mut ViewContext<Self>,
) -> Self {
cx.observe(&project, |_, _, cx| cx.notify()).detach();
cx.subscribe(&project, move |this, _, event, cx| {
match event {
project::Event::RemoteIdChanged(remote_id) => {
this.update_window_title(cx);
this.project_remote_id_changed(*remote_id, cx);
}
project::Event::CollaboratorLeft(peer_id) => {
this.collaborator_left(*peer_id, cx);
}
project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded => {
this.update_window_title(cx);
this.serialize_workspace(cx);
}
project::Event::DisconnectedFromHost => {
this.update_window_edited(cx);
cx.blur();
}
project::Event::Closed => {
cx.remove_window();
}
project::Event::DeletedEntry(entry_id) => {
for pane in this.panes.iter() {
pane.update(cx, |pane, cx| {
pane.handle_deleted_project_item(*entry_id, cx)
});
}
}
project::Event::Notification(message) => this.show_notification(0, cx, |cx| {
cx.add_view(|_| MessageNotification::new(message.clone()))
}),
_ => {}
}
cx.notify()
})
.detach();
let weak_handle = cx.weak_handle();
let pane_history_timestamp = Arc::new(AtomicUsize::new(0));
let center_pane = cx.add_view(|cx| {
Pane::new(
weak_handle.clone(),
project.clone(),
app_state.background_actions,
pane_history_timestamp.clone(),
cx,
)
});
cx.subscribe(&center_pane, Self::handle_pane_event).detach();
cx.focus(&center_pane);
cx.emit(Event::PaneAdded(center_pane.clone()));
let mut current_user = app_state.user_store.read(cx).watch_current_user();
let mut connection_status = app_state.client.status();
let _observe_current_user = cx.spawn(|this, mut cx| async move {
current_user.recv().await;
connection_status.recv().await;
let mut stream =
Stream::map(current_user, drop).merge(Stream::map(connection_status, drop));
while stream.recv().await.is_some() {
this.update(&mut cx, |_, cx| cx.notify())?;
}
anyhow::Ok(())
});
// All leader updates are enqueued and then processed in a single task, so
// that each asynchronous operation can be run in order.
let (leader_updates_tx, mut leader_updates_rx) =
mpsc::unbounded::<(PeerId, proto::UpdateFollowers)>();
let _apply_leader_updates = cx.spawn(|this, mut cx| async move {
while let Some((leader_id, update)) = leader_updates_rx.next().await {
Self::process_leader_update(&this, leader_id, update, &mut cx)
.await
.log_err();
}
Ok(())
});
cx.emit_global(WorkspaceCreated(weak_handle.clone()));
let left_dock = cx.add_view(|_| Dock::new(DockPosition::Left));
let bottom_dock = cx.add_view(|_| Dock::new(DockPosition::Bottom));
let right_dock = cx.add_view(|_| Dock::new(DockPosition::Right));
let left_dock_buttons =
cx.add_view(|cx| PanelButtons::new(left_dock.clone(), weak_handle.clone(), cx));
let bottom_dock_buttons =
cx.add_view(|cx| PanelButtons::new(bottom_dock.clone(), weak_handle.clone(), cx));
let right_dock_buttons =
cx.add_view(|cx| PanelButtons::new(right_dock.clone(), weak_handle.clone(), cx));
let status_bar = cx.add_view(|cx| {
let mut status_bar = StatusBar::new(&center_pane.clone(), cx);
status_bar.add_left_item(left_dock_buttons, cx);
status_bar.add_right_item(right_dock_buttons, cx);
status_bar.add_right_item(bottom_dock_buttons, cx);
status_bar
});
cx.update_default_global::<DragAndDrop<Workspace>, _, _>(|drag_and_drop, _| {
drag_and_drop.register_container(weak_handle.clone());
});
let mut active_call = None;
if cx.has_global::<ModelHandle<ActiveCall>>() {
let call = cx.global::<ModelHandle<ActiveCall>>().clone();
let mut subscriptions = Vec::new();
subscriptions.push(cx.subscribe(&call, Self::on_active_call_event));
active_call = Some((call, subscriptions));
}
let subscriptions = vec![
cx.observe_fullscreen(|_, _, cx| cx.notify()),
cx.observe_window_activation(Self::on_window_activation_changed),
cx.observe_window_bounds(move |_, mut bounds, display, cx| {
// Transform fixed bounds to be stored in terms of the containing display
if let WindowBounds::Fixed(mut window_bounds) = bounds {
if let Some(screen) = cx.platform().screen_by_id(display) {
let screen_bounds = screen.bounds();
window_bounds
.set_origin_x(window_bounds.origin_x() - screen_bounds.origin_x());
window_bounds
.set_origin_y(window_bounds.origin_y() - screen_bounds.origin_y());
bounds = WindowBounds::Fixed(window_bounds);
}
}
cx.background()
.spawn(DB.set_window_bounds(workspace_id, bounds, display))
.detach_and_log_err(cx);
}),
cx.observe(&left_dock, |this, _, cx| {
this.serialize_workspace(cx);
cx.notify();
}),
cx.observe(&bottom_dock, |this, _, cx| {
this.serialize_workspace(cx);
cx.notify();
}),
cx.observe(&right_dock, |this, _, cx| {
this.serialize_workspace(cx);
cx.notify();
}),
];
let mut this = Workspace {
weak_self: weak_handle.clone(),
modal: None,
zoomed: None,
zoomed_position: None,
center: PaneGroup::new(center_pane.clone()),
panes: vec![center_pane.clone()],
panes_by_item: Default::default(),
active_pane: center_pane.clone(),
last_active_center_pane: Some(center_pane.downgrade()),
status_bar,
titlebar_item: None,
notifications: Default::default(),
remote_entity_subscription: None,
left_dock,
bottom_dock,
right_dock,
project: project.clone(),
leader_state: Default::default(),
follower_states_by_leader: Default::default(),
last_leaders_by_pane: Default::default(),
window_edited: false,
active_call,
database_id: workspace_id,
app_state,
_observe_current_user,
_apply_leader_updates,
_schedule_serialize: None,
leader_updates_tx,
subscriptions,
pane_history_timestamp,
};
this.project_remote_id_changed(project.read(cx).remote_id(), cx);
cx.defer(|this, cx| this.update_window_title(cx));
this
}
fn new_local(
abs_paths: Vec<PathBuf>,
app_state: Arc<AppState>,
requesting_window_id: Option<usize>,
cx: &mut AppContext,
) -> Task<(
WeakViewHandle<Workspace>,
Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
)> {
let project_handle = Project::local(
app_state.client.clone(),
app_state.user_store.clone(),
app_state.languages.clone(),
app_state.fs.clone(),
cx,
);
cx.spawn(|mut cx| async move {
let serialized_workspace = persistence::DB.workspace_for_roots(&abs_paths.as_slice());
let paths_to_open = Arc::new(abs_paths);
// Get project paths for all of the abs_paths
let mut worktree_roots: HashSet<Arc<Path>> = Default::default();
let mut project_paths: Vec<(PathBuf, Option<ProjectPath>)> =
Vec::with_capacity(paths_to_open.len());
for path in paths_to_open.iter().cloned() {
if let Some((worktree, project_entry)) = cx
.update(|cx| {
Workspace::project_path_for_path(project_handle.clone(), &path, true, cx)
})
.await
.log_err()
{
worktree_roots.insert(worktree.read_with(&mut cx, |tree, _| tree.abs_path()));
project_paths.push((path, Some(project_entry)));
} else {
project_paths.push((path, None));
}
}
let workspace_id = if let Some(serialized_workspace) = serialized_workspace.as_ref() {
serialized_workspace.id
} else {
DB.next_id().await.unwrap_or(0)
};
let workspace = requesting_window_id
.and_then(|window_id| {
cx.update(|cx| {
cx.replace_root_view(window_id, |cx| {
Workspace::new(
workspace_id,
project_handle.clone(),
app_state.clone(),
cx,
)
})
})
})
.unwrap_or_else(|| {
let window_bounds_override = window_bounds_env_override(&cx);
let (bounds, display) = if let Some(bounds) = window_bounds_override {
(Some(bounds), None)
} else {
serialized_workspace
.as_ref()
.and_then(|serialized_workspace| {
let display = serialized_workspace.display?;
let mut bounds = serialized_workspace.bounds?;
// Stored bounds are relative to the containing display.
// So convert back to global coordinates if that screen still exists
if let WindowBounds::Fixed(mut window_bounds) = bounds {
if let Some(screen) = cx.platform().screen_by_id(display) {
let screen_bounds = screen.bounds();
window_bounds.set_origin_x(
window_bounds.origin_x() + screen_bounds.origin_x(),
);
window_bounds.set_origin_y(
window_bounds.origin_y() + screen_bounds.origin_y(),
);
bounds = WindowBounds::Fixed(window_bounds);
} else {
// Screen no longer exists. Return none here.
return None;
}
}
Some((bounds, display))
})
.unzip()
};
// Use the serialized workspace to construct the new window
cx.add_window(
(app_state.build_window_options)(bounds, display, cx.platform().as_ref()),
|cx| {
Workspace::new(
workspace_id,
project_handle.clone(),
app_state.clone(),
cx,
)
},
)
.1
});
(app_state.initialize_workspace)(
workspace.downgrade(),
serialized_workspace.is_some(),
app_state.clone(),
cx.clone(),
)
.await
.log_err();
cx.update_window(workspace.window_id(), |cx| cx.activate_window());
let workspace = workspace.downgrade();
notify_if_database_failed(&workspace, &mut cx);
let opened_items = open_items(
serialized_workspace,
&workspace,
project_paths,
app_state,
cx,
)
.await;
(workspace, opened_items)
})
}
pub fn weak_handle(&self) -> WeakViewHandle<Self> {
self.weak_self.clone()
}
pub fn left_dock(&self) -> &ViewHandle<Dock> {
&self.left_dock
}
pub fn bottom_dock(&self) -> &ViewHandle<Dock> {
&self.bottom_dock
}
pub fn right_dock(&self) -> &ViewHandle<Dock> {
&self.right_dock
}
pub fn add_panel<T: Panel>(&mut self, panel: ViewHandle<T>, cx: &mut ViewContext<Self>)
where
T::Event: std::fmt::Debug,
{
let dock = match panel.position(cx) {
DockPosition::Left => &self.left_dock,
DockPosition::Bottom => &self.bottom_dock,
DockPosition::Right => &self.right_dock,
};
self.subscriptions.push(cx.subscribe(&panel, {
let mut dock = dock.clone();
let mut prev_position = panel.position(cx);
move |this, panel, event, cx| {
if T::should_change_position_on_event(event) {
let new_position = panel.read(cx).position(cx);
let mut was_visible = false;
dock.update(cx, |dock, cx| {
prev_position = new_position;
was_visible = dock.is_open()
&& dock
.visible_panel()
.map_or(false, |active_panel| active_panel.id() == panel.id());
dock.remove_panel(&panel, cx);
});
if panel.is_zoomed(cx) {
this.zoomed_position = Some(new_position);
}
dock = match panel.read(cx).position(cx) {
DockPosition::Left => &this.left_dock,
DockPosition::Bottom => &this.bottom_dock,
DockPosition::Right => &this.right_dock,
}
.clone();
dock.update(cx, |dock, cx| {
dock.add_panel(panel.clone(), cx);
if was_visible {
dock.set_open(true, cx);
dock.activate_panel(dock.panels_len() - 1, cx);
}
});
} else if T::should_zoom_in_on_event(event) {
dock.update(cx, |dock, cx| dock.set_panel_zoomed(&panel, true, cx));
if !panel.has_focus(cx) {
cx.focus(&panel);
}
this.zoomed = Some(panel.downgrade().into_any());
this.zoomed_position = Some(panel.read(cx).position(cx));
} else if T::should_zoom_out_on_event(event) {
dock.update(cx, |dock, cx| dock.set_panel_zoomed(&panel, false, cx));
if this.zoomed_position == Some(prev_position) {
this.zoomed = None;
this.zoomed_position = None;
}
cx.notify();
} else if T::is_focus_event(event) {
let position = panel.read(cx).position(cx);
this.dismiss_zoomed_items_to_reveal(Some(position), cx);
if panel.is_zoomed(cx) {
this.zoomed = Some(panel.downgrade().into_any());
this.zoomed_position = Some(position);
} else {
this.zoomed = None;
this.zoomed_position = None;
}
this.update_active_view_for_followers(cx);
cx.notify();
}
}
}));
dock.update(cx, |dock, cx| dock.add_panel(panel, cx));
}
pub fn status_bar(&self) -> &ViewHandle<StatusBar> {
&self.status_bar
}
pub fn app_state(&self) -> &Arc<AppState> {
&self.app_state
}
pub fn user_store(&self) -> &ModelHandle<UserStore> {
&self.app_state.user_store
}
pub fn project(&self) -> &ModelHandle<Project> {
&self.project
}
pub fn recent_navigation_history(
&self,
limit: Option<usize>,
cx: &AppContext,
) -> Vec<(ProjectPath, Option<PathBuf>)> {
let mut abs_paths_opened: HashMap<PathBuf, HashSet<ProjectPath>> = HashMap::default();
let mut history: HashMap<ProjectPath, (Option<PathBuf>, usize)> = HashMap::default();
for pane in &self.panes {
let pane = pane.read(cx);
pane.nav_history()
.for_each_entry(cx, |entry, (project_path, fs_path)| {
if let Some(fs_path) = &fs_path {
abs_paths_opened
.entry(fs_path.clone())
.or_default()
.insert(project_path.clone());
}
let timestamp = entry.timestamp;
match history.entry(project_path) {
hash_map::Entry::Occupied(mut entry) => {
let (_, old_timestamp) = entry.get();
if &timestamp > old_timestamp {
entry.insert((fs_path, timestamp));
}
}
hash_map::Entry::Vacant(entry) => {
entry.insert((fs_path, timestamp));
}
}
});
}
history
.into_iter()
.sorted_by_key(|(_, (_, timestamp))| *timestamp)
.map(|(project_path, (fs_path, _))| (project_path, fs_path))
.rev()
.filter(|(history_path, abs_path)| {
let latest_project_path_opened = abs_path
.as_ref()
.and_then(|abs_path| abs_paths_opened.get(abs_path))
.and_then(|project_paths| {
project_paths
.iter()
.max_by(|b1, b2| b1.worktree_id.cmp(&b2.worktree_id))
});
match latest_project_path_opened {
Some(latest_project_path_opened) => latest_project_path_opened == history_path,
None => true,
}
})
.take(limit.unwrap_or(usize::MAX))
.collect()
}
fn navigate_history(
&mut self,
pane: WeakViewHandle<Pane>,
mode: NavigationMode,
cx: &mut ViewContext<Workspace>,
) -> Task<Result<()>> {
let to_load = if let Some(pane) = pane.upgrade(cx) {
cx.focus(&pane);
pane.update(cx, |pane, cx| {
loop {
// Retrieve the weak item handle from the history.
let entry = pane.nav_history_mut().pop(mode, cx)?;
// If the item is still present in this pane, then activate it.
if let Some(index) = entry
.item
.upgrade(cx)
.and_then(|v| pane.index_for_item(v.as_ref()))
{
let prev_active_item_index = pane.active_item_index();
pane.nav_history_mut().set_mode(mode);
pane.activate_item(index, true, true, cx);
pane.nav_history_mut().set_mode(NavigationMode::Normal);
let mut navigated = prev_active_item_index != pane.active_item_index();
if let Some(data) = entry.data {
navigated |= pane.active_item()?.navigate(data, cx);
}
if navigated {
break None;
}
}
// If the item is no longer present in this pane, then retrieve its
// project path in order to reopen it.
else {
break pane
.nav_history()
.path_for_item(entry.item.id())
.map(|(project_path, _)| (project_path, entry));
}
}
})
} else {
None
};
if let Some((project_path, entry)) = to_load {
// If the item was no longer present, then load it again from its previous path.
let task = self.load_path(project_path, cx);
cx.spawn(|workspace, mut cx| async move {
let task = task.await;
let mut navigated = false;
if let Some((project_entry_id, build_item)) = task.log_err() {
let prev_active_item_id = pane.update(&mut cx, |pane, _| {
pane.nav_history_mut().set_mode(mode);
pane.active_item().map(|p| p.id())
})?;
pane.update(&mut cx, |pane, cx| {
let item = pane.open_item(project_entry_id, true, cx, build_item);
navigated |= Some(item.id()) != prev_active_item_id;
pane.nav_history_mut().set_mode(NavigationMode::Normal);
if let Some(data) = entry.data {
navigated |= item.navigate(data, cx);
}
})?;
}
if !navigated {
workspace
.update(&mut cx, |workspace, cx| {
Self::navigate_history(workspace, pane, mode, cx)
})?
.await?;
}
Ok(())
})
} else {
Task::ready(Ok(()))
}
}
pub fn go_back(
&mut self,
pane: WeakViewHandle<Pane>,
cx: &mut ViewContext<Workspace>,
) -> Task<Result<()>> {
self.navigate_history(pane, NavigationMode::GoingBack, cx)
}
pub fn go_forward(
&mut self,
pane: WeakViewHandle<Pane>,
cx: &mut ViewContext<Workspace>,
) -> Task<Result<()>> {
self.navigate_history(pane, NavigationMode::GoingForward, cx)
}
pub fn reopen_closed_item(&mut self, cx: &mut ViewContext<Workspace>) -> Task<Result<()>> {
self.navigate_history(
self.active_pane().downgrade(),
NavigationMode::ReopeningClosedItem,
cx,
)
}
pub fn client(&self) -> &Client {
&self.app_state.client
}
pub fn set_titlebar_item(&mut self, item: AnyViewHandle, cx: &mut ViewContext<Self>) {
self.titlebar_item = Some(item);
cx.notify();
}
pub fn titlebar_item(&self) -> Option<AnyViewHandle> {
self.titlebar_item.clone()
}
/// Call the given callback with a workspace whose project is local.
///
/// If the given workspace has a local project, then it will be passed
/// to the callback. Otherwise, a new empty window will be created.
pub fn with_local_workspace<T, F>(
&mut self,
cx: &mut ViewContext<Self>,
callback: F,
) -> Task<Result<T>>
where
T: 'static,
F: 'static + FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
{
if self.project.read(cx).is_local() {
Task::Ready(Some(Ok(callback(self, cx))))
} else {
let task = Self::new_local(Vec::new(), self.app_state.clone(), None, cx);
cx.spawn(|_vh, mut cx| async move {
let (workspace, _) = task.await;
workspace.update(&mut cx, callback)
})
}
}
pub fn worktrees<'a>(
&self,
cx: &'a AppContext,
) -> impl 'a + Iterator<Item = ModelHandle<Worktree>> {
self.project.read(cx).worktrees(cx)
}
pub fn visible_worktrees<'a>(
&self,
cx: &'a AppContext,
) -> impl 'a + Iterator<Item = ModelHandle<Worktree>> {
self.project.read(cx).visible_worktrees(cx)
}
pub fn worktree_scans_complete(&self, cx: &AppContext) -> impl Future<Output = ()> + 'static {
let futures = self
.worktrees(cx)
.filter_map(|worktree| worktree.read(cx).as_local())
.map(|worktree| worktree.scan_complete())
.collect::<Vec<_>>();
async move {
for future in futures {
future.await;
}
}
}
pub fn close_global(_: &CloseWindow, cx: &mut AppContext) {
cx.spawn(|mut cx| async move {
let id = cx
.window_ids()
.into_iter()
.find(|&id| cx.window_is_active(id));
if let Some(id) = id {
//This can only get called when the window's project connection has been lost
//so we don't need to prompt the user for anything and instead just close the window
cx.remove_window(id);
}
})
.detach();
}
pub fn close(
&mut self,
_: &CloseWindow,
cx: &mut ViewContext<Self>,
) -> Option<Task<Result<()>>> {
let window_id = cx.window_id();
let prepare = self.prepare_to_close(false, cx);
Some(cx.spawn(|_, mut cx| async move {
if prepare.await? {
cx.remove_window(window_id);
}
Ok(())
}))
}
pub fn prepare_to_close(
&mut self,
quitting: bool,
cx: &mut ViewContext<Self>,
) -> Task<Result<bool>> {
let active_call = self.active_call().cloned();
let window_id = cx.window_id();
cx.spawn(|this, mut cx| async move {
let workspace_count = cx
.window_ids()
.into_iter()
.filter_map(|window_id| cx.root_view(window_id)?.clone().downcast::<Workspace>())
.count();
if let Some(active_call) = active_call {
if !quitting
&& workspace_count == 1
&& active_call.read_with(&cx, |call, _| call.room().is_some())
{
let answer = cx.prompt(
window_id,
PromptLevel::Warning,
"Do you want to leave the current call?",
&["Close window and hang up", "Cancel"],
);
if let Some(mut answer) = answer {
if answer.next().await == Some(1) {
return anyhow::Ok(false);
} else {
active_call
.update(&mut cx, |call, cx| call.hang_up(cx))
.await
.log_err();
}
}
}
}
Ok(this
.update(&mut cx, |this, cx| this.save_all_internal(true, cx))?
.await?)
})
}
fn save_all(&mut self, _: &SaveAll, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
let save_all = self.save_all_internal(false, cx);
Some(cx.foreground().spawn(async move {
save_all.await?;
Ok(())
}))
}
fn save_all_internal(
&mut self,
should_prompt_to_save: bool,
cx: &mut ViewContext<Self>,
) -> Task<Result<bool>> {
if self.project.read(cx).is_read_only() {
return Task::ready(Ok(true));
}
let dirty_items = self
.panes
.iter()
.flat_map(|pane| {
pane.read(cx).items().filter_map(|item| {
if item.is_dirty(cx) {
Some((pane.downgrade(), item.boxed_clone()))
} else {
None
}
})
})
.collect::<Vec<_>>();
let project = self.project.clone();
cx.spawn(|_, mut cx| async move {
for (pane, item) in dirty_items {
let (singleton, project_entry_ids) =
cx.read(|cx| (item.is_singleton(cx), item.project_entry_ids(cx)));
if singleton || !project_entry_ids.is_empty() {
if let Some(ix) =
pane.read_with(&cx, |pane, _| pane.index_for_item(item.as_ref()))?
{
if !Pane::save_item(
project.clone(),
&pane,
ix,
&*item,
should_prompt_to_save,
&mut cx,
)
.await?
{
return Ok(false);
}
}
}
}
Ok(true)
})
}
pub fn open(&mut self, _: &Open, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
let mut paths = cx.prompt_for_paths(PathPromptOptions {
files: true,
directories: true,
multiple: true,
});
Some(cx.spawn(|this, mut cx| async move {
if let Some(paths) = paths.recv().await.flatten() {
if let Some(task) = this
.update(&mut cx, |this, cx| this.open_workspace_for_paths(paths, cx))
.log_err()
{
task.await?
}
}
Ok(())
}))
}
pub fn open_workspace_for_paths(
&mut self,
paths: Vec<PathBuf>,
cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
let window_id = cx.window_id();
let is_remote = self.project.read(cx).is_remote();
let has_worktree = self.project.read(cx).worktrees(cx).next().is_some();
let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx));
let close_task = if is_remote || has_worktree || has_dirty_items {
None
} else {
Some(self.prepare_to_close(false, cx))
};
let app_state = self.app_state.clone();
cx.spawn(|_, mut cx| async move {
let window_id_to_replace = if let Some(close_task) = close_task {
if !close_task.await? {
return Ok(());
}
Some(window_id)
} else {
None
};
cx.update(|cx| open_paths(&paths, &app_state, window_id_to_replace, cx))
.await?;
Ok(())
})
}
#[allow(clippy::type_complexity)]
pub fn open_paths(
&mut self,
mut abs_paths: Vec<PathBuf>,
visible: bool,
cx: &mut ViewContext<Self>,
) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
log::info!("open paths {:?}", abs_paths);
let fs = self.app_state.fs.clone();
// Sort the paths to ensure we add worktrees for parents before their children.
abs_paths.sort_unstable();
cx.spawn(|this, mut cx| async move {
let mut project_paths = Vec::new();
for path in &abs_paths {
if let Some(project_path) = this
.update(&mut cx, |this, cx| {
Workspace::project_path_for_path(this.project.clone(), path, visible, cx)
})
.log_err()
{
project_paths.push(project_path.await.log_err());
} else {
project_paths.push(None);
}
}
let tasks = abs_paths
.iter()
.cloned()
.zip(project_paths.into_iter())
.map(|(abs_path, project_path)| {
let this = this.clone();
cx.spawn(|mut cx| {
let fs = fs.clone();
async move {
let (_worktree, project_path) = project_path?;
if fs.is_file(&abs_path).await {
Some(
this.update(&mut cx, |this, cx| {
this.open_path(project_path, None, true, cx)
})
.log_err()?
.await,
)
} else {
None
}
}
})
})
.collect::<Vec<_>>();
futures::future::join_all(tasks).await
})
}
fn add_folder_to_project(&mut self, _: &AddFolderToProject, cx: &mut ViewContext<Self>) {
let mut paths = cx.prompt_for_paths(PathPromptOptions {
files: false,
directories: true,
multiple: true,
});
cx.spawn(|this, mut cx| async move {
if let Some(paths) = paths.recv().await.flatten() {
let results = this
.update(&mut cx, |this, cx| this.open_paths(paths, true, cx))?
.await;
for result in results.into_iter().flatten() {
result.log_err();
}
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
fn project_path_for_path(
project: ModelHandle<Project>,
abs_path: &Path,
visible: bool,
cx: &mut AppContext,
) -> Task<Result<(ModelHandle<Worktree>, ProjectPath)>> {
let entry = project.update(cx, |project, cx| {
project.find_or_create_local_worktree(abs_path, visible, cx)
});
cx.spawn(|cx| async move {
let (worktree, path) = entry.await?;
let worktree_id = worktree.read_with(&cx, |t, _| t.id());
Ok((
worktree,
ProjectPath {
worktree_id,
path: path.into(),
},
))
})
}
/// Returns the modal that was toggled closed if it was open.
pub fn toggle_modal<V, F>(
&mut self,
cx: &mut ViewContext<Self>,
add_view: F,
) -> Option<ViewHandle<V>>
where
V: 'static + Modal,
F: FnOnce(&mut Self, &mut ViewContext<Self>) -> ViewHandle<V>,
{
cx.notify();
// Whatever modal was visible is getting clobbered. If its the same type as V, then return
// it. Otherwise, create a new modal and set it as active.
if let Some(already_open_modal) = self
.dismiss_modal(cx)
.and_then(|modal| modal.downcast::<V>())
{
cx.focus_self();
Some(already_open_modal)
} else {
let modal = add_view(self, cx);
cx.subscribe(&modal, |this, _, event, cx| {
if V::dismiss_on_event(event) {
this.dismiss_modal(cx);
}
})
.detach();
let previously_focused_view_id = cx.focused_view_id();
cx.focus(&modal);
self.modal = Some(ActiveModal {
view: Box::new(modal),
previously_focused_view_id,
});
None
}
}
pub fn modal<V: 'static + View>(&self) -> Option<ViewHandle<V>> {
self.modal
.as_ref()
.and_then(|modal| modal.view.as_any().clone().downcast::<V>())
}
pub fn dismiss_modal(&mut self, cx: &mut ViewContext<Self>) -> Option<AnyViewHandle> {
if let Some(modal) = self.modal.take() {
if let Some(previously_focused_view_id) = modal.previously_focused_view_id {
if modal.view.has_focus(cx) {
cx.window_context().focus(Some(previously_focused_view_id));
}
}
cx.notify();
Some(modal.view.as_any().clone())
} else {
None
}
}
pub fn items<'a>(
&'a self,
cx: &'a AppContext,
) -> impl 'a + Iterator<Item = &Box<dyn ItemHandle>> {
self.panes.iter().flat_map(|pane| pane.read(cx).items())
}
pub fn item_of_type<T: Item>(&self, cx: &AppContext) -> Option<ViewHandle<T>> {
self.items_of_type(cx).max_by_key(|item| item.id())
}
pub fn items_of_type<'a, T: Item>(
&'a self,
cx: &'a AppContext,
) -> impl 'a + Iterator<Item = ViewHandle<T>> {
self.panes
.iter()
.flat_map(|pane| pane.read(cx).items_of_type())
}
pub fn active_item(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
self.active_pane().read(cx).active_item()
}
fn active_project_path(&self, cx: &ViewContext<Self>) -> Option<ProjectPath> {
self.active_item(cx).and_then(|item| item.project_path(cx))
}
pub fn save_active_item(
&mut self,
force_name_change: bool,
cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
let project = self.project.clone();
if let Some(item) = self.active_item(cx) {
if !force_name_change && item.can_save(cx) {
if item.has_conflict(cx) {
const CONFLICT_MESSAGE: &str = "This file has changed on disk since you started editing it. Do you want to overwrite it?";
let mut answer = cx.prompt(
PromptLevel::Warning,
CONFLICT_MESSAGE,
&["Overwrite", "Cancel"],
);
cx.spawn(|this, mut cx| async move {
let answer = answer.recv().await;
if answer == Some(0) {
this.update(&mut cx, |this, cx| item.save(this.project.clone(), cx))?
.await?;
}
Ok(())
})
} else {
item.save(self.project.clone(), cx)
}
} else if item.is_singleton(cx) {
let worktree = self.worktrees(cx).next();
let start_abs_path = worktree
.and_then(|w| w.read(cx).as_local())
.map_or(Path::new(""), |w| w.abs_path())
.to_path_buf();
let mut abs_path = cx.prompt_for_new_path(&start_abs_path);
cx.spawn(|this, mut cx| async move {
if let Some(abs_path) = abs_path.recv().await.flatten() {
this.update(&mut cx, |_, cx| item.save_as(project, abs_path, cx))?
.await?;
}
Ok(())
})
} else {
Task::ready(Ok(()))
}
} else {
Task::ready(Ok(()))
}
}
pub fn toggle_dock(&mut self, dock_side: DockPosition, cx: &mut ViewContext<Self>) {
let dock = match dock_side {
DockPosition::Left => &self.left_dock,
DockPosition::Bottom => &self.bottom_dock,
DockPosition::Right => &self.right_dock,
};
let mut focus_center = false;
let mut reveal_dock = false;
dock.update(cx, |dock, cx| {
let other_is_zoomed = self.zoomed.is_some() && self.zoomed_position != Some(dock_side);
let was_visible = dock.is_open() && !other_is_zoomed;
dock.set_open(!was_visible, cx);
if let Some(active_panel) = dock.active_panel() {
if was_visible {
if active_panel.has_focus(cx) {
focus_center = true;
}
} else {
cx.focus(active_panel.as_any());
reveal_dock = true;
}
}
});
if reveal_dock {
self.dismiss_zoomed_items_to_reveal(Some(dock_side), cx);
}
if focus_center {
cx.focus_self();
}
cx.notify();
self.serialize_workspace(cx);
}
/// Transfer focus to the panel of the given type.
pub fn focus_panel<T: Panel>(&mut self, cx: &mut ViewContext<Self>) -> Option<ViewHandle<T>> {
self.focus_or_unfocus_panel::<T>(cx, |_, _| true)?
.as_any()
.clone()
.downcast()
}
/// Focus the panel of the given type if it isn't already focused. If it is
/// already focused, then transfer focus back to the workspace center.
pub fn toggle_panel_focus<T: Panel>(&mut self, cx: &mut ViewContext<Self>) {
self.focus_or_unfocus_panel::<T>(cx, |panel, cx| !panel.has_focus(cx));
}
/// Focus or unfocus the given panel type, depending on the given callback.
fn focus_or_unfocus_panel<T: Panel>(
&mut self,
cx: &mut ViewContext<Self>,
should_focus: impl Fn(&dyn PanelHandle, &mut ViewContext<Dock>) -> bool,
) -> Option<Rc<dyn PanelHandle>> {
for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
let mut focus_center = false;
let mut reveal_dock = false;
let panel = dock.update(cx, |dock, cx| {
dock.activate_panel(panel_index, cx);
let panel = dock.active_panel().cloned();
if let Some(panel) = panel.as_ref() {
if should_focus(&**panel, cx) {
dock.set_open(true, cx);
cx.focus(panel.as_any());
reveal_dock = true;
} else {
// if panel.is_zoomed(cx) {
// dock.set_open(false, cx);
// }
focus_center = true;
}
}
panel
});
if focus_center {
cx.focus_self();
}
self.serialize_workspace(cx);
cx.notify();
return panel;
}
}
None
}
pub fn panel<T: Panel>(&self, cx: &WindowContext) -> Option<ViewHandle<T>> {
for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
let dock = dock.read(cx);
if let Some(panel) = dock.panel::<T>() {
return Some(panel);
}
}
None
}
fn zoom_out(&mut self, cx: &mut ViewContext<Self>) {
for pane in &self.panes {
pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
}
self.left_dock.update(cx, |dock, cx| dock.zoom_out(cx));
self.bottom_dock.update(cx, |dock, cx| dock.zoom_out(cx));
self.right_dock.update(cx, |dock, cx| dock.zoom_out(cx));
self.zoomed = None;
self.zoomed_position = None;
cx.notify();
}
#[cfg(any(test, feature = "test-support"))]
pub fn zoomed_view(&self, cx: &AppContext) -> Option<AnyViewHandle> {
self.zoomed.and_then(|view| view.upgrade(cx))
}
fn dismiss_zoomed_items_to_reveal(
&mut self,
dock_to_reveal: Option<DockPosition>,
cx: &mut ViewContext<Self>,
) {
// If a center pane is zoomed, unzoom it.
for pane in &self.panes {
if pane != &self.active_pane || dock_to_reveal.is_some() {
pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
}
}
// If another dock is zoomed, hide it.
let mut focus_center = false;
for dock in [&self.left_dock, &self.right_dock, &self.bottom_dock] {
dock.update(cx, |dock, cx| {
if Some(dock.position()) != dock_to_reveal {
if let Some(panel) = dock.active_panel() {
if panel.is_zoomed(cx) {
focus_center |= panel.has_focus(cx);
dock.set_open(false, cx);
}
}
}
});
}
if focus_center {
cx.focus_self();
}
if self.zoomed_position != dock_to_reveal {
self.zoomed = None;
self.zoomed_position = None;
}
cx.notify();
}
fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
let pane = cx.add_view(|cx| {
Pane::new(
self.weak_handle(),
self.project.clone(),
self.app_state.background_actions,
self.pane_history_timestamp.clone(),
cx,
)
});
cx.subscribe(&pane, Self::handle_pane_event).detach();
self.panes.push(pane.clone());
cx.focus(&pane);
cx.emit(Event::PaneAdded(pane.clone()));
pane
}
pub fn add_item_to_center(
&mut self,
item: Box<dyn ItemHandle>,
cx: &mut ViewContext<Self>,
) -> bool {
if let Some(center_pane) = self.last_active_center_pane.clone() {
if let Some(center_pane) = center_pane.upgrade(cx) {
center_pane.update(cx, |pane, cx| pane.add_item(item, true, true, None, cx));
true
} else {
false
}
} else {
false
}
}
pub fn add_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
self.active_pane
.update(cx, |pane, cx| pane.add_item(item, true, true, None, cx));
}
pub fn split_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
let new_pane = self.split_pane(self.active_pane.clone(), SplitDirection::Right, cx);
new_pane.update(cx, move |new_pane, cx| {
new_pane.add_item(item, true, true, None, cx)
})
}
pub fn open_abs_path(
&mut self,
abs_path: PathBuf,
visible: bool,
cx: &mut ViewContext<Self>,
) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
cx.spawn(|workspace, mut cx| async move {
let open_paths_task_result = workspace
.update(&mut cx, |workspace, cx| {
workspace.open_paths(vec![abs_path.clone()], visible, cx)
})
.with_context(|| format!("open abs path {abs_path:?} task spawn"))?
.await;
anyhow::ensure!(
open_paths_task_result.len() == 1,
"open abs path {abs_path:?} task returned incorrect number of results"
);
match open_paths_task_result
.into_iter()
.next()
.expect("ensured single task result")
{
Some(open_result) => {
open_result.with_context(|| format!("open abs path {abs_path:?} task join"))
}
None => anyhow::bail!("open abs path {abs_path:?} task returned None"),
}
})
}
pub fn split_abs_path(
&mut self,
abs_path: PathBuf,
visible: bool,
cx: &mut ViewContext<Self>,
) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
let project_path_task =
Workspace::project_path_for_path(self.project.clone(), &abs_path, visible, cx);
cx.spawn(|this, mut cx| async move {
let (_, path) = project_path_task.await?;
this.update(&mut cx, |this, cx| this.split_path(path, cx))?
.await
})
}
pub fn open_path(
&mut self,
path: impl Into<ProjectPath>,
pane: Option<WeakViewHandle<Pane>>,
focus_item: bool,
cx: &mut ViewContext<Self>,
) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
let pane = pane.unwrap_or_else(|| {
self.last_active_center_pane.clone().unwrap_or_else(|| {
self.panes
.first()
.expect("There must be an active pane")
.downgrade()
})
});
let task = self.load_path(path.into(), cx);
cx.spawn(|_, mut cx| async move {
let (project_entry_id, build_item) = task.await?;
pane.update(&mut cx, |pane, cx| {
pane.open_item(project_entry_id, focus_item, cx, build_item)
})
})
}
pub fn split_path(
&mut self,
path: impl Into<ProjectPath>,
cx: &mut ViewContext<Self>,
) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
let pane = self.last_active_center_pane.clone().unwrap_or_else(|| {
self.panes
.first()
.expect("There must be an active pane")
.downgrade()
});
if let Member::Pane(center_pane) = &self.center.root {
if center_pane.read(cx).items_len() == 0 {
return self.open_path(path, Some(pane), true, cx);
}
}
let task = self.load_path(path.into(), cx);
cx.spawn(|this, mut cx| async move {
let (project_entry_id, build_item) = task.await?;
this.update(&mut cx, move |this, cx| -> Option<_> {
let pane = pane.upgrade(cx)?;
let new_pane = this.split_pane(pane, SplitDirection::Right, cx);
new_pane.update(cx, |new_pane, cx| {
Some(new_pane.open_item(project_entry_id, true, cx, build_item))
})
})
.map(|option| option.ok_or_else(|| anyhow!("pane was dropped")))?
})
}
pub(crate) fn load_path(
&mut self,
path: ProjectPath,
cx: &mut ViewContext<Self>,
) -> Task<
Result<(
ProjectEntryId,
impl 'static + FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
)>,
> {
let project = self.project().clone();
let project_item = project.update(cx, |project, cx| project.open_path(path, cx));
cx.spawn(|_, mut cx| async move {
let (project_entry_id, project_item) = project_item.await?;
let build_item = cx.update(|cx| {
cx.default_global::<ProjectItemBuilders>()
.get(&project_item.model_type())
.ok_or_else(|| anyhow!("no item builder for project item"))
.cloned()
})?;
let build_item =
move |cx: &mut ViewContext<Pane>| build_item(project, project_item, cx);
Ok((project_entry_id, build_item))
})
}
pub fn open_project_item<T>(
&mut self,
project_item: ModelHandle<T::Item>,
cx: &mut ViewContext<Self>,
) -> ViewHandle<T>
where
T: ProjectItem,
{
use project::Item as _;
let entry_id = project_item.read(cx).entry_id(cx);
if let Some(item) = entry_id
.and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id, cx))
.and_then(|item| item.downcast())
{
self.activate_item(&item, cx);
return item;
}
let item = cx.add_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
self.add_item(Box::new(item.clone()), cx);
item
}
pub fn split_project_item<T>(
&mut self,
project_item: ModelHandle<T::Item>,
cx: &mut ViewContext<Self>,
) -> ViewHandle<T>
where
T: ProjectItem,
{
use project::Item as _;
let entry_id = project_item.read(cx).entry_id(cx);
if let Some(item) = entry_id
.and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id, cx))
.and_then(|item| item.downcast())
{
self.activate_item(&item, cx);
return item;
}
let item = cx.add_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
self.split_item(Box::new(item.clone()), cx);
item
}
pub fn open_shared_screen(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
if let Some(shared_screen) = self.shared_screen_for_peer(peer_id, &self.active_pane, cx) {
self.active_pane.update(cx, |pane, cx| {
pane.add_item(Box::new(shared_screen), false, true, None, cx)
});
}
}
pub fn activate_item(&mut self, item: &dyn ItemHandle, cx: &mut ViewContext<Self>) -> bool {
let result = self.panes.iter().find_map(|pane| {
pane.read(cx)
.index_for_item(item)
.map(|ix| (pane.clone(), ix))
});
if let Some((pane, ix)) = result {
pane.update(cx, |pane, cx| pane.activate_item(ix, true, true, cx));
true
} else {
false
}
}
fn activate_pane_at_index(&mut self, action: &ActivatePane, cx: &mut ViewContext<Self>) {
let panes = self.center.panes();
if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
cx.focus(&pane);
} else {
self.split_and_clone(self.active_pane.clone(), SplitDirection::Right, cx);
}
}
pub fn activate_next_pane(&mut self, cx: &mut ViewContext<Self>) {
let panes = self.center.panes();
if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
let next_ix = (ix + 1) % panes.len();
let next_pane = panes[next_ix].clone();
cx.focus(&next_pane);
}
}
pub fn activate_previous_pane(&mut self, cx: &mut ViewContext<Self>) {
let panes = self.center.panes();
if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
let prev_pane = panes[prev_ix].clone();
cx.focus(&prev_pane);
}
}
pub fn activate_pane_in_direction(
&mut self,
direction: SplitDirection,
cx: &mut ViewContext<Self>,
) {
let bounding_box = match self.center.bounding_box_for_pane(&self.active_pane) {
Some(coordinates) => coordinates,
None => {
return;
}
};
let cursor = self.active_pane.read(cx).pixel_position_of_cursor(cx);
let center = match cursor {
Some(cursor) if bounding_box.contains_point(cursor) => cursor,
_ => bounding_box.center(),
};
let distance_to_next = theme::current(cx).workspace.pane_divider.width + 1.;
let target = match direction {
SplitDirection::Left => vec2f(bounding_box.origin_x() - distance_to_next, center.y()),
SplitDirection::Right => vec2f(bounding_box.max_x() + distance_to_next, center.y()),
SplitDirection::Up => vec2f(center.x(), bounding_box.origin_y() - distance_to_next),
SplitDirection::Down => vec2f(center.x(), bounding_box.max_y() + distance_to_next),
};
if let Some(pane) = self.center.pane_at_pixel_position(target) {
cx.focus(pane);
}
}
fn handle_pane_focused(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
if self.active_pane != pane {
self.active_pane = pane.clone();
self.status_bar.update(cx, |status_bar, cx| {
status_bar.set_active_pane(&self.active_pane, cx);
});
self.active_item_path_changed(cx);
self.last_active_center_pane = Some(pane.downgrade());
}
self.dismiss_zoomed_items_to_reveal(None, cx);
if pane.read(cx).is_zoomed() {
self.zoomed = Some(pane.downgrade().into_any());
} else {
self.zoomed = None;
}
self.zoomed_position = None;
self.update_active_view_for_followers(cx);
cx.notify();
}
fn handle_pane_event(
&mut self,
pane: ViewHandle<Pane>,
event: &pane::Event,
cx: &mut ViewContext<Self>,
) {
match event {
pane::Event::AddItem { item } => item.added_to_pane(self, pane, cx),
pane::Event::Split(direction) => {
self.split_and_clone(pane, *direction, cx);
}
pane::Event::Remove => self.remove_pane(pane, cx),
pane::Event::ActivateItem { local } => {
if *local {
self.unfollow(&pane, cx);
}
if &pane == self.active_pane() {
self.active_item_path_changed(cx);
}
}
pane::Event::ChangeItemTitle => {
if pane == self.active_pane {
self.active_item_path_changed(cx);
}
self.update_window_edited(cx);
}
pane::Event::RemoveItem { item_id } => {
self.update_window_edited(cx);
if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) {
if entry.get().id() == pane.id() {
entry.remove();
}
}
}
pane::Event::Focus => {
self.handle_pane_focused(pane.clone(), cx);
}
pane::Event::ZoomIn => {
if pane == self.active_pane {
pane.update(cx, |pane, cx| pane.set_zoomed(true, cx));
if pane.read(cx).has_focus() {
self.zoomed = Some(pane.downgrade().into_any());
self.zoomed_position = None;
}
cx.notify();
}
}
pane::Event::ZoomOut => {
pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
if self.zoomed_position.is_none() {
self.zoomed = None;
}
cx.notify();
}
}
self.serialize_workspace(cx);
}
pub fn split_pane(
&mut self,
pane_to_split: ViewHandle<Pane>,
split_direction: SplitDirection,
cx: &mut ViewContext<Self>,
) -> ViewHandle<Pane> {
let new_pane = self.add_pane(cx);
self.center
.split(&pane_to_split, &new_pane, split_direction)
.unwrap();
cx.notify();
new_pane
}
pub fn split_and_clone(
&mut self,
pane: ViewHandle<Pane>,
direction: SplitDirection,
cx: &mut ViewContext<Self>,
) -> Option<ViewHandle<Pane>> {
let item = pane.read(cx).active_item()?;
let maybe_pane_handle = if let Some(clone) = item.clone_on_split(self.database_id(), cx) {
let new_pane = self.add_pane(cx);
new_pane.update(cx, |pane, cx| pane.add_item(clone, true, true, None, cx));
self.center.split(&pane, &new_pane, direction).unwrap();
Some(new_pane)
} else {
None
};
cx.notify();
maybe_pane_handle
}
pub fn split_pane_with_item(
&mut self,
pane_to_split: WeakViewHandle<Pane>,
split_direction: SplitDirection,
from: WeakViewHandle<Pane>,
item_id_to_move: usize,
cx: &mut ViewContext<Self>,
) {
let Some(pane_to_split) = pane_to_split.upgrade(cx) else { return; };
let Some(from) = from.upgrade(cx) else { return; };
let new_pane = self.add_pane(cx);
self.move_item(from.clone(), new_pane.clone(), item_id_to_move, 0, cx);
self.center
.split(&pane_to_split, &new_pane, split_direction)
.unwrap();
cx.notify();
}
pub fn split_pane_with_project_entry(
&mut self,
pane_to_split: WeakViewHandle<Pane>,
split_direction: SplitDirection,
project_entry: ProjectEntryId,
cx: &mut ViewContext<Self>,
) -> Option<Task<Result<()>>> {
let pane_to_split = pane_to_split.upgrade(cx)?;
let new_pane = self.add_pane(cx);
self.center
.split(&pane_to_split, &new_pane, split_direction)
.unwrap();
let path = self.project.read(cx).path_for_entry(project_entry, cx)?;
let task = self.open_path(path, Some(new_pane.downgrade()), true, cx);
Some(cx.foreground().spawn(async move {
task.await?;
Ok(())
}))
}
pub fn move_item(
&mut self,
source: ViewHandle<Pane>,
destination: ViewHandle<Pane>,
item_id_to_move: usize,
destination_index: usize,
cx: &mut ViewContext<Self>,
) {
let item_to_move = source
.read(cx)
.items()
.enumerate()
.find(|(_, item_handle)| item_handle.id() == item_id_to_move);
if item_to_move.is_none() {
log::warn!("Tried to move item handle which was not in `from` pane. Maybe tab was closed during drop");
return;
}
let (item_ix, item_handle) = item_to_move.unwrap();
let item_handle = item_handle.clone();
if source != destination {
// Close item from previous pane
source.update(cx, |source, cx| {
source.remove_item(item_ix, false, cx);
});
}
// This automatically removes duplicate items in the pane
destination.update(cx, |destination, cx| {
destination.add_item(item_handle, true, true, Some(destination_index), cx);
cx.focus_self();
});
}
fn remove_pane(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
if self.center.remove(&pane).unwrap() {
self.force_remove_pane(&pane, cx);
self.unfollow(&pane, cx);
self.last_leaders_by_pane.remove(&pane.downgrade());
for removed_item in pane.read(cx).items() {
self.panes_by_item.remove(&removed_item.id());
}
cx.notify();
} else {
self.active_item_path_changed(cx);
}
}
pub fn panes(&self) -> &[ViewHandle<Pane>] {
&self.panes
}
pub fn active_pane(&self) -> &ViewHandle<Pane> {
&self.active_pane
}
fn project_remote_id_changed(&mut self, remote_id: Option<u64>, cx: &mut ViewContext<Self>) {
if let Some(remote_id) = remote_id {
self.remote_entity_subscription = Some(
self.app_state
.client
.add_view_for_remote_entity(remote_id, cx),
);
} else {
self.remote_entity_subscription.take();
}
}
fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
self.leader_state.followers.remove(&peer_id);
if let Some(states_by_pane) = self.follower_states_by_leader.remove(&peer_id) {
for state in states_by_pane.into_values() {
for item in state.items_by_leader_view_id.into_values() {
item.set_leader_replica_id(None, cx);
}
}
}
cx.notify();
}
pub fn toggle_follow(
&mut self,
leader_id: PeerId,
cx: &mut ViewContext<Self>,
) -> Option<Task<Result<()>>> {
let pane = self.active_pane().clone();
if let Some(prev_leader_id) = self.unfollow(&pane, cx) {
if leader_id == prev_leader_id {
return None;
}
}
self.last_leaders_by_pane
.insert(pane.downgrade(), leader_id);
self.follower_states_by_leader
.entry(leader_id)
.or_default()
.insert(pane.clone(), Default::default());
cx.notify();
let project_id = self.project.read(cx).remote_id()?;
let request = self.app_state.client.request(proto::Follow {
project_id,
leader_id: Some(leader_id),
});
Some(cx.spawn(|this, mut cx| async move {
let response = request.await?;
this.update(&mut cx, |this, _| {
let state = this
.follower_states_by_leader
.get_mut(&leader_id)
.and_then(|states_by_pane| states_by_pane.get_mut(&pane))
.ok_or_else(|| anyhow!("following interrupted"))?;
state.active_view_id = if let Some(active_view_id) = response.active_view_id {
Some(ViewId::from_proto(active_view_id)?)
} else {
None
};
Ok::<_, anyhow::Error>(())
})??;
Self::add_views_from_leader(
this.clone(),
leader_id,
vec![pane],
response.views,
&mut cx,
)
.await?;
this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx))?;
Ok(())
}))
}
pub fn follow_next_collaborator(
&mut self,
_: &FollowNextCollaborator,
cx: &mut ViewContext<Self>,
) -> Option<Task<Result<()>>> {
let collaborators = self.project.read(cx).collaborators();
let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
let mut collaborators = collaborators.keys().copied();
for peer_id in collaborators.by_ref() {
if peer_id == leader_id {
break;
}
}
collaborators.next()
} else if let Some(last_leader_id) =
self.last_leaders_by_pane.get(&self.active_pane.downgrade())
{
if collaborators.contains_key(last_leader_id) {
Some(*last_leader_id)
} else {
None
}
} else {
None
};
next_leader_id
.or_else(|| collaborators.keys().copied().next())
.and_then(|leader_id| self.toggle_follow(leader_id, cx))
}
pub fn unfollow(
&mut self,
pane: &ViewHandle<Pane>,
cx: &mut ViewContext<Self>,
) -> Option<PeerId> {
for (leader_id, states_by_pane) in &mut self.follower_states_by_leader {
let leader_id = *leader_id;
if let Some(state) = states_by_pane.remove(pane) {
for (_, item) in state.items_by_leader_view_id {
item.set_leader_replica_id(None, cx);
}
if states_by_pane.is_empty() {
self.follower_states_by_leader.remove(&leader_id);
if let Some(project_id) = self.project.read(cx).remote_id() {
self.app_state
.client
.send(proto::Unfollow {
project_id,
leader_id: Some(leader_id),
})
.log_err();
}
}
cx.notify();
return Some(leader_id);
}
}
None
}
pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
self.follower_states_by_leader.contains_key(&peer_id)
}
pub fn is_followed_by(&self, peer_id: PeerId) -> bool {
self.leader_state.followers.contains(&peer_id)
}
fn render_titlebar(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
// TODO: There should be a better system in place for this
// (https://github.com/zed-industries/zed/issues/1290)
let is_fullscreen = cx.window_is_fullscreen();
let container_theme = if is_fullscreen {
let mut container_theme = theme.titlebar.container;
container_theme.padding.left = container_theme.padding.right;
container_theme
} else {
theme.titlebar.container
};
enum TitleBar {}
MouseEventHandler::<TitleBar, _>::new(0, cx, |_, cx| {
Stack::new()
.with_children(
self.titlebar_item
.as_ref()
.map(|item| ChildView::new(item, cx)),
)
.contained()
.with_style(container_theme)
})
.on_click(MouseButton::Left, |event, _, cx| {
if event.click_count == 2 {
cx.zoom_window();
}
})
.constrained()
.with_height(theme.titlebar.height)
.into_any_named("titlebar")
}
fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
let active_entry = self.active_project_path(cx);
self.project
.update(cx, |project, cx| project.set_active_path(active_entry, cx));
self.update_window_title(cx);
}
fn update_window_title(&mut self, cx: &mut ViewContext<Self>) {
let project = self.project().read(cx);
let mut title = String::new();
if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
let filename = path
.path
.file_name()
.map(|s| s.to_string_lossy())
.or_else(|| {
Some(Cow::Borrowed(
project
.worktree_for_id(path.worktree_id, cx)?
.read(cx)
.root_name(),
))
});
if let Some(filename) = filename {
title.push_str(filename.as_ref());
title.push_str("");
}
}
for (i, name) in project.worktree_root_names(cx).enumerate() {
if i > 0 {
title.push_str(", ");
}
title.push_str(name);
}
if title.is_empty() {
title = "empty project".to_string();
}
if project.is_remote() {
title.push_str("");
} else if project.is_shared() {
title.push_str("");
}
cx.set_window_title(&title);
}
fn update_window_edited(&mut self, cx: &mut ViewContext<Self>) {
let is_edited = !self.project.read(cx).is_read_only()
&& self
.items(cx)
.any(|item| item.has_conflict(cx) || item.is_dirty(cx));
if is_edited != self.window_edited {
self.window_edited = is_edited;
cx.set_window_edited(self.window_edited)
}
}
fn render_disconnected_overlay(
&self,
cx: &mut ViewContext<Workspace>,
) -> Option<AnyElement<Workspace>> {
if self.project.read(cx).is_read_only() {
enum DisconnectedOverlay {}
Some(
MouseEventHandler::<DisconnectedOverlay, _>::new(0, cx, |_, cx| {
let theme = &theme::current(cx);
Label::new(
"Your connection to the remote project has been lost.",
theme.workspace.disconnected_overlay.text.clone(),
)
.aligned()
.contained()
.with_style(theme.workspace.disconnected_overlay.container)
})
.with_cursor_style(CursorStyle::Arrow)
.capture_all()
.into_any_named("disconnected overlay"),
)
} else {
None
}
}
fn render_notifications(
&self,
theme: &theme::Workspace,
cx: &AppContext,
) -> Option<AnyElement<Workspace>> {
if self.notifications.is_empty() {
None
} else {
Some(
Flex::column()
.with_children(self.notifications.iter().map(|(_, _, notification)| {
ChildView::new(notification.as_any(), cx)
.contained()
.with_style(theme.notification)
}))
.constrained()
.with_width(theme.notifications.width)
.contained()
.with_style(theme.notifications.container)
.aligned()
.bottom()
.right()
.into_any(),
)
}
}
// RPC handlers
async fn handle_follow(
this: WeakViewHandle<Self>,
envelope: TypedEnvelope<proto::Follow>,
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<proto::FollowResponse> {
this.update(&mut cx, |this, cx| {
let client = &this.app_state.client;
this.leader_state
.followers
.insert(envelope.original_sender_id()?);
let active_view_id = this.active_item(cx).and_then(|i| {
Some(
i.to_followable_item_handle(cx)?
.remote_id(client, cx)?
.to_proto(),
)
});
cx.notify();
Ok(proto::FollowResponse {
active_view_id,
views: this
.panes()
.iter()
.flat_map(|pane| {
let leader_id = this.leader_for_pane(pane);
pane.read(cx).items().filter_map({
let cx = &cx;
move |item| {
let item = item.to_followable_item_handle(cx)?;
let id = item.remote_id(client, cx)?.to_proto();
let variant = item.to_state_proto(cx)?;
Some(proto::View {
id: Some(id),
leader_id,
variant: Some(variant),
})
}
})
})
.collect(),
})
})?
}
async fn handle_unfollow(
this: WeakViewHandle<Self>,
envelope: TypedEnvelope<proto::Unfollow>,
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, cx| {
this.leader_state
.followers
.remove(&envelope.original_sender_id()?);
cx.notify();
Ok(())
})?
}
async fn handle_update_followers(
this: WeakViewHandle<Self>,
envelope: TypedEnvelope<proto::UpdateFollowers>,
_: Arc<Client>,
cx: AsyncAppContext,
) -> Result<()> {
let leader_id = envelope.original_sender_id()?;
this.read_with(&cx, |this, _| {
this.leader_updates_tx
.unbounded_send((leader_id, envelope.payload))
})??;
Ok(())
}
async fn process_leader_update(
this: &WeakViewHandle<Self>,
leader_id: PeerId,
update: proto::UpdateFollowers,
cx: &mut AsyncAppContext,
) -> Result<()> {
match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
this.update(cx, |this, _| {
if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
for state in state.values_mut() {
state.active_view_id =
if let Some(active_view_id) = update_active_view.id.clone() {
Some(ViewId::from_proto(active_view_id)?)
} else {
None
};
}
}
anyhow::Ok(())
})??;
}
proto::update_followers::Variant::UpdateView(update_view) => {
let variant = update_view
.variant
.ok_or_else(|| anyhow!("missing update view variant"))?;
let id = update_view
.id
.ok_or_else(|| anyhow!("missing update view id"))?;
let mut tasks = Vec::new();
this.update(cx, |this, cx| {
let project = this.project.clone();
if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
for state in state.values_mut() {
let view_id = ViewId::from_proto(id.clone())?;
if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
tasks.push(item.apply_update_proto(&project, variant.clone(), cx));
}
}
}
anyhow::Ok(())
})??;
try_join_all(tasks).await.log_err();
}
proto::update_followers::Variant::CreateView(view) => {
let panes = this.read_with(cx, |this, _| {
this.follower_states_by_leader
.get(&leader_id)
.into_iter()
.flat_map(|states_by_pane| states_by_pane.keys())
.cloned()
.collect()
})?;
Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], cx).await?;
}
}
this.update(cx, |this, cx| this.leader_updated(leader_id, cx))?;
Ok(())
}
async fn add_views_from_leader(
this: WeakViewHandle<Self>,
leader_id: PeerId,
panes: Vec<ViewHandle<Pane>>,
views: Vec<proto::View>,
cx: &mut AsyncAppContext,
) -> Result<()> {
let project = this.read_with(cx, |this, _| this.project.clone())?;
let replica_id = project
.read_with(cx, |project, _| {
project
.collaborators()
.get(&leader_id)
.map(|c| c.replica_id)
})
.ok_or_else(|| anyhow!("no such collaborator {}", leader_id))?;
let item_builders = cx.update(|cx| {
cx.default_global::<FollowableItemBuilders>()
.values()
.map(|b| b.0)
.collect::<Vec<_>>()
});
let mut item_tasks_by_pane = HashMap::default();
for pane in panes {
let mut item_tasks = Vec::new();
let mut leader_view_ids = Vec::new();
for view in &views {
let Some(id) = &view.id else { continue };
let id = ViewId::from_proto(id.clone())?;
let mut variant = view.variant.clone();
if variant.is_none() {
Err(anyhow!("missing variant"))?;
}
for build_item in &item_builders {
let task = cx.update(|cx| {
build_item(pane.clone(), project.clone(), id, &mut variant, cx)
});
if let Some(task) = task {
item_tasks.push(task);
leader_view_ids.push(id);
break;
} else {
assert!(variant.is_some());
}
}
}
item_tasks_by_pane.insert(pane, (item_tasks, leader_view_ids));
}
for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane {
let items = futures::future::try_join_all(item_tasks).await?;
this.update(cx, |this, cx| {
let state = this
.follower_states_by_leader
.get_mut(&leader_id)?
.get_mut(&pane)?;
for (id, item) in leader_view_ids.into_iter().zip(items) {
item.set_leader_replica_id(Some(replica_id), cx);
state.items_by_leader_view_id.insert(id, item);
}
Some(())
})?;
}
Ok(())
}
fn update_active_view_for_followers(&self, cx: &AppContext) {
if self.active_pane.read(cx).has_focus() {
self.update_followers(
proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView {
id: self.active_item(cx).and_then(|item| {
item.to_followable_item_handle(cx)?
.remote_id(&self.app_state.client, cx)
.map(|id| id.to_proto())
}),
leader_id: self.leader_for_pane(&self.active_pane),
}),
cx,
);
} else {
self.update_followers(
proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView {
id: None,
leader_id: None,
}),
cx,
);
}
}
fn update_followers(
&self,
update: proto::update_followers::Variant,
cx: &AppContext,
) -> Option<()> {
let project_id = self.project.read(cx).remote_id()?;
if !self.leader_state.followers.is_empty() {
self.app_state
.client
.send(proto::UpdateFollowers {
project_id,
follower_ids: self.leader_state.followers.iter().copied().collect(),
variant: Some(update),
})
.log_err();
}
None
}
pub fn leader_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<PeerId> {
self.follower_states_by_leader
.iter()
.find_map(|(leader_id, state)| {
if state.contains_key(pane) {
Some(*leader_id)
} else {
None
}
})
}
fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
cx.notify();
let call = self.active_call()?;
let room = call.read(cx).room()?.read(cx);
let participant = room.remote_participant_for_peer_id(leader_id)?;
let mut items_to_activate = Vec::new();
match participant.location {
call::ParticipantLocation::SharedProject { project_id } => {
if Some(project_id) == self.project.read(cx).remote_id() {
for (pane, state) in self.follower_states_by_leader.get(&leader_id)? {
if let Some(item) = state
.active_view_id
.and_then(|id| state.items_by_leader_view_id.get(&id))
{
items_to_activate.push((pane.clone(), item.boxed_clone()));
} else if let Some(shared_screen) =
self.shared_screen_for_peer(leader_id, pane, cx)
{
items_to_activate.push((pane.clone(), Box::new(shared_screen)));
}
}
}
}
call::ParticipantLocation::UnsharedProject => {}
call::ParticipantLocation::External => {
for (pane, _) in self.follower_states_by_leader.get(&leader_id)? {
if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
items_to_activate.push((pane.clone(), Box::new(shared_screen)));
}
}
}
}
for (pane, item) in items_to_activate {
let pane_was_focused = pane.read(cx).has_focus();
if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) {
pane.update(cx, |pane, cx| pane.activate_item(index, false, false, cx));
} else {
pane.update(cx, |pane, cx| {
pane.add_item(item.boxed_clone(), false, false, None, cx)
});
}
if pane_was_focused {
pane.update(cx, |pane, cx| pane.focus_active_item(cx));
}
}
None
}
fn shared_screen_for_peer(
&self,
peer_id: PeerId,
pane: &ViewHandle<Pane>,
cx: &mut ViewContext<Self>,
) -> Option<ViewHandle<SharedScreen>> {
let call = self.active_call()?;
let room = call.read(cx).room()?.read(cx);
let participant = room.remote_participant_for_peer_id(peer_id)?;
let track = participant.video_tracks.values().next()?.clone();
let user = participant.user.clone();
for item in pane.read(cx).items_of_type::<SharedScreen>() {
if item.read(cx).peer_id == peer_id {
return Some(item);
}
}
Some(cx.add_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx)))
}
pub fn on_window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
if active {
cx.background()
.spawn(persistence::DB.update_timestamp(self.database_id()))
.detach();
} else {
for pane in &self.panes {
pane.update(cx, |pane, cx| {
if let Some(item) = pane.active_item() {
item.workspace_deactivated(cx);
}
if matches!(
settings::get::<WorkspaceSettings>(cx).autosave,
AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
) {
for item in pane.items() {
Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
.detach_and_log_err(cx);
}
}
});
}
}
}
fn active_call(&self) -> Option<&ModelHandle<ActiveCall>> {
self.active_call.as_ref().map(|(call, _)| call)
}
fn on_active_call_event(
&mut self,
_: ModelHandle<ActiveCall>,
event: &call::room::Event,
cx: &mut ViewContext<Self>,
) {
match event {
call::room::Event::ParticipantLocationChanged { participant_id }
| call::room::Event::RemoteVideoTracksChanged { participant_id } => {
self.leader_updated(*participant_id, cx);
}
_ => {}
}
}
pub fn database_id(&self) -> WorkspaceId {
self.database_id
}
fn location(&self, cx: &AppContext) -> Option<WorkspaceLocation> {
let project = self.project().read(cx);
if project.is_local() {
Some(
project
.visible_worktrees(cx)
.map(|worktree| worktree.read(cx).abs_path())
.collect::<Vec<_>>()
.into(),
)
} else {
None
}
}
fn remove_panes(&mut self, member: Member, cx: &mut ViewContext<Workspace>) {
match member {
Member::Axis(PaneAxis { members, .. }) => {
for child in members.iter() {
self.remove_panes(child.clone(), cx)
}
}
Member::Pane(pane) => {
self.force_remove_pane(&pane, cx);
}
}
}
fn force_remove_pane(&mut self, pane: &ViewHandle<Pane>, cx: &mut ViewContext<Workspace>) {
self.panes.retain(|p| p != pane);
cx.focus(self.panes.last().unwrap());
if self.last_active_center_pane == Some(pane.downgrade()) {
self.last_active_center_pane = None;
}
cx.notify();
}
fn schedule_serialize(&mut self, cx: &mut ViewContext<Self>) {
self._schedule_serialize = Some(cx.spawn(|this, cx| async move {
cx.background().timer(Duration::from_millis(100)).await;
this.read_with(&cx, |this, cx| this.serialize_workspace(cx))
.ok();
}));
}
fn serialize_workspace(&self, cx: &ViewContext<Self>) {
fn serialize_pane_handle(
pane_handle: &ViewHandle<Pane>,
cx: &AppContext,
) -> SerializedPane {
let (items, active) = {
let pane = pane_handle.read(cx);
let active_item_id = pane.active_item().map(|item| item.id());
(
pane.items()
.filter_map(|item_handle| {
Some(SerializedItem {
kind: Arc::from(item_handle.serialized_item_kind()?),
item_id: item_handle.id(),
active: Some(item_handle.id()) == active_item_id,
})
})
.collect::<Vec<_>>(),
pane.has_focus(),
)
};
SerializedPane::new(items, active)
}
fn build_serialized_pane_group(
pane_group: &Member,
cx: &AppContext,
) -> SerializedPaneGroup {
match pane_group {
Member::Axis(PaneAxis {
axis,
members,
flexes,
bounding_boxes: _,
}) => SerializedPaneGroup::Group {
axis: *axis,
children: members
.iter()
.map(|member| build_serialized_pane_group(member, cx))
.collect::<Vec<_>>(),
flexes: Some(flexes.borrow().clone()),
},
Member::Pane(pane_handle) => {
SerializedPaneGroup::Pane(serialize_pane_handle(&pane_handle, cx))
}
}
}
fn build_serialized_docks(this: &Workspace, cx: &ViewContext<Workspace>) -> DockStructure {
let left_dock = this.left_dock.read(cx);
let left_visible = left_dock.is_open();
let left_active_panel = left_dock.visible_panel().and_then(|panel| {
Some(
cx.view_ui_name(panel.as_any().window_id(), panel.id())?
.to_string(),
)
});
let left_dock_zoom = left_dock
.visible_panel()
.map(|panel| panel.is_zoomed(cx))
.unwrap_or(false);
let right_dock = this.right_dock.read(cx);
let right_visible = right_dock.is_open();
let right_active_panel = right_dock.visible_panel().and_then(|panel| {
Some(
cx.view_ui_name(panel.as_any().window_id(), panel.id())?
.to_string(),
)
});
let right_dock_zoom = right_dock
.visible_panel()
.map(|panel| panel.is_zoomed(cx))
.unwrap_or(false);
let bottom_dock = this.bottom_dock.read(cx);
let bottom_visible = bottom_dock.is_open();
let bottom_active_panel = bottom_dock.visible_panel().and_then(|panel| {
Some(
cx.view_ui_name(panel.as_any().window_id(), panel.id())?
.to_string(),
)
});
let bottom_dock_zoom = bottom_dock
.visible_panel()
.map(|panel| panel.is_zoomed(cx))
.unwrap_or(false);
DockStructure {
left: DockData {
visible: left_visible,
active_panel: left_active_panel,
zoom: left_dock_zoom,
},
right: DockData {
visible: right_visible,
active_panel: right_active_panel,
zoom: right_dock_zoom,
},
bottom: DockData {
visible: bottom_visible,
active_panel: bottom_active_panel,
zoom: bottom_dock_zoom,
},
}
}
if let Some(location) = self.location(cx) {
// Load bearing special case:
// - with_local_workspace() relies on this to not have other stuff open
// when you open your log
if !location.paths().is_empty() {
let center_group = build_serialized_pane_group(&self.center.root, cx);
let docks = build_serialized_docks(self, cx);
let serialized_workspace = SerializedWorkspace {
id: self.database_id,
location,
center_group,
bounds: Default::default(),
display: Default::default(),
docks,
};
cx.background()
.spawn(persistence::DB.save_workspace(serialized_workspace))
.detach();
}
}
}
pub(crate) fn load_workspace(
workspace: WeakViewHandle<Workspace>,
serialized_workspace: SerializedWorkspace,
paths_to_open: Vec<Option<ProjectPath>>,
cx: &mut AppContext,
) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
cx.spawn(|mut cx| async move {
let result = async_iife! {{
let (project, old_center_pane) =
workspace.read_with(&cx, |workspace, _| {
(
workspace.project().clone(),
workspace.last_active_center_pane.clone(),
)
})?;
let mut center_items = None;
let mut center_group = None;
// Traverse the splits tree and add to things
if let Some((group, active_pane, items)) = serialized_workspace
.center_group
.deserialize(&project, serialized_workspace.id, &workspace, &mut cx)
.await {
center_items = Some(items);
center_group = Some((group, active_pane))
}
let resulting_list = cx.read(|cx| {
let mut opened_items = center_items
.unwrap_or_default()
.into_iter()
.filter_map(|item| {
let item = item?;
let project_path = item.project_path(cx)?;
Some((project_path, item))
})
.collect::<HashMap<_, _>>();
paths_to_open
.into_iter()
.map(|path_to_open| {
path_to_open.map(|path_to_open| {
Ok(opened_items.remove(&path_to_open))
})
.transpose()
.map(|item| item.flatten())
.transpose()
})
.collect::<Vec<_>>()
});
// Remove old panes from workspace panes list
workspace.update(&mut cx, |workspace, cx| {
if let Some((center_group, active_pane)) = center_group {
workspace.remove_panes(workspace.center.root.clone(), cx);
// Swap workspace center group
workspace.center = PaneGroup::with_root(center_group);
// Change the focus to the workspace first so that we retrigger focus in on the pane.
cx.focus_self();
if let Some(active_pane) = active_pane {
cx.focus(&active_pane);
} else {
cx.focus(workspace.panes.last().unwrap());
}
} else {
let old_center_handle = old_center_pane.and_then(|weak| weak.upgrade(cx));
if let Some(old_center_handle) = old_center_handle {
cx.focus(&old_center_handle)
} else {
cx.focus_self()
}
}
let docks = serialized_workspace.docks;
workspace.left_dock.update(cx, |dock, cx| {
dock.set_open(docks.left.visible, cx);
if let Some(active_panel) = docks.left.active_panel {
if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
dock.activate_panel(ix, cx);
}
}
dock.active_panel()
.map(|panel| {
panel.set_zoomed(docks.left.zoom, cx)
});
if docks.left.visible && docks.left.zoom {
cx.focus_self()
}
});
// TODO: I think the bug is that setting zoom or active undoes the bottom zoom or something
workspace.right_dock.update(cx, |dock, cx| {
dock.set_open(docks.right.visible, cx);
if let Some(active_panel) = docks.right.active_panel {
if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
dock.activate_panel(ix, cx);
}
}
dock.active_panel()
.map(|panel| {
panel.set_zoomed(docks.right.zoom, cx)
});
if docks.right.visible && docks.right.zoom {
cx.focus_self()
}
});
workspace.bottom_dock.update(cx, |dock, cx| {
dock.set_open(docks.bottom.visible, cx);
if let Some(active_panel) = docks.bottom.active_panel {
if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
dock.activate_panel(ix, cx);
}
}
dock.active_panel()
.map(|panel| {
panel.set_zoomed(docks.bottom.zoom, cx)
});
if docks.bottom.visible && docks.bottom.zoom {
cx.focus_self()
}
});
cx.notify();
})?;
// Serialize ourself to make sure our timestamps and any pane / item changes are replicated
workspace.read_with(&cx, |workspace, cx| workspace.serialize_workspace(cx))?;
Ok::<_, anyhow::Error>(resulting_list)
}};
result.await.unwrap_or_default()
})
}
#[cfg(any(test, feature = "test-support"))]
pub fn test_new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
let app_state = Arc::new(AppState {
languages: project.read(cx).languages().clone(),
client: project.read(cx).client(),
user_store: project.read(cx).user_store(),
fs: project.read(cx).fs().clone(),
build_window_options: |_, _, _| Default::default(),
initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
background_actions: || &[],
});
Self::new(0, project, app_state, cx)
}
fn render_dock(&self, position: DockPosition, cx: &WindowContext) -> Option<AnyElement<Self>> {
let dock = match position {
DockPosition::Left => &self.left_dock,
DockPosition::Right => &self.right_dock,
DockPosition::Bottom => &self.bottom_dock,
};
let active_panel = dock.read(cx).visible_panel()?;
let element = if Some(active_panel.id()) == self.zoomed.as_ref().map(|zoomed| zoomed.id()) {
dock.read(cx).render_placeholder(cx)
} else {
ChildView::new(dock, cx).into_any()
};
Some(
element
.constrained()
.dynamically(move |constraint, _, cx| match position {
DockPosition::Left | DockPosition::Right => SizeConstraint::new(
Vector2F::new(20., constraint.min.y()),
Vector2F::new(cx.window_size().x() * 0.8, constraint.max.y()),
),
DockPosition::Bottom => SizeConstraint::new(
Vector2F::new(constraint.min.x(), 20.),
Vector2F::new(constraint.max.x(), cx.window_size().y() * 0.8),
),
})
.into_any(),
)
}
}
fn window_bounds_env_override(cx: &AsyncAppContext) -> Option<WindowBounds> {
ZED_WINDOW_POSITION
.zip(*ZED_WINDOW_SIZE)
.map(|(position, size)| {
WindowBounds::Fixed(RectF::new(
cx.platform().screens()[0].bounds().origin() + position,
size,
))
})
}
async fn open_items(
serialized_workspace: Option<SerializedWorkspace>,
workspace: &WeakViewHandle<Workspace>,
mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
app_state: Arc<AppState>,
mut cx: AsyncAppContext,
) -> Vec<Option<anyhow::Result<Box<dyn ItemHandle>>>> {
let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
if let Some(serialized_workspace) = serialized_workspace {
let workspace = workspace.clone();
let restored_items = cx
.update(|cx| {
Workspace::load_workspace(
workspace,
serialized_workspace,
project_paths_to_open
.iter()
.map(|(_, project_path)| project_path)
.cloned()
.collect(),
cx,
)
})
.await;
let restored_project_paths = cx.read(|cx| {
restored_items
.iter()
.filter_map(|item| item.as_ref()?.as_ref().ok()?.project_path(cx))
.collect::<HashSet<_>>()
});
opened_items = restored_items;
project_paths_to_open
.iter_mut()
.for_each(|(_, project_path)| {
if let Some(project_path_to_open) = project_path {
if restored_project_paths.contains(project_path_to_open) {
*project_path = None;
}
}
});
} else {
for _ in 0..project_paths_to_open.len() {
opened_items.push(None);
}
}
assert!(opened_items.len() == project_paths_to_open.len());
let tasks =
project_paths_to_open
.into_iter()
.enumerate()
.map(|(i, (abs_path, project_path))| {
let workspace = workspace.clone();
cx.spawn(|mut cx| {
let fs = app_state.fs.clone();
async move {
let file_project_path = project_path?;
if fs.is_file(&abs_path).await {
Some((
i,
workspace
.update(&mut cx, |workspace, cx| {
workspace.open_path(file_project_path, None, true, cx)
})
.log_err()?
.await,
))
} else {
None
}
}
})
});
for maybe_opened_path in futures::future::join_all(tasks.into_iter())
.await
.into_iter()
{
if let Some((i, path_open_result)) = maybe_opened_path {
opened_items[i] = Some(path_open_result);
}
}
opened_items
}
fn notify_of_new_dock(workspace: &WeakViewHandle<Workspace>, cx: &mut AsyncAppContext) {
const NEW_PANEL_BLOG_POST: &str = "https://zed.dev/blog/new-panel-system";
const NEW_DOCK_HINT_KEY: &str = "show_new_dock_key";
const MESSAGE_ID: usize = 2;
if workspace
.read_with(cx, |workspace, cx| {
workspace.has_shown_notification_once::<MessageNotification>(MESSAGE_ID, cx)
})
.unwrap_or(false)
{
return;
}
if db::kvp::KEY_VALUE_STORE
.read_kvp(NEW_DOCK_HINT_KEY)
.ok()
.flatten()
.is_some()
{
if !workspace
.read_with(cx, |workspace, cx| {
workspace.has_shown_notification_once::<MessageNotification>(MESSAGE_ID, cx)
})
.unwrap_or(false)
{
cx.update(|cx| {
cx.update_global::<NotificationTracker, _, _>(|tracker, _| {
let entry = tracker
.entry(TypeId::of::<MessageNotification>())
.or_default();
if !entry.contains(&MESSAGE_ID) {
entry.push(MESSAGE_ID);
}
});
});
}
return;
}
cx.spawn(|_| async move {
db::kvp::KEY_VALUE_STORE
.write_kvp(NEW_DOCK_HINT_KEY.to_string(), "seen".to_string())
.await
.ok();
})
.detach();
workspace
.update(cx, |workspace, cx| {
workspace.show_notification_once(2, cx, |cx| {
cx.add_view(|_| {
MessageNotification::new_element(|text, _| {
Text::new(
"Looking for the dock? Try ctrl-`!\nshift-escape now zooms your pane.",
text,
)
.with_custom_runs(vec![26..32, 34..46], |_, bounds, scene, cx| {
let code_span_background_color = settings::get::<ThemeSettings>(cx)
.theme
.editor
.document_highlight_read_background;
scene.push_quad(gpui::Quad {
bounds,
background: Some(code_span_background_color),
border: Default::default(),
corner_radius: 2.0,
})
})
.into_any()
})
.with_click_message("Read more about the new panel system")
.on_click(|cx| cx.platform().open_url(NEW_PANEL_BLOG_POST))
})
})
})
.ok();
}
fn notify_if_database_failed(workspace: &WeakViewHandle<Workspace>, cx: &mut AsyncAppContext) {
const REPORT_ISSUE_URL: &str ="https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";
workspace
.update(cx, |workspace, cx| {
if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
workspace.show_notification_once(0, cx, |cx| {
cx.add_view(|_| {
MessageNotification::new("Failed to load the database file.")
.with_click_message("Click to let us know about this error")
.on_click(|cx| cx.platform().open_url(REPORT_ISSUE_URL))
})
});
}
})
.log_err();
}
impl Entity for Workspace {
type Event = Event;
}
impl View for Workspace {
fn ui_name() -> &'static str {
"Workspace"
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let theme = theme::current(cx).clone();
Stack::new()
.with_child(
Flex::column()
.with_child(self.render_titlebar(&theme, cx))
.with_child(
Stack::new()
.with_child({
let project = self.project.clone();
Flex::row()
.with_children(self.render_dock(DockPosition::Left, cx))
.with_child(
Flex::column()
.with_child(
FlexItem::new(
self.center.render(
&project,
&theme,
&self.follower_states_by_leader,
self.active_call(),
self.active_pane(),
self.zoomed
.as_ref()
.and_then(|zoomed| zoomed.upgrade(cx))
.as_ref(),
&self.app_state,
cx,
),
)
.flex(1., true),
)
.with_children(
self.render_dock(DockPosition::Bottom, cx),
)
.flex(1., true),
)
.with_children(self.render_dock(DockPosition::Right, cx))
})
.with_child(Overlay::new(
Stack::new()
.with_children(self.zoomed.as_ref().and_then(|zoomed| {
enum ZoomBackground {}
let zoomed = zoomed.upgrade(cx)?;
let mut foreground_style =
theme.workspace.zoomed_pane_foreground;
if let Some(zoomed_dock_position) = self.zoomed_position {
foreground_style =
theme.workspace.zoomed_panel_foreground;
let margin = foreground_style.margin.top;
let border = foreground_style.border.top;
// Only include a margin and border on the opposite side.
foreground_style.margin.top = 0.;
foreground_style.margin.left = 0.;
foreground_style.margin.bottom = 0.;
foreground_style.margin.right = 0.;
foreground_style.border.top = false;
foreground_style.border.left = false;
foreground_style.border.bottom = false;
foreground_style.border.right = false;
match zoomed_dock_position {
DockPosition::Left => {
foreground_style.margin.right = margin;
foreground_style.border.right = border;
}
DockPosition::Right => {
foreground_style.margin.left = margin;
foreground_style.border.left = border;
}
DockPosition::Bottom => {
foreground_style.margin.top = margin;
foreground_style.border.top = border;
}
}
}
Some(
ChildView::new(&zoomed, cx)
.contained()
.with_style(foreground_style)
.aligned()
.contained()
.with_style(theme.workspace.zoomed_background)
.mouse::<ZoomBackground>(0)
.capture_all()
.on_down(
MouseButton::Left,
|_, this: &mut Self, cx| {
this.zoom_out(cx);
},
),
)
}))
.with_children(self.modal.as_ref().map(|modal| {
ChildView::new(modal.view.as_any(), cx)
.contained()
.with_style(theme.workspace.modal)
.aligned()
.top()
}))
.with_children(self.render_notifications(&theme.workspace, cx)),
))
.flex(1.0, true),
)
.with_child(ChildView::new(&self.status_bar, cx))
.contained()
.with_background_color(theme.workspace.background),
)
.with_children(DragAndDrop::render(cx))
.with_children(self.render_disconnected_overlay(cx))
.into_any_named("workspace")
}
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
if cx.is_self_focused() {
cx.focus(&self.active_pane);
}
}
}
impl ViewId {
pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
Ok(Self {
creator: message
.creator
.ok_or_else(|| anyhow!("creator is missing"))?,
id: message.id,
})
}
pub(crate) fn to_proto(&self) -> proto::ViewId {
proto::ViewId {
creator: Some(self.creator),
id: self.id,
}
}
}
pub trait WorkspaceHandle {
fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
}
impl WorkspaceHandle for ViewHandle<Workspace> {
fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
self.read(cx)
.worktrees(cx)
.flat_map(|worktree| {
let worktree_id = worktree.read(cx).id();
worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
worktree_id,
path: f.path.clone(),
})
})
.collect::<Vec<_>>()
}
}
impl std::fmt::Debug for OpenPaths {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OpenPaths")
.field("paths", &self.paths)
.finish()
}
}
pub struct WorkspaceCreated(pub WeakViewHandle<Workspace>);
pub fn activate_workspace_for_project(
cx: &mut AsyncAppContext,
predicate: impl Fn(&mut Project, &mut ModelContext<Project>) -> bool,
) -> Option<WeakViewHandle<Workspace>> {
for window_id in cx.window_ids() {
let handle = cx
.update_window(window_id, |cx| {
if let Some(workspace_handle) = cx.root_view().clone().downcast::<Workspace>() {
let project = workspace_handle.read(cx).project.clone();
if project.update(cx, &predicate) {
cx.activate_window();
return Some(workspace_handle.clone());
}
}
None
})
.flatten();
if let Some(handle) = handle {
return Some(handle.downgrade());
}
}
None
}
pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
DB.last_workspace().await.log_err().flatten()
}
#[allow(clippy::type_complexity)]
pub fn open_paths(
abs_paths: &[PathBuf],
app_state: &Arc<AppState>,
requesting_window_id: Option<usize>,
cx: &mut AppContext,
) -> Task<
Result<(
WeakViewHandle<Workspace>,
Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
)>,
> {
let app_state = app_state.clone();
let abs_paths = abs_paths.to_vec();
cx.spawn(|mut cx| async move {
// Open paths in existing workspace if possible
let existing = activate_workspace_for_project(&mut cx, |project, cx| {
project.contains_paths(&abs_paths, cx)
});
if let Some(existing) = existing {
Ok((
existing.clone(),
existing
.update(&mut cx, |workspace, cx| {
workspace.open_paths(abs_paths, true, cx)
})?
.await,
))
} else {
Ok(cx
.update(|cx| {
Workspace::new_local(abs_paths, app_state.clone(), requesting_window_id, cx)
})
.await)
}
})
}
pub fn open_new(
app_state: &Arc<AppState>,
cx: &mut AppContext,
init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static,
) -> Task<()> {
let task = Workspace::new_local(Vec::new(), app_state.clone(), None, cx);
cx.spawn(|mut cx| async move {
let (workspace, opened_paths) = task.await;
workspace
.update(&mut cx, |workspace, cx| {
if opened_paths.is_empty() {
init(workspace, cx)
}
})
.log_err();
})
}
pub fn create_and_open_local_file(
path: &'static Path,
cx: &mut ViewContext<Workspace>,
default_content: impl 'static + Send + FnOnce() -> Rope,
) -> Task<Result<Box<dyn ItemHandle>>> {
cx.spawn(|workspace, mut cx| async move {
let fs = workspace.read_with(&cx, |workspace, _| workspace.app_state().fs.clone())?;
if !fs.is_file(path).await {
fs.create_file(path, Default::default()).await?;
fs.save(path, &default_content(), Default::default())
.await?;
}
let mut items = workspace
.update(&mut cx, |workspace, cx| {
workspace.with_local_workspace(cx, |workspace, cx| {
workspace.open_paths(vec![path.to_path_buf()], false, cx)
})
})?
.await?
.await;
let item = items.pop().flatten();
item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
})
}
pub fn join_remote_project(
project_id: u64,
follow_user_id: u64,
app_state: Arc<AppState>,
cx: &mut AppContext,
) -> Task<Result<()>> {
cx.spawn(|mut cx| async move {
let existing_workspace = cx
.window_ids()
.into_iter()
.filter_map(|window_id| cx.root_view(window_id)?.clone().downcast::<Workspace>())
.find(|workspace| {
cx.read_window(workspace.window_id(), |cx| {
workspace.read(cx).project().read(cx).remote_id() == Some(project_id)
})
.unwrap_or(false)
});
let workspace = if let Some(existing_workspace) = existing_workspace {
existing_workspace.downgrade()
} else {
let active_call = cx.read(ActiveCall::global);
let room = active_call
.read_with(&cx, |call, _| call.room().cloned())
.ok_or_else(|| anyhow!("not in a call"))?;
let project = room
.update(&mut cx, |room, cx| {
room.join_project(
project_id,
app_state.languages.clone(),
app_state.fs.clone(),
cx,
)
})
.await?;
let window_bounds_override = window_bounds_env_override(&cx);
let (_, workspace) = cx.add_window(
(app_state.build_window_options)(
window_bounds_override,
None,
cx.platform().as_ref(),
),
|cx| Workspace::new(0, project, app_state.clone(), cx),
);
(app_state.initialize_workspace)(
workspace.downgrade(),
false,
app_state.clone(),
cx.clone(),
)
.await
.log_err();
workspace.downgrade()
};
cx.activate_window(workspace.window_id());
cx.platform().activate(true);
workspace.update(&mut cx, |workspace, cx| {
if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
let follow_peer_id = room
.read(cx)
.remote_participants()
.iter()
.find(|(_, participant)| participant.user.id == follow_user_id)
.map(|(_, p)| p.peer_id)
.or_else(|| {
// If we couldn't follow the given user, follow the host instead.
let collaborator = workspace
.project()
.read(cx)
.collaborators()
.values()
.find(|collaborator| collaborator.replica_id == 0)?;
Some(collaborator.peer_id)
});
if let Some(follow_peer_id) = follow_peer_id {
if !workspace.is_being_followed(follow_peer_id) {
workspace
.toggle_follow(follow_peer_id, cx)
.map(|follow| follow.detach_and_log_err(cx));
}
}
}
})?;
anyhow::Ok(())
})
}
pub fn restart(_: &Restart, cx: &mut AppContext) {
let should_confirm = settings::get::<WorkspaceSettings>(cx).confirm_quit;
cx.spawn(|mut cx| async move {
let mut workspaces = cx
.window_ids()
.into_iter()
.filter_map(|window_id| {
Some(
cx.root_view(window_id)?
.clone()
.downcast::<Workspace>()?
.downgrade(),
)
})
.collect::<Vec<_>>();
// If multiple windows have unsaved changes, and need a save prompt,
// prompt in the active window before switching to a different window.
workspaces.sort_by_key(|workspace| !cx.window_is_active(workspace.window_id()));
if let (true, Some(workspace)) = (should_confirm, workspaces.first()) {
let answer = cx.prompt(
workspace.window_id(),
PromptLevel::Info,
"Are you sure you want to restart?",
&["Restart", "Cancel"],
);
if let Some(mut answer) = answer {
let answer = answer.next().await;
if answer != Some(0) {
return Ok(());
}
}
}
// If the user cancels any save prompt, then keep the app open.
for workspace in workspaces {
if !workspace
.update(&mut cx, |workspace, cx| {
workspace.prepare_to_close(true, cx)
})?
.await?
{
return Ok(());
}
}
cx.platform().restart();
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
fn parse_pixel_position_env_var(value: &str) -> Option<Vector2F> {
let mut parts = value.split(',');
let width: usize = parts.next()?.parse().ok()?;
let height: usize = parts.next()?.parse().ok()?;
Some(vec2f(width as f32, height as f32))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
dock::test::{TestPanel, TestPanelEvent},
item::test::{TestItem, TestItemEvent, TestProjectItem},
};
use fs::FakeFs;
use gpui::{executor::Deterministic, test::EmptyView, TestAppContext};
use project::{Project, ProjectEntryId};
use serde_json::json;
use settings::SettingsStore;
use std::{cell::RefCell, rc::Rc};
#[gpui::test]
async fn test_tab_disambiguation(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, [], cx).await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
// Adding an item with no ambiguity renders the tab without detail.
let item1 = cx.add_view(window_id, |_| {
let mut item = TestItem::new();
item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
item
});
workspace.update(cx, |workspace, cx| {
workspace.add_item(Box::new(item1.clone()), cx);
});
item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), None));
// Adding an item that creates ambiguity increases the level of detail on
// both tabs.
let item2 = cx.add_view(window_id, |_| {
let mut item = TestItem::new();
item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
item
});
workspace.update(cx, |workspace, cx| {
workspace.add_item(Box::new(item2.clone()), cx);
});
item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
// Adding an item that creates ambiguity increases the level of detail only
// on the ambiguous tabs. In this case, the ambiguity can't be resolved so
// we stop at the highest detail available.
let item3 = cx.add_view(window_id, |_| {
let mut item = TestItem::new();
item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
item
});
workspace.update(cx, |workspace, cx| {
workspace.add_item(Box::new(item3.clone()), cx);
});
item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
item3.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
}
#[gpui::test]
async fn test_tracking_active_path(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.background());
fs.insert_tree(
"/root1",
json!({
"one.txt": "",
"two.txt": "",
}),
)
.await;
fs.insert_tree(
"/root2",
json!({
"three.txt": "",
}),
)
.await;
let project = Project::test(fs, ["root1".as_ref()], cx).await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
let worktree_id = project.read_with(cx, |project, cx| {
project.worktrees(cx).next().unwrap().read(cx).id()
});
let item1 = cx.add_view(window_id, |cx| {
TestItem::new().with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
});
let item2 = cx.add_view(window_id, |cx| {
TestItem::new().with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
});
// Add an item to an empty pane
workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx));
project.read_with(cx, |project, cx| {
assert_eq!(
project.active_entry(),
project
.entry_for_path(&(worktree_id, "one.txt").into(), cx)
.map(|e| e.id)
);
});
assert_eq!(
cx.current_window_title(window_id).as_deref(),
Some("one.txt — root1")
);
// Add a second item to a non-empty pane
workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
assert_eq!(
cx.current_window_title(window_id).as_deref(),
Some("two.txt — root1")
);
project.read_with(cx, |project, cx| {
assert_eq!(
project.active_entry(),
project
.entry_for_path(&(worktree_id, "two.txt").into(), cx)
.map(|e| e.id)
);
});
// Close the active item
pane.update(cx, |pane, cx| {
pane.close_active_item(&Default::default(), cx).unwrap()
})
.await
.unwrap();
assert_eq!(
cx.current_window_title(window_id).as_deref(),
Some("one.txt — root1")
);
project.read_with(cx, |project, cx| {
assert_eq!(
project.active_entry(),
project
.entry_for_path(&(worktree_id, "one.txt").into(), cx)
.map(|e| e.id)
);
});
// Add a project folder
project
.update(cx, |project, cx| {
project.find_or_create_local_worktree("/root2", true, cx)
})
.await
.unwrap();
assert_eq!(
cx.current_window_title(window_id).as_deref(),
Some("one.txt — root1, root2")
);
// Remove a project folder
project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
assert_eq!(
cx.current_window_title(window_id).as_deref(),
Some("one.txt — root2")
);
}
#[gpui::test]
async fn test_close_window(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.background());
fs.insert_tree("/root", json!({ "one": "" })).await;
let project = Project::test(fs, ["root".as_ref()], cx).await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
// When there are no dirty items, there's nothing to do.
let item1 = cx.add_view(window_id, |_| TestItem::new());
workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
assert!(task.await.unwrap());
// When there are dirty untitled items, prompt to save each one. If the user
// cancels any prompt, then abort.
let item2 = cx.add_view(window_id, |_| TestItem::new().with_dirty(true));
let item3 = cx.add_view(window_id, |cx| {
TestItem::new()
.with_dirty(true)
.with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
});
workspace.update(cx, |w, cx| {
w.add_item(Box::new(item2.clone()), cx);
w.add_item(Box::new(item3.clone()), cx);
});
let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
cx.foreground().run_until_parked();
cx.simulate_prompt_answer(window_id, 2 /* cancel */);
cx.foreground().run_until_parked();
assert!(!cx.has_pending_prompt(window_id));
assert!(!task.await.unwrap());
}
#[gpui::test]
async fn test_close_pane_items(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, None, cx).await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
let item1 = cx.add_view(window_id, |cx| {
TestItem::new()
.with_dirty(true)
.with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
});
let item2 = cx.add_view(window_id, |cx| {
TestItem::new()
.with_dirty(true)
.with_conflict(true)
.with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
});
let item3 = cx.add_view(window_id, |cx| {
TestItem::new()
.with_dirty(true)
.with_conflict(true)
.with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
});
let item4 = cx.add_view(window_id, |cx| {
TestItem::new()
.with_dirty(true)
.with_project_items(&[TestProjectItem::new_untitled(cx)])
});
let pane = workspace.update(cx, |workspace, cx| {
workspace.add_item(Box::new(item1.clone()), cx);
workspace.add_item(Box::new(item2.clone()), cx);
workspace.add_item(Box::new(item3.clone()), cx);
workspace.add_item(Box::new(item4.clone()), cx);
workspace.active_pane().clone()
});
let close_items = pane.update(cx, |pane, cx| {
pane.activate_item(1, true, true, cx);
assert_eq!(pane.active_item().unwrap().id(), item2.id());
let item1_id = item1.id();
let item3_id = item3.id();
let item4_id = item4.id();
pane.close_items(cx, move |id| [item1_id, item3_id, item4_id].contains(&id))
});
cx.foreground().run_until_parked();
// There's a prompt to save item 1.
pane.read_with(cx, |pane, _| {
assert_eq!(pane.items_len(), 4);
assert_eq!(pane.active_item().unwrap().id(), item1.id());
});
assert!(cx.has_pending_prompt(window_id));
// Confirm saving item 1.
cx.simulate_prompt_answer(window_id, 0);
cx.foreground().run_until_parked();
// Item 1 is saved. There's a prompt to save item 3.
pane.read_with(cx, |pane, cx| {
assert_eq!(item1.read(cx).save_count, 1);
assert_eq!(item1.read(cx).save_as_count, 0);
assert_eq!(item1.read(cx).reload_count, 0);
assert_eq!(pane.items_len(), 3);
assert_eq!(pane.active_item().unwrap().id(), item3.id());
});
assert!(cx.has_pending_prompt(window_id));
// Cancel saving item 3.
cx.simulate_prompt_answer(window_id, 1);
cx.foreground().run_until_parked();
// Item 3 is reloaded. There's a prompt to save item 4.
pane.read_with(cx, |pane, cx| {
assert_eq!(item3.read(cx).save_count, 0);
assert_eq!(item3.read(cx).save_as_count, 0);
assert_eq!(item3.read(cx).reload_count, 1);
assert_eq!(pane.items_len(), 2);
assert_eq!(pane.active_item().unwrap().id(), item4.id());
});
assert!(cx.has_pending_prompt(window_id));
// Confirm saving item 4.
cx.simulate_prompt_answer(window_id, 0);
cx.foreground().run_until_parked();
// There's a prompt for a path for item 4.
cx.simulate_new_path_selection(|_| Some(Default::default()));
close_items.await.unwrap();
// The requested items are closed.
pane.read_with(cx, |pane, cx| {
assert_eq!(item4.read(cx).save_count, 0);
assert_eq!(item4.read(cx).save_as_count, 1);
assert_eq!(item4.read(cx).reload_count, 0);
assert_eq!(pane.items_len(), 1);
assert_eq!(pane.active_item().unwrap().id(), item2.id());
});
}
#[gpui::test]
async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, [], cx).await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
// Create several workspace items with single project entries, and two
// workspace items with multiple project entries.
let single_entry_items = (0..=4)
.map(|project_entry_id| {
cx.add_view(window_id, |cx| {
TestItem::new()
.with_dirty(true)
.with_project_items(&[TestProjectItem::new(
project_entry_id,
&format!("{project_entry_id}.txt"),
cx,
)])
})
})
.collect::<Vec<_>>();
let item_2_3 = cx.add_view(window_id, |cx| {
TestItem::new()
.with_dirty(true)
.with_singleton(false)
.with_project_items(&[
single_entry_items[2].read(cx).project_items[0].clone(),
single_entry_items[3].read(cx).project_items[0].clone(),
])
});
let item_3_4 = cx.add_view(window_id, |cx| {
TestItem::new()
.with_dirty(true)
.with_singleton(false)
.with_project_items(&[
single_entry_items[3].read(cx).project_items[0].clone(),
single_entry_items[4].read(cx).project_items[0].clone(),
])
});
// Create two panes that contain the following project entries:
// left pane:
// multi-entry items: (2, 3)
// single-entry items: 0, 1, 2, 3, 4
// right pane:
// single-entry items: 1
// multi-entry items: (3, 4)
let left_pane = workspace.update(cx, |workspace, cx| {
let left_pane = workspace.active_pane().clone();
workspace.add_item(Box::new(item_2_3.clone()), cx);
for item in single_entry_items {
workspace.add_item(Box::new(item), cx);
}
left_pane.update(cx, |pane, cx| {
pane.activate_item(2, true, true, cx);
});
workspace
.split_and_clone(left_pane.clone(), SplitDirection::Right, cx)
.unwrap();
left_pane
});
//Need to cause an effect flush in order to respect new focus
workspace.update(cx, |workspace, cx| {
workspace.add_item(Box::new(item_3_4.clone()), cx);
cx.focus(&left_pane);
});
// When closing all of the items in the left pane, we should be prompted twice:
// once for project entry 0, and once for project entry 2. After those two
// prompts, the task should complete.
let close = left_pane.update(cx, |pane, cx| pane.close_items(cx, |_| true));
cx.foreground().run_until_parked();
left_pane.read_with(cx, |pane, cx| {
assert_eq!(
pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
&[ProjectEntryId::from_proto(0)]
);
});
cx.simulate_prompt_answer(window_id, 0);
cx.foreground().run_until_parked();
left_pane.read_with(cx, |pane, cx| {
assert_eq!(
pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
&[ProjectEntryId::from_proto(2)]
);
});
cx.simulate_prompt_answer(window_id, 0);
cx.foreground().run_until_parked();
close.await.unwrap();
left_pane.read_with(cx, |pane, _| {
assert_eq!(pane.items_len(), 0);
});
}
#[gpui::test]
async fn test_autosave(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, [], cx).await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
let item = cx.add_view(window_id, |cx| {
TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
});
let item_id = item.id();
workspace.update(cx, |workspace, cx| {
workspace.add_item(Box::new(item.clone()), cx);
});
// Autosave on window change.
item.update(cx, |item, cx| {
cx.update_global(|settings: &mut SettingsStore, cx| {
settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
settings.autosave = Some(AutosaveSetting::OnWindowChange);
})
});
item.is_dirty = true;
});
// Deactivating the window saves the file.
cx.simulate_window_activation(None);
deterministic.run_until_parked();
item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
// Autosave on focus change.
item.update(cx, |item, cx| {
cx.focus_self();
cx.update_global(|settings: &mut SettingsStore, cx| {
settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
settings.autosave = Some(AutosaveSetting::OnFocusChange);
})
});
item.is_dirty = true;
});
// Blurring the item saves the file.
item.update(cx, |_, cx| cx.blur());
deterministic.run_until_parked();
item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
// Deactivating the window still saves the file.
cx.simulate_window_activation(Some(window_id));
item.update(cx, |item, cx| {
cx.focus_self();
item.is_dirty = true;
});
cx.simulate_window_activation(None);
deterministic.run_until_parked();
item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
// Autosave after delay.
item.update(cx, |item, cx| {
cx.update_global(|settings: &mut SettingsStore, cx| {
settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
})
});
item.is_dirty = true;
cx.emit(TestItemEvent::Edit);
});
// Delay hasn't fully expired, so the file is still dirty and unsaved.
deterministic.advance_clock(Duration::from_millis(250));
item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
// After delay expires, the file is saved.
deterministic.advance_clock(Duration::from_millis(250));
item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
// Autosave on focus change, ensuring closing the tab counts as such.
item.update(cx, |item, cx| {
cx.update_global(|settings: &mut SettingsStore, cx| {
settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
settings.autosave = Some(AutosaveSetting::OnFocusChange);
})
});
item.is_dirty = true;
});
pane.update(cx, |pane, cx| pane.close_items(cx, move |id| id == item_id))
.await
.unwrap();
assert!(!cx.has_pending_prompt(window_id));
item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
// Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
workspace.update(cx, |workspace, cx| {
workspace.add_item(Box::new(item.clone()), cx);
});
item.update(cx, |item, cx| {
item.project_items[0].update(cx, |item, _| {
item.entry_id = None;
});
item.is_dirty = true;
cx.blur();
});
deterministic.run_until_parked();
item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
// Ensure autosave is prevented for deleted files also when closing the buffer.
let _close_items =
pane.update(cx, |pane, cx| pane.close_items(cx, move |id| id == item_id));
deterministic.run_until_parked();
assert!(cx.has_pending_prompt(window_id));
item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
}
#[gpui::test]
async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, [], cx).await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
let item = cx.add_view(window_id, |cx| {
TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
});
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone());
let toolbar_notify_count = Rc::new(RefCell::new(0));
workspace.update(cx, |workspace, cx| {
workspace.add_item(Box::new(item.clone()), cx);
let toolbar_notification_count = toolbar_notify_count.clone();
cx.observe(&toolbar, move |_, _, _| {
*toolbar_notification_count.borrow_mut() += 1
})
.detach();
});
pane.read_with(cx, |pane, _| {
assert!(!pane.can_navigate_backward());
assert!(!pane.can_navigate_forward());
});
item.update(cx, |item, cx| {
item.set_state("one".to_string(), cx);
});
// Toolbar must be notified to re-render the navigation buttons
assert_eq!(*toolbar_notify_count.borrow(), 1);
pane.read_with(cx, |pane, _| {
assert!(pane.can_navigate_backward());
assert!(!pane.can_navigate_forward());
});
workspace
.update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
.await
.unwrap();
assert_eq!(*toolbar_notify_count.borrow(), 3);
pane.read_with(cx, |pane, _| {
assert!(!pane.can_navigate_backward());
assert!(pane.can_navigate_forward());
});
}
#[gpui::test]
async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, [], cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
let panel = workspace.update(cx, |workspace, cx| {
let panel = cx.add_view(|_| TestPanel::new(DockPosition::Right));
workspace.add_panel(panel.clone(), cx);
workspace
.right_dock()
.update(cx, |right_dock, cx| right_dock.set_open(true, cx));
panel
});
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
pane.update(cx, |pane, cx| {
let item = cx.add_view(|_| TestItem::new());
pane.add_item(Box::new(item), true, true, None, cx);
});
// Transfer focus from center to panel
workspace.update(cx, |workspace, cx| {
workspace.toggle_panel_focus::<TestPanel>(cx);
});
workspace.read_with(cx, |workspace, cx| {
assert!(workspace.right_dock().read(cx).is_open());
assert!(!panel.is_zoomed(cx));
assert!(panel.has_focus(cx));
});
// Transfer focus from panel to center
workspace.update(cx, |workspace, cx| {
workspace.toggle_panel_focus::<TestPanel>(cx);
});
workspace.read_with(cx, |workspace, cx| {
assert!(workspace.right_dock().read(cx).is_open());
assert!(!panel.is_zoomed(cx));
assert!(!panel.has_focus(cx));
});
// Close the dock
workspace.update(cx, |workspace, cx| {
workspace.toggle_dock(DockPosition::Right, cx);
});
workspace.read_with(cx, |workspace, cx| {
assert!(!workspace.right_dock().read(cx).is_open());
assert!(!panel.is_zoomed(cx));
assert!(!panel.has_focus(cx));
});
// Open the dock
workspace.update(cx, |workspace, cx| {
workspace.toggle_dock(DockPosition::Right, cx);
});
workspace.read_with(cx, |workspace, cx| {
assert!(workspace.right_dock().read(cx).is_open());
assert!(!panel.is_zoomed(cx));
assert!(panel.has_focus(cx));
});
// Focus and zoom panel
panel.update(cx, |panel, cx| {
cx.focus_self();
panel.set_zoomed(true, cx)
});
workspace.read_with(cx, |workspace, cx| {
assert!(workspace.right_dock().read(cx).is_open());
assert!(panel.is_zoomed(cx));
assert!(panel.has_focus(cx));
});
// Transfer focus to the center closes the dock
workspace.update(cx, |workspace, cx| {
workspace.toggle_panel_focus::<TestPanel>(cx);
});
workspace.read_with(cx, |workspace, cx| {
assert!(!workspace.right_dock().read(cx).is_open());
assert!(panel.is_zoomed(cx));
assert!(!panel.has_focus(cx));
});
// Transferring focus back to the panel keeps it zoomed
workspace.update(cx, |workspace, cx| {
workspace.toggle_panel_focus::<TestPanel>(cx);
});
workspace.read_with(cx, |workspace, cx| {
assert!(workspace.right_dock().read(cx).is_open());
assert!(panel.is_zoomed(cx));
assert!(panel.has_focus(cx));
});
// Close the dock while it is zoomed
workspace.update(cx, |workspace, cx| {
workspace.toggle_dock(DockPosition::Right, cx)
});
workspace.read_with(cx, |workspace, cx| {
assert!(!workspace.right_dock().read(cx).is_open());
assert!(panel.is_zoomed(cx));
assert!(workspace.zoomed.is_none());
assert!(!panel.has_focus(cx));
});
// Opening the dock, when it's zoomed, retains focus
workspace.update(cx, |workspace, cx| {
workspace.toggle_dock(DockPosition::Right, cx)
});
workspace.read_with(cx, |workspace, cx| {
assert!(workspace.right_dock().read(cx).is_open());
assert!(panel.is_zoomed(cx));
assert!(workspace.zoomed.is_some());
assert!(panel.has_focus(cx));
});
// Unzoom and close the panel, zoom the active pane.
panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
workspace.update(cx, |workspace, cx| {
workspace.toggle_dock(DockPosition::Right, cx)
});
pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
// Opening a dock unzooms the pane.
workspace.update(cx, |workspace, cx| {
workspace.toggle_dock(DockPosition::Right, cx)
});
workspace.read_with(cx, |workspace, cx| {
let pane = pane.read(cx);
assert!(!pane.is_zoomed());
assert!(!pane.has_focus());
assert!(workspace.right_dock().read(cx).is_open());
assert!(workspace.zoomed.is_none());
});
}
#[gpui::test]
async fn test_panels(cx: &mut gpui::TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, [], cx).await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
// Add panel_1 on the left, panel_2 on the right.
let panel_1 = cx.add_view(|_| TestPanel::new(DockPosition::Left));
workspace.add_panel(panel_1.clone(), cx);
workspace
.left_dock()
.update(cx, |left_dock, cx| left_dock.set_open(true, cx));
let panel_2 = cx.add_view(|_| TestPanel::new(DockPosition::Right));
workspace.add_panel(panel_2.clone(), cx);
workspace
.right_dock()
.update(cx, |right_dock, cx| right_dock.set_open(true, cx));
let left_dock = workspace.left_dock();
assert_eq!(
left_dock.read(cx).visible_panel().unwrap().id(),
panel_1.id()
);
assert_eq!(
left_dock.read(cx).active_panel_size(cx).unwrap(),
panel_1.size(cx)
);
left_dock.update(cx, |left_dock, cx| left_dock.resize_active_panel(1337., cx));
assert_eq!(
workspace
.right_dock()
.read(cx)
.visible_panel()
.unwrap()
.id(),
panel_2.id()
);
(panel_1, panel_2)
});
// Move panel_1 to the right
panel_1.update(cx, |panel_1, cx| {
panel_1.set_position(DockPosition::Right, cx)
});
workspace.update(cx, |workspace, cx| {
// Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
// Since it was the only panel on the left, the left dock should now be closed.
assert!(!workspace.left_dock().read(cx).is_open());
assert!(workspace.left_dock().read(cx).visible_panel().is_none());
let right_dock = workspace.right_dock();
assert_eq!(
right_dock.read(cx).visible_panel().unwrap().id(),
panel_1.id()
);
assert_eq!(right_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
// Now we move panel_2 to the left
panel_2.set_position(DockPosition::Left, cx);
});
workspace.update(cx, |workspace, cx| {
// Since panel_2 was not visible on the right, we don't open the left dock.
assert!(!workspace.left_dock().read(cx).is_open());
// And the right dock is unaffected in it's displaying of panel_1
assert!(workspace.right_dock().read(cx).is_open());
assert_eq!(
workspace
.right_dock()
.read(cx)
.visible_panel()
.unwrap()
.id(),
panel_1.id()
);
});
// Move panel_1 back to the left
panel_1.update(cx, |panel_1, cx| {
panel_1.set_position(DockPosition::Left, cx)
});
workspace.update(cx, |workspace, cx| {
// Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
let left_dock = workspace.left_dock();
assert!(left_dock.read(cx).is_open());
assert_eq!(
left_dock.read(cx).visible_panel().unwrap().id(),
panel_1.id()
);
assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
// And right the dock should be closed as it no longer has any panels.
assert!(!workspace.right_dock().read(cx).is_open());
// Now we move panel_1 to the bottom
panel_1.set_position(DockPosition::Bottom, cx);
});
workspace.update(cx, |workspace, cx| {
// Since panel_1 was visible on the left, we close the left dock.
assert!(!workspace.left_dock().read(cx).is_open());
// The bottom dock is sized based on the panel's default size,
// since the panel orientation changed from vertical to horizontal.
let bottom_dock = workspace.bottom_dock();
assert_eq!(
bottom_dock.read(cx).active_panel_size(cx).unwrap(),
panel_1.size(cx),
);
// Close bottom dock and move panel_1 back to the left.
bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
panel_1.set_position(DockPosition::Left, cx);
});
// Emit activated event on panel 1
panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Activated));
// Now the left dock is open and panel_1 is active and focused.
workspace.read_with(cx, |workspace, cx| {
let left_dock = workspace.left_dock();
assert!(left_dock.read(cx).is_open());
assert_eq!(
left_dock.read(cx).visible_panel().unwrap().id(),
panel_1.id()
);
assert!(panel_1.is_focused(cx));
});
// Emit closed event on panel 2, which is not active
panel_2.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed));
// Wo don't close the left dock, because panel_2 wasn't the active panel
workspace.read_with(cx, |workspace, cx| {
let left_dock = workspace.left_dock();
assert!(left_dock.read(cx).is_open());
assert_eq!(
left_dock.read(cx).visible_panel().unwrap().id(),
panel_1.id()
);
});
// Emitting a ZoomIn event shows the panel as zoomed.
panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomIn));
workspace.read_with(cx, |workspace, _| {
assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
});
// Move panel to another dock while it is zoomed
panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
workspace.read_with(cx, |workspace, _| {
assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
});
// If focus is transferred to another view that's not a panel or another pane, we still show
// the panel as zoomed.
let focus_receiver = cx.add_view(window_id, |_| EmptyView);
focus_receiver.update(cx, |_, cx| cx.focus_self());
workspace.read_with(cx, |workspace, _| {
assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
});
// If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
workspace.update(cx, |_, cx| cx.focus_self());
workspace.read_with(cx, |workspace, _| {
assert_eq!(workspace.zoomed, None);
assert_eq!(workspace.zoomed_position, None);
});
// If focus is transferred again to another view that's not a panel or a pane, we won't
// show the panel as zoomed because it wasn't zoomed before.
focus_receiver.update(cx, |_, cx| cx.focus_self());
workspace.read_with(cx, |workspace, _| {
assert_eq!(workspace.zoomed, None);
assert_eq!(workspace.zoomed_position, None);
});
// When focus is transferred back to the panel, it is zoomed again.
panel_1.update(cx, |_, cx| cx.focus_self());
workspace.read_with(cx, |workspace, _| {
assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
});
// Emitting a ZoomOut event unzooms the panel.
panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomOut));
workspace.read_with(cx, |workspace, _| {
assert_eq!(workspace.zoomed, None);
assert_eq!(workspace.zoomed_position, None);
});
// Emit closed event on panel 1, which is active
panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed));
// Now the left dock is closed, because panel_1 was the active panel
workspace.read_with(cx, |workspace, cx| {
let right_dock = workspace.right_dock();
assert!(!right_dock.read(cx).is_open());
});
}
pub fn init_test(cx: &mut TestAppContext) {
cx.foreground().forbid_parking();
cx.update(|cx| {
cx.set_global(SettingsStore::test(cx));
theme::init((), cx);
language::init(cx);
crate::init_settings(cx);
Project::init_settings(cx);
});
}
}