1/// NOTE: Focus only 'takes' after an update has flushed_effects.
2///
3/// This may cause issues when you're trying to write tests that use workspace focus to add items at
4/// specific locations.
5pub mod dock;
6pub mod item;
7pub mod notifications;
8pub mod pane;
9pub mod pane_group;
10mod persistence;
11pub mod searchable;
12pub mod shared_screen;
13pub mod sidebar;
14mod status_bar;
15mod toolbar;
16
17pub use smallvec;
18
19use anyhow::{anyhow, Result};
20use call::ActiveCall;
21use client::{
22 proto::{self, PeerId},
23 Client, TypedEnvelope, UserStore,
24};
25use collections::{hash_map, HashMap, HashSet};
26use dock::{Dock, DockDefaultItemFactory, ToggleDockButton};
27use drag_and_drop::DragAndDrop;
28use fs::{self, Fs};
29use futures::{
30 channel::{mpsc, oneshot},
31 future::try_join_all,
32 FutureExt, StreamExt,
33};
34use gpui::{
35 actions,
36 elements::*,
37 geometry::{
38 rect::RectF,
39 vector::{vec2f, Vector2F},
40 },
41 impl_actions, impl_internal_actions,
42 keymap_matcher::KeymapContext,
43 platform::{CursorStyle, WindowOptions},
44 AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
45 MouseButton, MutableAppContext, PathPromptOptions, Platform, PromptLevel, RenderContext,
46 SizeConstraint, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowBounds,
47};
48use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem};
49use language::LanguageRegistry;
50use std::{
51 any::TypeId,
52 borrow::Cow,
53 cmp, env,
54 future::Future,
55 path::{Path, PathBuf},
56 sync::Arc,
57 time::Duration,
58};
59
60use crate::{
61 notifications::simple_message_notification::{MessageNotification, OsOpen},
62 persistence::model::{SerializedPane, SerializedPaneGroup, SerializedWorkspace},
63};
64use lazy_static::lazy_static;
65use log::{error, warn};
66use notifications::NotificationHandle;
67pub use pane::*;
68pub use pane_group::*;
69use persistence::{model::SerializedItem, DB};
70pub use persistence::{
71 model::{ItemId, WorkspaceLocation},
72 WorkspaceDb, DB as WORKSPACE_DB,
73};
74use postage::prelude::Stream;
75use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
76use serde::Deserialize;
77use settings::{Autosave, DockAnchor, Settings};
78use shared_screen::SharedScreen;
79use sidebar::{Sidebar, SidebarButtons, SidebarSide, ToggleSidebarItem};
80use status_bar::StatusBar;
81pub use status_bar::StatusItemView;
82use theme::{Theme, ThemeRegistry};
83pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
84use util::ResultExt;
85
86lazy_static! {
87 static ref ZED_WINDOW_SIZE: Option<Vector2F> = env::var("ZED_WINDOW_SIZE")
88 .ok()
89 .as_deref()
90 .and_then(parse_pixel_position_env_var);
91 static ref ZED_WINDOW_POSITION: Option<Vector2F> = env::var("ZED_WINDOW_POSITION")
92 .ok()
93 .as_deref()
94 .and_then(parse_pixel_position_env_var);
95}
96
97#[derive(Clone, PartialEq)]
98pub struct RemoveWorktreeFromProject(pub WorktreeId);
99
100actions!(
101 workspace,
102 [
103 Open,
104 NewFile,
105 NewWindow,
106 CloseWindow,
107 AddFolderToProject,
108 Unfollow,
109 Save,
110 SaveAs,
111 SaveAll,
112 ActivatePreviousPane,
113 ActivateNextPane,
114 FollowNextCollaborator,
115 ToggleLeftSidebar,
116 NewTerminal,
117 NewSearch,
118 Feedback,
119 Restart
120 ]
121);
122
123#[derive(Clone, PartialEq)]
124pub struct OpenPaths {
125 pub paths: Vec<PathBuf>,
126}
127
128#[derive(Clone, Deserialize, PartialEq)]
129pub struct ActivatePane(pub usize);
130
131#[derive(Clone, PartialEq)]
132pub struct ToggleFollow(pub PeerId);
133
134#[derive(Clone, PartialEq)]
135pub struct JoinProject {
136 pub project_id: u64,
137 pub follow_user_id: u64,
138}
139
140#[derive(Clone, PartialEq)]
141pub struct OpenSharedScreen {
142 pub peer_id: PeerId,
143}
144
145#[derive(Clone, PartialEq)]
146pub struct SplitWithItem {
147 pane_to_split: WeakViewHandle<Pane>,
148 split_direction: SplitDirection,
149 from: WeakViewHandle<Pane>,
150 item_id_to_move: usize,
151}
152
153#[derive(Clone, PartialEq)]
154pub struct SplitWithProjectEntry {
155 pane_to_split: WeakViewHandle<Pane>,
156 split_direction: SplitDirection,
157 project_entry: ProjectEntryId,
158}
159
160#[derive(Clone, PartialEq)]
161pub struct OpenProjectEntryInPane {
162 pane: WeakViewHandle<Pane>,
163 project_entry: ProjectEntryId,
164}
165
166pub type WorkspaceId = i64;
167
168impl_internal_actions!(
169 workspace,
170 [
171 OpenPaths,
172 ToggleFollow,
173 JoinProject,
174 OpenSharedScreen,
175 RemoveWorktreeFromProject,
176 SplitWithItem,
177 SplitWithProjectEntry,
178 OpenProjectEntryInPane,
179 ]
180);
181impl_actions!(workspace, [ActivatePane]);
182
183pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
184 pane::init(cx);
185 dock::init(cx);
186 notifications::init(cx);
187
188 cx.add_global_action(open);
189 cx.add_global_action({
190 let app_state = Arc::downgrade(&app_state);
191 move |action: &OpenPaths, cx: &mut MutableAppContext| {
192 if let Some(app_state) = app_state.upgrade() {
193 open_paths(&action.paths, &app_state, cx).detach();
194 }
195 }
196 });
197 cx.add_global_action({
198 let app_state = Arc::downgrade(&app_state);
199 move |_: &NewFile, cx: &mut MutableAppContext| {
200 if let Some(app_state) = app_state.upgrade() {
201 open_new(&app_state, cx).detach();
202 }
203 }
204 });
205
206 cx.add_global_action({
207 let app_state = Arc::downgrade(&app_state);
208 move |_: &NewWindow, cx: &mut MutableAppContext| {
209 if let Some(app_state) = app_state.upgrade() {
210 open_new(&app_state, cx).detach();
211 }
212 }
213 });
214
215 cx.add_async_action(Workspace::toggle_follow);
216 cx.add_async_action(Workspace::follow_next_collaborator);
217 cx.add_async_action(Workspace::close);
218 cx.add_global_action(Workspace::close_global);
219 cx.add_async_action(Workspace::save_all);
220 cx.add_action(Workspace::open_shared_screen);
221 cx.add_action(Workspace::add_folder_to_project);
222 cx.add_action(Workspace::remove_folder_from_project);
223 cx.add_action(
224 |workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext<Workspace>| {
225 let pane = workspace.active_pane().clone();
226 workspace.unfollow(&pane, cx);
227 },
228 );
229 cx.add_action(
230 |workspace: &mut Workspace, _: &Save, cx: &mut ViewContext<Workspace>| {
231 workspace.save_active_item(false, cx).detach_and_log_err(cx);
232 },
233 );
234 cx.add_action(
235 |workspace: &mut Workspace, _: &SaveAs, cx: &mut ViewContext<Workspace>| {
236 workspace.save_active_item(true, cx).detach_and_log_err(cx);
237 },
238 );
239 cx.add_action(Workspace::toggle_sidebar_item);
240 cx.add_action(Workspace::focus_center);
241 cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| {
242 workspace.activate_previous_pane(cx)
243 });
244 cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| {
245 workspace.activate_next_pane(cx)
246 });
247 cx.add_action(|workspace: &mut Workspace, _: &ToggleLeftSidebar, cx| {
248 workspace.toggle_sidebar(SidebarSide::Left, cx);
249 });
250 cx.add_action(Workspace::activate_pane_at_index);
251
252 cx.add_action(Workspace::split_pane_with_item);
253 cx.add_action(Workspace::split_pane_with_project_entry);
254
255 cx.add_async_action(
256 |workspace: &mut Workspace,
257 OpenProjectEntryInPane {
258 pane,
259 project_entry,
260 }: &_,
261 cx| {
262 workspace
263 .project
264 .read(cx)
265 .path_for_entry(*project_entry, cx)
266 .map(|path| {
267 let task = workspace.open_path(path, Some(pane.clone()), true, cx);
268 cx.foreground().spawn(async move {
269 task.await?;
270 Ok(())
271 })
272 })
273 },
274 );
275
276 let client = &app_state.client;
277 client.add_view_request_handler(Workspace::handle_follow);
278 client.add_view_message_handler(Workspace::handle_unfollow);
279 client.add_view_message_handler(Workspace::handle_update_followers);
280}
281
282type ProjectItemBuilders = HashMap<
283 TypeId,
284 fn(ModelHandle<Project>, AnyModelHandle, &mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
285>;
286pub fn register_project_item<I: ProjectItem>(cx: &mut MutableAppContext) {
287 cx.update_default_global(|builders: &mut ProjectItemBuilders, _| {
288 builders.insert(TypeId::of::<I::Item>(), |project, model, cx| {
289 let item = model.downcast::<I::Item>().unwrap();
290 Box::new(cx.add_view(|cx| I::for_project_item(project, item, cx)))
291 });
292 });
293}
294
295type FollowableItemBuilder = fn(
296 ViewHandle<Pane>,
297 ModelHandle<Project>,
298 ViewId,
299 &mut Option<proto::view::Variant>,
300 &mut MutableAppContext,
301) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>;
302type FollowableItemBuilders = HashMap<
303 TypeId,
304 (
305 FollowableItemBuilder,
306 fn(AnyViewHandle) -> Box<dyn FollowableItemHandle>,
307 ),
308>;
309pub fn register_followable_item<I: FollowableItem>(cx: &mut MutableAppContext) {
310 cx.update_default_global(|builders: &mut FollowableItemBuilders, _| {
311 builders.insert(
312 TypeId::of::<I>(),
313 (
314 |pane, project, id, state, cx| {
315 I::from_state_proto(pane, project, id, state, cx).map(|task| {
316 cx.foreground()
317 .spawn(async move { Ok(Box::new(task.await?) as Box<_>) })
318 })
319 },
320 |this| Box::new(this.downcast::<I>().unwrap()),
321 ),
322 );
323 });
324}
325
326type ItemDeserializers = HashMap<
327 Arc<str>,
328 fn(
329 ModelHandle<Project>,
330 WeakViewHandle<Workspace>,
331 WorkspaceId,
332 ItemId,
333 &mut ViewContext<Pane>,
334 ) -> Task<Result<Box<dyn ItemHandle>>>,
335>;
336pub fn register_deserializable_item<I: Item>(cx: &mut MutableAppContext) {
337 cx.update_default_global(|deserializers: &mut ItemDeserializers, _cx| {
338 if let Some(serialized_item_kind) = I::serialized_item_kind() {
339 deserializers.insert(
340 Arc::from(serialized_item_kind),
341 |project, workspace, workspace_id, item_id, cx| {
342 let task = I::deserialize(project, workspace, workspace_id, item_id, cx);
343 cx.foreground()
344 .spawn(async { Ok(Box::new(task.await?) as Box<_>) })
345 },
346 );
347 }
348 });
349}
350
351pub struct AppState {
352 pub languages: Arc<LanguageRegistry>,
353 pub themes: Arc<ThemeRegistry>,
354 pub client: Arc<client::Client>,
355 pub user_store: ModelHandle<client::UserStore>,
356 pub fs: Arc<dyn fs::Fs>,
357 pub build_window_options:
358 fn(Option<WindowBounds>, Option<uuid::Uuid>, &dyn Platform) -> WindowOptions<'static>,
359 pub initialize_workspace: fn(&mut Workspace, &Arc<AppState>, &mut ViewContext<Workspace>),
360 pub dock_default_item_factory: DockDefaultItemFactory,
361}
362
363impl AppState {
364 #[cfg(any(test, feature = "test-support"))]
365 pub fn test(cx: &mut MutableAppContext) -> Arc<Self> {
366 let settings = Settings::test(cx);
367 cx.set_global(settings);
368
369 let fs = fs::FakeFs::new(cx.background().clone());
370 let languages = Arc::new(LanguageRegistry::test());
371 let http_client = client::test::FakeHttpClient::with_404_response();
372 let client = Client::new(http_client.clone(), cx);
373 let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
374 let themes = ThemeRegistry::new((), cx.font_cache().clone());
375 Arc::new(Self {
376 client,
377 themes,
378 fs,
379 languages,
380 user_store,
381 initialize_workspace: |_, _, _| {},
382 build_window_options: |_, _, _| Default::default(),
383 dock_default_item_factory: |_, _| unimplemented!(),
384 })
385 }
386}
387
388struct DelayedDebouncedEditAction {
389 task: Option<Task<()>>,
390 cancel_channel: Option<oneshot::Sender<()>>,
391}
392
393impl DelayedDebouncedEditAction {
394 fn new() -> DelayedDebouncedEditAction {
395 DelayedDebouncedEditAction {
396 task: None,
397 cancel_channel: None,
398 }
399 }
400
401 fn fire_new<F, Fut>(
402 &mut self,
403 delay: Duration,
404 workspace: &Workspace,
405 cx: &mut ViewContext<Workspace>,
406 f: F,
407 ) where
408 F: FnOnce(ModelHandle<Project>, AsyncAppContext) -> Fut + 'static,
409 Fut: 'static + Future<Output = ()>,
410 {
411 if let Some(channel) = self.cancel_channel.take() {
412 _ = channel.send(());
413 }
414
415 let project = workspace.project().downgrade();
416
417 let (sender, mut receiver) = oneshot::channel::<()>();
418 self.cancel_channel = Some(sender);
419
420 let previous_task = self.task.take();
421 self.task = Some(cx.spawn_weak(|_, cx| async move {
422 let mut timer = cx.background().timer(delay).fuse();
423 if let Some(previous_task) = previous_task {
424 previous_task.await;
425 }
426
427 futures::select_biased! {
428 _ = receiver => return,
429 _ = timer => {}
430 }
431
432 if let Some(project) = project.upgrade(&cx) {
433 (f)(project, cx).await;
434 }
435 }));
436 }
437}
438
439pub enum Event {
440 DockAnchorChanged,
441 PaneAdded(ViewHandle<Pane>),
442 ContactRequestedJoin(u64),
443}
444
445pub struct Workspace {
446 weak_self: WeakViewHandle<Self>,
447 client: Arc<Client>,
448 user_store: ModelHandle<client::UserStore>,
449 remote_entity_subscription: Option<client::Subscription>,
450 fs: Arc<dyn Fs>,
451 modal: Option<AnyViewHandle>,
452 center: PaneGroup,
453 left_sidebar: ViewHandle<Sidebar>,
454 right_sidebar: ViewHandle<Sidebar>,
455 panes: Vec<ViewHandle<Pane>>,
456 panes_by_item: HashMap<usize, WeakViewHandle<Pane>>,
457 active_pane: ViewHandle<Pane>,
458 last_active_center_pane: Option<WeakViewHandle<Pane>>,
459 status_bar: ViewHandle<StatusBar>,
460 titlebar_item: Option<AnyViewHandle>,
461 dock: Dock,
462 notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
463 project: ModelHandle<Project>,
464 leader_state: LeaderState,
465 follower_states_by_leader: FollowerStatesByLeader,
466 last_leaders_by_pane: HashMap<WeakViewHandle<Pane>, PeerId>,
467 window_edited: bool,
468 active_call: Option<(ModelHandle<ActiveCall>, Vec<gpui::Subscription>)>,
469 leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
470 database_id: WorkspaceId,
471 _apply_leader_updates: Task<Result<()>>,
472 _observe_current_user: Task<()>,
473}
474
475#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
476pub struct ViewId {
477 pub creator: PeerId,
478 pub id: u64,
479}
480
481#[derive(Default)]
482struct LeaderState {
483 followers: HashSet<PeerId>,
484}
485
486type FollowerStatesByLeader = HashMap<PeerId, HashMap<ViewHandle<Pane>, FollowerState>>;
487
488#[derive(Default)]
489struct FollowerState {
490 active_view_id: Option<ViewId>,
491 items_by_leader_view_id: HashMap<ViewId, Box<dyn FollowableItemHandle>>,
492}
493
494impl Workspace {
495 pub fn new(
496 serialized_workspace: Option<SerializedWorkspace>,
497 workspace_id: WorkspaceId,
498 project: ModelHandle<Project>,
499 dock_default_factory: DockDefaultItemFactory,
500 cx: &mut ViewContext<Self>,
501 ) -> Self {
502 cx.observe_fullscreen(|_, _, cx| cx.notify()).detach();
503
504 cx.observe_window_activation(Self::on_window_activation_changed)
505 .detach();
506 cx.observe(&project, |_, _, cx| cx.notify()).detach();
507 cx.subscribe(&project, move |this, _, event, cx| {
508 match event {
509 project::Event::RemoteIdChanged(remote_id) => {
510 this.update_window_title(cx);
511 this.project_remote_id_changed(*remote_id, cx);
512 }
513
514 project::Event::CollaboratorLeft(peer_id) => {
515 this.collaborator_left(*peer_id, cx);
516 }
517
518 project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded => {
519 this.update_window_title(cx);
520 this.serialize_workspace(cx);
521 }
522
523 project::Event::DisconnectedFromHost => {
524 this.update_window_edited(cx);
525 cx.blur();
526 }
527
528 _ => {}
529 }
530 cx.notify()
531 })
532 .detach();
533
534 let center_pane = cx.add_view(|cx| Pane::new(None, cx));
535 let pane_id = center_pane.id();
536 cx.subscribe(¢er_pane, move |this, _, event, cx| {
537 this.handle_pane_event(pane_id, event, cx)
538 })
539 .detach();
540 cx.focus(¢er_pane);
541 cx.emit(Event::PaneAdded(center_pane.clone()));
542 let dock = Dock::new(dock_default_factory, cx);
543 let dock_pane = dock.pane().clone();
544
545 let fs = project.read(cx).fs().clone();
546 let user_store = project.read(cx).user_store();
547 let client = project.read(cx).client();
548 let mut current_user = user_store.read(cx).watch_current_user();
549 let mut connection_status = client.status();
550 let _observe_current_user = cx.spawn_weak(|this, mut cx| async move {
551 current_user.recv().await;
552 connection_status.recv().await;
553 let mut stream =
554 Stream::map(current_user, drop).merge(Stream::map(connection_status, drop));
555
556 while stream.recv().await.is_some() {
557 cx.update(|cx| {
558 if let Some(this) = this.upgrade(cx) {
559 this.update(cx, |_, cx| cx.notify());
560 }
561 })
562 }
563 });
564 let handle = cx.handle();
565 let weak_handle = cx.weak_handle();
566
567 // All leader updates are enqueued and then processed in a single task, so
568 // that each asynchronous operation can be run in order.
569 let (leader_updates_tx, mut leader_updates_rx) =
570 mpsc::unbounded::<(PeerId, proto::UpdateFollowers)>();
571 let _apply_leader_updates = cx.spawn_weak(|this, mut cx| async move {
572 while let Some((leader_id, update)) = leader_updates_rx.next().await {
573 let Some(this) = this.upgrade(&cx) else { break };
574 Self::process_leader_update(this, leader_id, update, &mut cx)
575 .await
576 .log_err();
577 }
578
579 Ok(())
580 });
581
582 cx.emit_global(WorkspaceCreated(weak_handle.clone()));
583
584 let left_sidebar = cx.add_view(|_| Sidebar::new(SidebarSide::Left));
585 let right_sidebar = cx.add_view(|_| Sidebar::new(SidebarSide::Right));
586 let left_sidebar_buttons = cx.add_view(|cx| SidebarButtons::new(left_sidebar.clone(), cx));
587 let toggle_dock = cx.add_view(|cx| ToggleDockButton::new(handle, cx));
588 let right_sidebar_buttons =
589 cx.add_view(|cx| SidebarButtons::new(right_sidebar.clone(), cx));
590 let status_bar = cx.add_view(|cx| {
591 let mut status_bar = StatusBar::new(¢er_pane.clone(), cx);
592 status_bar.add_left_item(left_sidebar_buttons, cx);
593 status_bar.add_right_item(right_sidebar_buttons, cx);
594 status_bar.add_right_item(toggle_dock, cx);
595 status_bar
596 });
597
598 cx.update_default_global::<DragAndDrop<Workspace>, _, _>(|drag_and_drop, _| {
599 drag_and_drop.register_container(weak_handle.clone());
600 });
601
602 let mut active_call = None;
603 if cx.has_global::<ModelHandle<ActiveCall>>() {
604 let call = cx.global::<ModelHandle<ActiveCall>>().clone();
605 let mut subscriptions = Vec::new();
606 subscriptions.push(cx.subscribe(&call, Self::on_active_call_event));
607 active_call = Some((call, subscriptions));
608 }
609
610 let mut this = Workspace {
611 modal: None,
612 weak_self: weak_handle.clone(),
613 center: PaneGroup::new(center_pane.clone()),
614 dock,
615 // When removing an item, the last element remaining in this array
616 // is used to find where focus should fallback to. As such, the order
617 // of these two variables is important.
618 panes: vec![dock_pane.clone(), center_pane.clone()],
619 panes_by_item: Default::default(),
620 active_pane: center_pane.clone(),
621 last_active_center_pane: Some(center_pane.downgrade()),
622 status_bar,
623 titlebar_item: None,
624 notifications: Default::default(),
625 client,
626 remote_entity_subscription: None,
627 user_store,
628 fs,
629 left_sidebar,
630 right_sidebar,
631 project: project.clone(),
632 leader_state: Default::default(),
633 follower_states_by_leader: Default::default(),
634 last_leaders_by_pane: Default::default(),
635 window_edited: false,
636 active_call,
637 database_id: workspace_id,
638 _observe_current_user,
639 _apply_leader_updates,
640 leader_updates_tx,
641 };
642 this.project_remote_id_changed(project.read(cx).remote_id(), cx);
643 cx.defer(|this, cx| this.update_window_title(cx));
644
645 if let Some(serialized_workspace) = serialized_workspace {
646 cx.defer(move |_, cx| {
647 Self::load_from_serialized_workspace(weak_handle, serialized_workspace, cx)
648 });
649 }
650
651 this
652 }
653
654 fn new_local(
655 abs_paths: Vec<PathBuf>,
656 app_state: Arc<AppState>,
657 cx: &mut MutableAppContext,
658 ) -> Task<(
659 ViewHandle<Workspace>,
660 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
661 )> {
662 let project_handle = Project::local(
663 app_state.client.clone(),
664 app_state.user_store.clone(),
665 app_state.languages.clone(),
666 app_state.fs.clone(),
667 cx,
668 );
669
670 cx.spawn(|mut cx| async move {
671 let serialized_workspace = persistence::DB.workspace_for_roots(&abs_paths.as_slice());
672
673 let paths_to_open = serialized_workspace
674 .as_ref()
675 .map(|workspace| workspace.location.paths())
676 .unwrap_or(Arc::new(abs_paths));
677
678 // Get project paths for all of the abs_paths
679 let mut worktree_roots: HashSet<Arc<Path>> = Default::default();
680 let mut project_paths = Vec::new();
681 for path in paths_to_open.iter() {
682 if let Some((worktree, project_entry)) = cx
683 .update(|cx| {
684 Workspace::project_path_for_path(project_handle.clone(), &path, true, cx)
685 })
686 .await
687 .log_err()
688 {
689 worktree_roots.insert(worktree.read_with(&mut cx, |tree, _| tree.abs_path()));
690 project_paths.push(Some(project_entry));
691 } else {
692 project_paths.push(None);
693 }
694 }
695
696 let workspace_id = if let Some(serialized_workspace) = serialized_workspace.as_ref() {
697 serialized_workspace.id
698 } else {
699 DB.next_id().await.unwrap_or(0)
700 };
701
702 let window_bounds_override =
703 ZED_WINDOW_POSITION
704 .zip(*ZED_WINDOW_SIZE)
705 .map(|(position, size)| {
706 WindowBounds::Fixed(RectF::new(
707 cx.platform().screens()[0].bounds().origin() + position,
708 size,
709 ))
710 });
711
712 let (bounds, display) = if let Some(bounds) = window_bounds_override {
713 (Some(bounds), None)
714 } else {
715 serialized_workspace
716 .as_ref()
717 .and_then(|serialized_workspace| {
718 let display = serialized_workspace.display?;
719 let mut bounds = serialized_workspace.bounds?;
720
721 // Stored bounds are relative to the containing display.
722 // So convert back to global coordinates if that screen still exists
723 if let WindowBounds::Fixed(mut window_bounds) = bounds {
724 if let Some(screen) = cx.platform().screen_by_id(display) {
725 let screen_bounds = screen.bounds();
726 window_bounds.set_origin_x(
727 window_bounds.origin_x() + screen_bounds.origin_x(),
728 );
729 window_bounds.set_origin_y(
730 window_bounds.origin_y() + screen_bounds.origin_y(),
731 );
732 bounds = WindowBounds::Fixed(window_bounds);
733 } else {
734 // Screen no longer exists. Return none here.
735 return None;
736 }
737 }
738
739 Some((bounds, display))
740 })
741 .unzip()
742 };
743
744 // Use the serialized workspace to construct the new window
745 let (_, workspace) = cx.add_window(
746 (app_state.build_window_options)(bounds, display, cx.platform().as_ref()),
747 |cx| {
748 let mut workspace = Workspace::new(
749 serialized_workspace,
750 workspace_id,
751 project_handle,
752 app_state.dock_default_item_factory,
753 cx,
754 );
755 (app_state.initialize_workspace)(&mut workspace, &app_state, cx);
756 cx.observe_window_bounds(move |_, mut bounds, display, cx| {
757 // Transform fixed bounds to be stored in terms of the containing display
758 if let WindowBounds::Fixed(mut window_bounds) = bounds {
759 if let Some(screen) = cx.platform().screen_by_id(display) {
760 let screen_bounds = screen.bounds();
761 window_bounds.set_origin_x(
762 window_bounds.origin_x() - screen_bounds.origin_x(),
763 );
764 window_bounds.set_origin_y(
765 window_bounds.origin_y() - screen_bounds.origin_y(),
766 );
767 bounds = WindowBounds::Fixed(window_bounds);
768 }
769 }
770
771 cx.background()
772 .spawn(DB.set_window_bounds(workspace_id, bounds, display))
773 .detach_and_log_err(cx);
774 })
775 .detach();
776 workspace
777 },
778 );
779
780 notify_if_database_failed(&workspace, &mut cx);
781
782 // Call open path for each of the project paths
783 // (this will bring them to the front if they were in the serialized workspace)
784 debug_assert!(paths_to_open.len() == project_paths.len());
785 let tasks = paths_to_open
786 .iter()
787 .cloned()
788 .zip(project_paths.into_iter())
789 .map(|(abs_path, project_path)| {
790 let workspace = workspace.clone();
791 cx.spawn(|mut cx| {
792 let fs = app_state.fs.clone();
793 async move {
794 let project_path = project_path?;
795 if fs.is_file(&abs_path).await {
796 Some(
797 workspace
798 .update(&mut cx, |workspace, cx| {
799 workspace.open_path(project_path, None, true, cx)
800 })
801 .await,
802 )
803 } else {
804 None
805 }
806 }
807 })
808 });
809
810 let opened_items = futures::future::join_all(tasks.into_iter()).await;
811
812 (workspace, opened_items)
813 })
814 }
815
816 pub fn weak_handle(&self) -> WeakViewHandle<Self> {
817 self.weak_self.clone()
818 }
819
820 pub fn left_sidebar(&self) -> &ViewHandle<Sidebar> {
821 &self.left_sidebar
822 }
823
824 pub fn right_sidebar(&self) -> &ViewHandle<Sidebar> {
825 &self.right_sidebar
826 }
827
828 pub fn status_bar(&self) -> &ViewHandle<StatusBar> {
829 &self.status_bar
830 }
831
832 pub fn user_store(&self) -> &ModelHandle<UserStore> {
833 &self.user_store
834 }
835
836 pub fn project(&self) -> &ModelHandle<Project> {
837 &self.project
838 }
839
840 pub fn client(&self) -> &Arc<Client> {
841 &self.client
842 }
843
844 pub fn set_titlebar_item(
845 &mut self,
846 item: impl Into<AnyViewHandle>,
847 cx: &mut ViewContext<Self>,
848 ) {
849 self.titlebar_item = Some(item.into());
850 cx.notify();
851 }
852
853 pub fn titlebar_item(&self) -> Option<AnyViewHandle> {
854 self.titlebar_item.clone()
855 }
856
857 /// Call the given callback with a workspace whose project is local.
858 ///
859 /// If the given workspace has a local project, then it will be passed
860 /// to the callback. Otherwise, a new empty window will be created.
861 pub fn with_local_workspace<T, F>(
862 &mut self,
863 app_state: &Arc<AppState>,
864 cx: &mut ViewContext<Self>,
865 callback: F,
866 ) -> Task<T>
867 where
868 T: 'static,
869 F: 'static + FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
870 {
871 if self.project.read(cx).is_local() {
872 Task::Ready(Some(callback(self, cx)))
873 } else {
874 let task = Self::new_local(Vec::new(), app_state.clone(), cx);
875 cx.spawn(|_vh, mut cx| async move {
876 let (workspace, _) = task.await;
877 workspace.update(&mut cx, callback)
878 })
879 }
880 }
881
882 pub fn worktrees<'a>(
883 &self,
884 cx: &'a AppContext,
885 ) -> impl 'a + Iterator<Item = ModelHandle<Worktree>> {
886 self.project.read(cx).worktrees(cx)
887 }
888
889 pub fn visible_worktrees<'a>(
890 &self,
891 cx: &'a AppContext,
892 ) -> impl 'a + Iterator<Item = ModelHandle<Worktree>> {
893 self.project.read(cx).visible_worktrees(cx)
894 }
895
896 pub fn worktree_scans_complete(&self, cx: &AppContext) -> impl Future<Output = ()> + 'static {
897 let futures = self
898 .worktrees(cx)
899 .filter_map(|worktree| worktree.read(cx).as_local())
900 .map(|worktree| worktree.scan_complete())
901 .collect::<Vec<_>>();
902 async move {
903 for future in futures {
904 future.await;
905 }
906 }
907 }
908
909 pub fn close_global(_: &CloseWindow, cx: &mut MutableAppContext) {
910 let id = cx.window_ids().find(|&id| cx.window_is_active(id));
911 if let Some(id) = id {
912 //This can only get called when the window's project connection has been lost
913 //so we don't need to prompt the user for anything and instead just close the window
914 cx.remove_window(id);
915 }
916 }
917
918 pub fn close(
919 &mut self,
920 _: &CloseWindow,
921 cx: &mut ViewContext<Self>,
922 ) -> Option<Task<Result<()>>> {
923 let prepare = self.prepare_to_close(false, cx);
924 Some(cx.spawn(|this, mut cx| async move {
925 if prepare.await? {
926 this.update(&mut cx, |_, cx| {
927 let window_id = cx.window_id();
928 cx.remove_window(window_id);
929 });
930 }
931 Ok(())
932 }))
933 }
934
935 pub fn prepare_to_close(
936 &mut self,
937 quitting: bool,
938 cx: &mut ViewContext<Self>,
939 ) -> Task<Result<bool>> {
940 let active_call = self.active_call().cloned();
941 let window_id = cx.window_id();
942 let workspace_count = cx
943 .window_ids()
944 .flat_map(|window_id| cx.root_view::<Workspace>(window_id))
945 .count();
946
947 cx.spawn(|this, mut cx| async move {
948 if let Some(active_call) = active_call {
949 if !quitting
950 && workspace_count == 1
951 && active_call.read_with(&cx, |call, _| call.room().is_some())
952 {
953 let answer = cx
954 .prompt(
955 window_id,
956 PromptLevel::Warning,
957 "Do you want to leave the current call?",
958 &["Close window and hang up", "Cancel"],
959 )
960 .next()
961 .await;
962
963 if answer == Some(1) {
964 return anyhow::Ok(false);
965 } else {
966 active_call.update(&mut cx, |call, cx| call.hang_up(cx))?;
967 }
968 }
969 }
970
971 Ok(this
972 .update(&mut cx, |this, cx| this.save_all_internal(true, cx))
973 .await?)
974 })
975 }
976
977 fn save_all(&mut self, _: &SaveAll, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
978 let save_all = self.save_all_internal(false, cx);
979 Some(cx.foreground().spawn(async move {
980 save_all.await?;
981 Ok(())
982 }))
983 }
984
985 fn save_all_internal(
986 &mut self,
987 should_prompt_to_save: bool,
988 cx: &mut ViewContext<Self>,
989 ) -> Task<Result<bool>> {
990 if self.project.read(cx).is_read_only() {
991 return Task::ready(Ok(true));
992 }
993
994 let dirty_items = self
995 .panes
996 .iter()
997 .flat_map(|pane| {
998 pane.read(cx).items().filter_map(|item| {
999 if item.is_dirty(cx) {
1000 Some((pane.clone(), item.boxed_clone()))
1001 } else {
1002 None
1003 }
1004 })
1005 })
1006 .collect::<Vec<_>>();
1007
1008 let project = self.project.clone();
1009 cx.spawn_weak(|_, mut cx| async move {
1010 for (pane, item) in dirty_items {
1011 let (singleton, project_entry_ids) =
1012 cx.read(|cx| (item.is_singleton(cx), item.project_entry_ids(cx)));
1013 if singleton || !project_entry_ids.is_empty() {
1014 if let Some(ix) =
1015 pane.read_with(&cx, |pane, _| pane.index_for_item(item.as_ref()))
1016 {
1017 if !Pane::save_item(
1018 project.clone(),
1019 &pane,
1020 ix,
1021 &*item,
1022 should_prompt_to_save,
1023 &mut cx,
1024 )
1025 .await?
1026 {
1027 return Ok(false);
1028 }
1029 }
1030 }
1031 }
1032 Ok(true)
1033 })
1034 }
1035
1036 #[allow(clippy::type_complexity)]
1037 pub fn open_paths(
1038 &mut self,
1039 mut abs_paths: Vec<PathBuf>,
1040 visible: bool,
1041 cx: &mut ViewContext<Self>,
1042 ) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
1043 let fs = self.fs.clone();
1044
1045 // Sort the paths to ensure we add worktrees for parents before their children.
1046 abs_paths.sort_unstable();
1047 cx.spawn(|this, mut cx| async move {
1048 let mut project_paths = Vec::new();
1049 for path in &abs_paths {
1050 project_paths.push(
1051 this.update(&mut cx, |this, cx| {
1052 Workspace::project_path_for_path(this.project.clone(), path, visible, cx)
1053 })
1054 .await
1055 .log_err(),
1056 );
1057 }
1058
1059 let tasks = abs_paths
1060 .iter()
1061 .cloned()
1062 .zip(project_paths.into_iter())
1063 .map(|(abs_path, project_path)| {
1064 let this = this.clone();
1065 cx.spawn(|mut cx| {
1066 let fs = fs.clone();
1067 async move {
1068 let (_worktree, project_path) = project_path?;
1069 if fs.is_file(&abs_path).await {
1070 Some(
1071 this.update(&mut cx, |this, cx| {
1072 this.open_path(project_path, None, true, cx)
1073 })
1074 .await,
1075 )
1076 } else {
1077 None
1078 }
1079 }
1080 })
1081 })
1082 .collect::<Vec<_>>();
1083
1084 futures::future::join_all(tasks).await
1085 })
1086 }
1087
1088 fn add_folder_to_project(&mut self, _: &AddFolderToProject, cx: &mut ViewContext<Self>) {
1089 let mut paths = cx.prompt_for_paths(PathPromptOptions {
1090 files: false,
1091 directories: true,
1092 multiple: true,
1093 });
1094 cx.spawn(|this, mut cx| async move {
1095 if let Some(paths) = paths.recv().await.flatten() {
1096 let results = this
1097 .update(&mut cx, |this, cx| this.open_paths(paths, true, cx))
1098 .await;
1099 for result in results.into_iter().flatten() {
1100 result.log_err();
1101 }
1102 }
1103 })
1104 .detach();
1105 }
1106
1107 fn remove_folder_from_project(
1108 &mut self,
1109 RemoveWorktreeFromProject(worktree_id): &RemoveWorktreeFromProject,
1110 cx: &mut ViewContext<Self>,
1111 ) {
1112 let future = self
1113 .project
1114 .update(cx, |project, cx| project.remove_worktree(*worktree_id, cx));
1115 cx.foreground().spawn(future).detach();
1116 }
1117
1118 fn project_path_for_path(
1119 project: ModelHandle<Project>,
1120 abs_path: &Path,
1121 visible: bool,
1122 cx: &mut MutableAppContext,
1123 ) -> Task<Result<(ModelHandle<Worktree>, ProjectPath)>> {
1124 let entry = project.update(cx, |project, cx| {
1125 project.find_or_create_local_worktree(abs_path, visible, cx)
1126 });
1127 cx.spawn(|cx| async move {
1128 let (worktree, path) = entry.await?;
1129 let worktree_id = worktree.read_with(&cx, |t, _| t.id());
1130 Ok((
1131 worktree,
1132 ProjectPath {
1133 worktree_id,
1134 path: path.into(),
1135 },
1136 ))
1137 })
1138 }
1139
1140 /// Returns the modal that was toggled closed if it was open.
1141 pub fn toggle_modal<V, F>(
1142 &mut self,
1143 cx: &mut ViewContext<Self>,
1144 add_view: F,
1145 ) -> Option<ViewHandle<V>>
1146 where
1147 V: 'static + View,
1148 F: FnOnce(&mut Self, &mut ViewContext<Self>) -> ViewHandle<V>,
1149 {
1150 cx.notify();
1151 // Whatever modal was visible is getting clobbered. If its the same type as V, then return
1152 // it. Otherwise, create a new modal and set it as active.
1153 let already_open_modal = self.modal.take().and_then(|modal| modal.downcast::<V>());
1154 if let Some(already_open_modal) = already_open_modal {
1155 cx.focus_self();
1156 Some(already_open_modal)
1157 } else {
1158 let modal = add_view(self, cx);
1159 cx.focus(&modal);
1160 self.modal = Some(modal.into());
1161 None
1162 }
1163 }
1164
1165 pub fn modal<V: 'static + View>(&self) -> Option<ViewHandle<V>> {
1166 self.modal
1167 .as_ref()
1168 .and_then(|modal| modal.clone().downcast::<V>())
1169 }
1170
1171 pub fn dismiss_modal(&mut self, cx: &mut ViewContext<Self>) {
1172 if self.modal.take().is_some() {
1173 cx.focus(&self.active_pane);
1174 cx.notify();
1175 }
1176 }
1177
1178 pub fn items<'a>(
1179 &'a self,
1180 cx: &'a AppContext,
1181 ) -> impl 'a + Iterator<Item = &Box<dyn ItemHandle>> {
1182 self.panes.iter().flat_map(|pane| pane.read(cx).items())
1183 }
1184
1185 pub fn item_of_type<T: Item>(&self, cx: &AppContext) -> Option<ViewHandle<T>> {
1186 self.items_of_type(cx).max_by_key(|item| item.id())
1187 }
1188
1189 pub fn items_of_type<'a, T: Item>(
1190 &'a self,
1191 cx: &'a AppContext,
1192 ) -> impl 'a + Iterator<Item = ViewHandle<T>> {
1193 self.panes
1194 .iter()
1195 .flat_map(|pane| pane.read(cx).items_of_type())
1196 }
1197
1198 pub fn active_item(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
1199 self.active_pane().read(cx).active_item()
1200 }
1201
1202 fn active_project_path(&self, cx: &ViewContext<Self>) -> Option<ProjectPath> {
1203 self.active_item(cx).and_then(|item| item.project_path(cx))
1204 }
1205
1206 pub fn save_active_item(
1207 &mut self,
1208 force_name_change: bool,
1209 cx: &mut ViewContext<Self>,
1210 ) -> Task<Result<()>> {
1211 let project = self.project.clone();
1212 if let Some(item) = self.active_item(cx) {
1213 if !force_name_change && item.can_save(cx) {
1214 if item.has_conflict(cx.as_ref()) {
1215 const CONFLICT_MESSAGE: &str = "This file has changed on disk since you started editing it. Do you want to overwrite it?";
1216
1217 let mut answer = cx.prompt(
1218 PromptLevel::Warning,
1219 CONFLICT_MESSAGE,
1220 &["Overwrite", "Cancel"],
1221 );
1222 cx.spawn(|_, mut cx| async move {
1223 let answer = answer.recv().await;
1224 if answer == Some(0) {
1225 cx.update(|cx| item.save(project, cx)).await?;
1226 }
1227 Ok(())
1228 })
1229 } else {
1230 item.save(project, cx)
1231 }
1232 } else if item.is_singleton(cx) {
1233 let worktree = self.worktrees(cx).next();
1234 let start_abs_path = worktree
1235 .and_then(|w| w.read(cx).as_local())
1236 .map_or(Path::new(""), |w| w.abs_path())
1237 .to_path_buf();
1238 let mut abs_path = cx.prompt_for_new_path(&start_abs_path);
1239 cx.spawn(|_, mut cx| async move {
1240 if let Some(abs_path) = abs_path.recv().await.flatten() {
1241 cx.update(|cx| item.save_as(project, abs_path, cx)).await?;
1242 }
1243 Ok(())
1244 })
1245 } else {
1246 Task::ready(Ok(()))
1247 }
1248 } else {
1249 Task::ready(Ok(()))
1250 }
1251 }
1252
1253 pub fn toggle_sidebar(&mut self, sidebar_side: SidebarSide, cx: &mut ViewContext<Self>) {
1254 let sidebar = match sidebar_side {
1255 SidebarSide::Left => &mut self.left_sidebar,
1256 SidebarSide::Right => &mut self.right_sidebar,
1257 };
1258 let open = sidebar.update(cx, |sidebar, cx| {
1259 let open = !sidebar.is_open();
1260 sidebar.set_open(open, cx);
1261 open
1262 });
1263
1264 if open {
1265 Dock::hide_on_sidebar_shown(self, sidebar_side, cx);
1266 }
1267
1268 self.serialize_workspace(cx);
1269
1270 cx.focus_self();
1271 cx.notify();
1272 }
1273
1274 pub fn toggle_sidebar_item(&mut self, action: &ToggleSidebarItem, cx: &mut ViewContext<Self>) {
1275 let sidebar = match action.sidebar_side {
1276 SidebarSide::Left => &mut self.left_sidebar,
1277 SidebarSide::Right => &mut self.right_sidebar,
1278 };
1279 let active_item = sidebar.update(cx, move |sidebar, cx| {
1280 if sidebar.is_open() && sidebar.active_item_ix() == action.item_index {
1281 sidebar.set_open(false, cx);
1282 None
1283 } else {
1284 sidebar.set_open(true, cx);
1285 sidebar.activate_item(action.item_index, cx);
1286 sidebar.active_item().cloned()
1287 }
1288 });
1289
1290 if let Some(active_item) = active_item {
1291 Dock::hide_on_sidebar_shown(self, action.sidebar_side, cx);
1292
1293 if active_item.is_focused(cx) {
1294 cx.focus_self();
1295 } else {
1296 cx.focus(active_item.to_any());
1297 }
1298 } else {
1299 cx.focus_self();
1300 }
1301
1302 self.serialize_workspace(cx);
1303
1304 cx.notify();
1305 }
1306
1307 pub fn toggle_sidebar_item_focus(
1308 &mut self,
1309 sidebar_side: SidebarSide,
1310 item_index: usize,
1311 cx: &mut ViewContext<Self>,
1312 ) {
1313 let sidebar = match sidebar_side {
1314 SidebarSide::Left => &mut self.left_sidebar,
1315 SidebarSide::Right => &mut self.right_sidebar,
1316 };
1317 let active_item = sidebar.update(cx, |sidebar, cx| {
1318 sidebar.set_open(true, cx);
1319 sidebar.activate_item(item_index, cx);
1320 sidebar.active_item().cloned()
1321 });
1322 if let Some(active_item) = active_item {
1323 Dock::hide_on_sidebar_shown(self, sidebar_side, cx);
1324
1325 if active_item.is_focused(cx) {
1326 cx.focus_self();
1327 } else {
1328 cx.focus(active_item.to_any());
1329 }
1330 }
1331
1332 self.serialize_workspace(cx);
1333
1334 cx.notify();
1335 }
1336
1337 pub fn focus_center(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
1338 cx.focus_self();
1339 cx.notify();
1340 }
1341
1342 fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
1343 let pane = cx.add_view(|cx| Pane::new(None, cx));
1344 let pane_id = pane.id();
1345 cx.subscribe(&pane, move |this, _, event, cx| {
1346 this.handle_pane_event(pane_id, event, cx)
1347 })
1348 .detach();
1349 self.panes.push(pane.clone());
1350 cx.focus(pane.clone());
1351 cx.emit(Event::PaneAdded(pane.clone()));
1352 pane
1353 }
1354
1355 pub fn add_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
1356 let active_pane = self.active_pane().clone();
1357 Pane::add_item(self, &active_pane, item, true, true, None, cx);
1358 }
1359
1360 pub fn open_path(
1361 &mut self,
1362 path: impl Into<ProjectPath>,
1363 pane: Option<WeakViewHandle<Pane>>,
1364 focus_item: bool,
1365 cx: &mut ViewContext<Self>,
1366 ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
1367 let pane = pane.unwrap_or_else(|| {
1368 if !self.dock_active() {
1369 self.active_pane().downgrade()
1370 } else {
1371 self.last_active_center_pane.clone().unwrap_or_else(|| {
1372 self.panes
1373 .first()
1374 .expect("There must be an active pane")
1375 .downgrade()
1376 })
1377 }
1378 });
1379
1380 let task = self.load_path(path.into(), cx);
1381 cx.spawn(|this, mut cx| async move {
1382 let (project_entry_id, build_item) = task.await?;
1383 let pane = pane
1384 .upgrade(&cx)
1385 .ok_or_else(|| anyhow!("pane was closed"))?;
1386 this.update(&mut cx, |this, cx| {
1387 Ok(Pane::open_item(
1388 this,
1389 pane,
1390 project_entry_id,
1391 focus_item,
1392 cx,
1393 build_item,
1394 ))
1395 })
1396 })
1397 }
1398
1399 pub(crate) fn load_path(
1400 &mut self,
1401 path: ProjectPath,
1402 cx: &mut ViewContext<Self>,
1403 ) -> Task<
1404 Result<(
1405 ProjectEntryId,
1406 impl 'static + FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
1407 )>,
1408 > {
1409 let project = self.project().clone();
1410 let project_item = project.update(cx, |project, cx| project.open_path(path, cx));
1411 cx.as_mut().spawn(|mut cx| async move {
1412 let (project_entry_id, project_item) = project_item.await?;
1413 let build_item = cx.update(|cx| {
1414 cx.default_global::<ProjectItemBuilders>()
1415 .get(&project_item.model_type())
1416 .ok_or_else(|| anyhow!("no item builder for project item"))
1417 .cloned()
1418 })?;
1419 let build_item =
1420 move |cx: &mut ViewContext<Pane>| build_item(project, project_item, cx);
1421 Ok((project_entry_id, build_item))
1422 })
1423 }
1424
1425 pub fn open_project_item<T>(
1426 &mut self,
1427 project_item: ModelHandle<T::Item>,
1428 cx: &mut ViewContext<Self>,
1429 ) -> ViewHandle<T>
1430 where
1431 T: ProjectItem,
1432 {
1433 use project::Item as _;
1434
1435 let entry_id = project_item.read(cx).entry_id(cx);
1436 if let Some(item) = entry_id
1437 .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id, cx))
1438 .and_then(|item| item.downcast())
1439 {
1440 self.activate_item(&item, cx);
1441 return item;
1442 }
1443
1444 let item = cx.add_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
1445 self.add_item(Box::new(item.clone()), cx);
1446 item
1447 }
1448
1449 pub fn open_shared_screen(&mut self, action: &OpenSharedScreen, cx: &mut ViewContext<Self>) {
1450 if let Some(shared_screen) =
1451 self.shared_screen_for_peer(action.peer_id, &self.active_pane, cx)
1452 {
1453 let pane = self.active_pane.clone();
1454 Pane::add_item(self, &pane, Box::new(shared_screen), false, true, None, cx);
1455 }
1456 }
1457
1458 pub fn activate_item(&mut self, item: &dyn ItemHandle, cx: &mut ViewContext<Self>) -> bool {
1459 let result = self.panes.iter().find_map(|pane| {
1460 pane.read(cx)
1461 .index_for_item(item)
1462 .map(|ix| (pane.clone(), ix))
1463 });
1464 if let Some((pane, ix)) = result {
1465 pane.update(cx, |pane, cx| pane.activate_item(ix, true, true, cx));
1466 true
1467 } else {
1468 false
1469 }
1470 }
1471
1472 fn activate_pane_at_index(&mut self, action: &ActivatePane, cx: &mut ViewContext<Self>) {
1473 let panes = self.center.panes();
1474 if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
1475 cx.focus(pane);
1476 } else {
1477 self.split_pane(self.active_pane.clone(), SplitDirection::Right, cx);
1478 }
1479 }
1480
1481 pub fn activate_next_pane(&mut self, cx: &mut ViewContext<Self>) {
1482 let panes = self.center.panes();
1483 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
1484 let next_ix = (ix + 1) % panes.len();
1485 let next_pane = panes[next_ix].clone();
1486 cx.focus(next_pane);
1487 }
1488 }
1489
1490 pub fn activate_previous_pane(&mut self, cx: &mut ViewContext<Self>) {
1491 let panes = self.center.panes();
1492 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
1493 let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
1494 let prev_pane = panes[prev_ix].clone();
1495 cx.focus(prev_pane);
1496 }
1497 }
1498
1499 fn handle_pane_focused(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
1500 if self.active_pane != pane {
1501 self.active_pane
1502 .update(cx, |pane, cx| pane.set_active(false, cx));
1503 self.active_pane = pane.clone();
1504 self.active_pane
1505 .update(cx, |pane, cx| pane.set_active(true, cx));
1506 self.status_bar.update(cx, |status_bar, cx| {
1507 status_bar.set_active_pane(&self.active_pane, cx);
1508 });
1509 self.active_item_path_changed(cx);
1510
1511 if &pane == self.dock_pane() {
1512 Dock::show(self, cx);
1513 } else {
1514 self.last_active_center_pane = Some(pane.downgrade());
1515 if self.dock.is_anchored_at(DockAnchor::Expanded) {
1516 Dock::hide(self, cx);
1517 }
1518 }
1519 cx.notify();
1520 }
1521
1522 self.update_followers(
1523 proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView {
1524 id: self.active_item(cx).and_then(|item| {
1525 item.to_followable_item_handle(cx)?
1526 .remote_id(&self.client, cx)
1527 .map(|id| id.to_proto())
1528 }),
1529 leader_id: self.leader_for_pane(&pane),
1530 }),
1531 cx,
1532 );
1533 }
1534
1535 fn handle_pane_event(
1536 &mut self,
1537 pane_id: usize,
1538 event: &pane::Event,
1539 cx: &mut ViewContext<Self>,
1540 ) {
1541 if let Some(pane) = self.pane(pane_id) {
1542 let is_dock = &pane == self.dock.pane();
1543 match event {
1544 pane::Event::Split(direction) if !is_dock => {
1545 self.split_pane(pane, *direction, cx);
1546 }
1547 pane::Event::Remove if !is_dock => self.remove_pane(pane, cx),
1548 pane::Event::Remove if is_dock => Dock::hide(self, cx),
1549 pane::Event::ActivateItem { local } => {
1550 if *local {
1551 self.unfollow(&pane, cx);
1552 }
1553 if &pane == self.active_pane() {
1554 self.active_item_path_changed(cx);
1555 }
1556 }
1557 pane::Event::ChangeItemTitle => {
1558 if pane == self.active_pane {
1559 self.active_item_path_changed(cx);
1560 }
1561 self.update_window_edited(cx);
1562 }
1563 pane::Event::RemoveItem { item_id } => {
1564 self.update_window_edited(cx);
1565 if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) {
1566 if entry.get().id() == pane.id() {
1567 entry.remove();
1568 }
1569 }
1570 }
1571 _ => {}
1572 }
1573
1574 self.serialize_workspace(cx);
1575 } else if self.dock.visible_pane().is_none() {
1576 error!("pane {} not found", pane_id);
1577 }
1578 }
1579
1580 pub fn split_pane(
1581 &mut self,
1582 pane: ViewHandle<Pane>,
1583 direction: SplitDirection,
1584 cx: &mut ViewContext<Self>,
1585 ) -> Option<ViewHandle<Pane>> {
1586 if &pane == self.dock_pane() {
1587 warn!("Can't split dock pane.");
1588 return None;
1589 }
1590
1591 let item = pane.read(cx).active_item()?;
1592 let maybe_pane_handle =
1593 if let Some(clone) = item.clone_on_split(self.database_id(), cx.as_mut()) {
1594 let new_pane = self.add_pane(cx);
1595 Pane::add_item(self, &new_pane, clone, true, true, None, cx);
1596 self.center.split(&pane, &new_pane, direction).unwrap();
1597 Some(new_pane)
1598 } else {
1599 None
1600 };
1601 cx.notify();
1602 maybe_pane_handle
1603 }
1604
1605 pub fn split_pane_with_item(&mut self, action: &SplitWithItem, cx: &mut ViewContext<Self>) {
1606 let Some(pane_to_split) = action.pane_to_split.upgrade(cx) else { return; };
1607 let Some(from) = action.from.upgrade(cx) else { return; };
1608 if &pane_to_split == self.dock_pane() {
1609 warn!("Can't split dock pane.");
1610 return;
1611 }
1612
1613 let new_pane = self.add_pane(cx);
1614 Pane::move_item(
1615 self,
1616 from.clone(),
1617 new_pane.clone(),
1618 action.item_id_to_move,
1619 0,
1620 cx,
1621 );
1622 self.center
1623 .split(&pane_to_split, &new_pane, action.split_direction)
1624 .unwrap();
1625 cx.notify();
1626 }
1627
1628 pub fn split_pane_with_project_entry(
1629 &mut self,
1630 action: &SplitWithProjectEntry,
1631 cx: &mut ViewContext<Self>,
1632 ) -> Option<Task<Result<()>>> {
1633 let pane_to_split = action.pane_to_split.upgrade(cx)?;
1634 if &pane_to_split == self.dock_pane() {
1635 warn!("Can't split dock pane.");
1636 return None;
1637 }
1638
1639 let new_pane = self.add_pane(cx);
1640 self.center
1641 .split(&pane_to_split, &new_pane, action.split_direction)
1642 .unwrap();
1643
1644 let path = self
1645 .project
1646 .read(cx)
1647 .path_for_entry(action.project_entry, cx)?;
1648 let task = self.open_path(path, Some(new_pane.downgrade()), true, cx);
1649 Some(cx.foreground().spawn(async move {
1650 task.await?;
1651 Ok(())
1652 }))
1653 }
1654
1655 fn remove_pane(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
1656 if self.center.remove(&pane).unwrap() {
1657 self.panes.retain(|p| p != &pane);
1658 cx.focus(self.panes.last().unwrap().clone());
1659 self.unfollow(&pane, cx);
1660 self.last_leaders_by_pane.remove(&pane.downgrade());
1661 for removed_item in pane.read(cx).items() {
1662 self.panes_by_item.remove(&removed_item.id());
1663 }
1664 if self.last_active_center_pane == Some(pane.downgrade()) {
1665 self.last_active_center_pane = None;
1666 }
1667
1668 cx.notify();
1669 } else {
1670 self.active_item_path_changed(cx);
1671 }
1672 }
1673
1674 pub fn panes(&self) -> &[ViewHandle<Pane>] {
1675 &self.panes
1676 }
1677
1678 fn pane(&self, pane_id: usize) -> Option<ViewHandle<Pane>> {
1679 self.panes.iter().find(|pane| pane.id() == pane_id).cloned()
1680 }
1681
1682 pub fn active_pane(&self) -> &ViewHandle<Pane> {
1683 &self.active_pane
1684 }
1685
1686 pub fn dock_pane(&self) -> &ViewHandle<Pane> {
1687 self.dock.pane()
1688 }
1689
1690 fn dock_active(&self) -> bool {
1691 &self.active_pane == self.dock.pane()
1692 }
1693
1694 fn project_remote_id_changed(&mut self, remote_id: Option<u64>, cx: &mut ViewContext<Self>) {
1695 if let Some(remote_id) = remote_id {
1696 self.remote_entity_subscription =
1697 Some(self.client.add_view_for_remote_entity(remote_id, cx));
1698 } else {
1699 self.remote_entity_subscription.take();
1700 }
1701 }
1702
1703 fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
1704 self.leader_state.followers.remove(&peer_id);
1705 if let Some(states_by_pane) = self.follower_states_by_leader.remove(&peer_id) {
1706 for state in states_by_pane.into_values() {
1707 for item in state.items_by_leader_view_id.into_values() {
1708 item.set_leader_replica_id(None, cx);
1709 }
1710 }
1711 }
1712 cx.notify();
1713 }
1714
1715 pub fn toggle_follow(
1716 &mut self,
1717 ToggleFollow(leader_id): &ToggleFollow,
1718 cx: &mut ViewContext<Self>,
1719 ) -> Option<Task<Result<()>>> {
1720 let leader_id = *leader_id;
1721 let pane = self.active_pane().clone();
1722
1723 if let Some(prev_leader_id) = self.unfollow(&pane, cx) {
1724 if leader_id == prev_leader_id {
1725 return None;
1726 }
1727 }
1728
1729 self.last_leaders_by_pane
1730 .insert(pane.downgrade(), leader_id);
1731 self.follower_states_by_leader
1732 .entry(leader_id)
1733 .or_default()
1734 .insert(pane.clone(), Default::default());
1735 cx.notify();
1736
1737 let project_id = self.project.read(cx).remote_id()?;
1738 let request = self.client.request(proto::Follow {
1739 project_id,
1740 leader_id: Some(leader_id),
1741 });
1742
1743 Some(cx.spawn_weak(|this, mut cx| async move {
1744 let response = request.await?;
1745 if let Some(this) = this.upgrade(&cx) {
1746 this.update(&mut cx, |this, _| {
1747 let state = this
1748 .follower_states_by_leader
1749 .get_mut(&leader_id)
1750 .and_then(|states_by_pane| states_by_pane.get_mut(&pane))
1751 .ok_or_else(|| anyhow!("following interrupted"))?;
1752 state.active_view_id = if let Some(active_view_id) = response.active_view_id {
1753 Some(ViewId::from_proto(active_view_id)?)
1754 } else {
1755 None
1756 };
1757 Ok::<_, anyhow::Error>(())
1758 })?;
1759 Self::add_views_from_leader(
1760 this.clone(),
1761 leader_id,
1762 vec![pane],
1763 response.views,
1764 &mut cx,
1765 )
1766 .await?;
1767 this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx));
1768 }
1769 Ok(())
1770 }))
1771 }
1772
1773 pub fn follow_next_collaborator(
1774 &mut self,
1775 _: &FollowNextCollaborator,
1776 cx: &mut ViewContext<Self>,
1777 ) -> Option<Task<Result<()>>> {
1778 let collaborators = self.project.read(cx).collaborators();
1779 let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
1780 let mut collaborators = collaborators.keys().copied();
1781 for peer_id in collaborators.by_ref() {
1782 if peer_id == leader_id {
1783 break;
1784 }
1785 }
1786 collaborators.next()
1787 } else if let Some(last_leader_id) =
1788 self.last_leaders_by_pane.get(&self.active_pane.downgrade())
1789 {
1790 if collaborators.contains_key(last_leader_id) {
1791 Some(*last_leader_id)
1792 } else {
1793 None
1794 }
1795 } else {
1796 None
1797 };
1798
1799 next_leader_id
1800 .or_else(|| collaborators.keys().copied().next())
1801 .and_then(|leader_id| self.toggle_follow(&ToggleFollow(leader_id), cx))
1802 }
1803
1804 pub fn unfollow(
1805 &mut self,
1806 pane: &ViewHandle<Pane>,
1807 cx: &mut ViewContext<Self>,
1808 ) -> Option<PeerId> {
1809 for (leader_id, states_by_pane) in &mut self.follower_states_by_leader {
1810 let leader_id = *leader_id;
1811 if let Some(state) = states_by_pane.remove(pane) {
1812 for (_, item) in state.items_by_leader_view_id {
1813 item.set_leader_replica_id(None, cx);
1814 }
1815
1816 if states_by_pane.is_empty() {
1817 self.follower_states_by_leader.remove(&leader_id);
1818 if let Some(project_id) = self.project.read(cx).remote_id() {
1819 self.client
1820 .send(proto::Unfollow {
1821 project_id,
1822 leader_id: Some(leader_id),
1823 })
1824 .log_err();
1825 }
1826 }
1827
1828 cx.notify();
1829 return Some(leader_id);
1830 }
1831 }
1832 None
1833 }
1834
1835 pub fn is_following(&self, peer_id: PeerId) -> bool {
1836 self.follower_states_by_leader.contains_key(&peer_id)
1837 }
1838
1839 pub fn is_followed(&self, peer_id: PeerId) -> bool {
1840 self.leader_state.followers.contains(&peer_id)
1841 }
1842
1843 fn render_titlebar(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
1844 let project = &self.project.read(cx);
1845 let mut worktree_root_names = String::new();
1846 for (i, name) in project.worktree_root_names(cx).enumerate() {
1847 if i > 0 {
1848 worktree_root_names.push_str(", ");
1849 }
1850 worktree_root_names.push_str(name);
1851 }
1852
1853 // TODO: There should be a better system in place for this
1854 // (https://github.com/zed-industries/zed/issues/1290)
1855 let is_fullscreen = cx.window_is_fullscreen(cx.window_id());
1856 let container_theme = if is_fullscreen {
1857 let mut container_theme = theme.workspace.titlebar.container;
1858 container_theme.padding.left = container_theme.padding.right;
1859 container_theme
1860 } else {
1861 theme.workspace.titlebar.container
1862 };
1863
1864 enum TitleBar {}
1865 ConstrainedBox::new(
1866 MouseEventHandler::<TitleBar>::new(0, cx, |_, cx| {
1867 Container::new(
1868 Stack::new()
1869 .with_child(
1870 Label::new(worktree_root_names, theme.workspace.titlebar.title.clone())
1871 .aligned()
1872 .left()
1873 .boxed(),
1874 )
1875 .with_children(
1876 self.titlebar_item
1877 .as_ref()
1878 .map(|item| ChildView::new(item, cx).aligned().right().boxed()),
1879 )
1880 .boxed(),
1881 )
1882 .with_style(container_theme)
1883 .boxed()
1884 })
1885 .on_click(MouseButton::Left, |event, cx| {
1886 if event.click_count == 2 {
1887 cx.zoom_window(cx.window_id());
1888 }
1889 })
1890 .boxed(),
1891 )
1892 .with_height(theme.workspace.titlebar.height)
1893 .named("titlebar")
1894 }
1895
1896 fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
1897 let active_entry = self.active_project_path(cx);
1898 self.project
1899 .update(cx, |project, cx| project.set_active_path(active_entry, cx));
1900 self.update_window_title(cx);
1901 }
1902
1903 fn update_window_title(&mut self, cx: &mut ViewContext<Self>) {
1904 let project = self.project().read(cx);
1905 let mut title = String::new();
1906
1907 if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
1908 let filename = path
1909 .path
1910 .file_name()
1911 .map(|s| s.to_string_lossy())
1912 .or_else(|| {
1913 Some(Cow::Borrowed(
1914 project
1915 .worktree_for_id(path.worktree_id, cx)?
1916 .read(cx)
1917 .root_name(),
1918 ))
1919 });
1920
1921 if let Some(filename) = filename {
1922 title.push_str(filename.as_ref());
1923 title.push_str(" — ");
1924 }
1925 }
1926
1927 for (i, name) in project.worktree_root_names(cx).enumerate() {
1928 if i > 0 {
1929 title.push_str(", ");
1930 }
1931 title.push_str(name);
1932 }
1933
1934 if title.is_empty() {
1935 title = "empty project".to_string();
1936 }
1937
1938 if project.is_remote() {
1939 title.push_str(" ↙");
1940 } else if project.is_shared() {
1941 title.push_str(" ↗");
1942 }
1943
1944 cx.set_window_title(&title);
1945 }
1946
1947 fn update_window_edited(&mut self, cx: &mut ViewContext<Self>) {
1948 let is_edited = !self.project.read(cx).is_read_only()
1949 && self
1950 .items(cx)
1951 .any(|item| item.has_conflict(cx) || item.is_dirty(cx));
1952 if is_edited != self.window_edited {
1953 self.window_edited = is_edited;
1954 cx.set_window_edited(self.window_edited)
1955 }
1956 }
1957
1958 fn render_disconnected_overlay(&self, cx: &mut RenderContext<Workspace>) -> Option<ElementBox> {
1959 if self.project.read(cx).is_read_only() {
1960 enum DisconnectedOverlay {}
1961 Some(
1962 MouseEventHandler::<DisconnectedOverlay>::new(0, cx, |_, cx| {
1963 let theme = &cx.global::<Settings>().theme;
1964 Label::new(
1965 "Your connection to the remote project has been lost.",
1966 theme.workspace.disconnected_overlay.text.clone(),
1967 )
1968 .aligned()
1969 .contained()
1970 .with_style(theme.workspace.disconnected_overlay.container)
1971 .boxed()
1972 })
1973 .with_cursor_style(CursorStyle::Arrow)
1974 .capture_all()
1975 .boxed(),
1976 )
1977 } else {
1978 None
1979 }
1980 }
1981
1982 fn render_notifications(
1983 &self,
1984 theme: &theme::Workspace,
1985 cx: &AppContext,
1986 ) -> Option<ElementBox> {
1987 if self.notifications.is_empty() {
1988 None
1989 } else {
1990 Some(
1991 Flex::column()
1992 .with_children(self.notifications.iter().map(|(_, _, notification)| {
1993 ChildView::new(notification.as_ref(), cx)
1994 .contained()
1995 .with_style(theme.notification)
1996 .boxed()
1997 }))
1998 .constrained()
1999 .with_width(theme.notifications.width)
2000 .contained()
2001 .with_style(theme.notifications.container)
2002 .aligned()
2003 .bottom()
2004 .right()
2005 .boxed(),
2006 )
2007 }
2008 }
2009
2010 // RPC handlers
2011
2012 async fn handle_follow(
2013 this: ViewHandle<Self>,
2014 envelope: TypedEnvelope<proto::Follow>,
2015 _: Arc<Client>,
2016 mut cx: AsyncAppContext,
2017 ) -> Result<proto::FollowResponse> {
2018 this.update(&mut cx, |this, cx| {
2019 let client = &this.client;
2020 this.leader_state
2021 .followers
2022 .insert(envelope.original_sender_id()?);
2023
2024 let active_view_id = this.active_item(cx).and_then(|i| {
2025 Some(
2026 i.to_followable_item_handle(cx)?
2027 .remote_id(client, cx)?
2028 .to_proto(),
2029 )
2030 });
2031
2032 cx.notify();
2033
2034 Ok(proto::FollowResponse {
2035 active_view_id,
2036 views: this
2037 .panes()
2038 .iter()
2039 .flat_map(|pane| {
2040 let leader_id = this.leader_for_pane(pane);
2041 pane.read(cx).items().filter_map({
2042 let cx = &cx;
2043 move |item| {
2044 let item = item.to_followable_item_handle(cx)?;
2045 let id = item.remote_id(client, cx)?.to_proto();
2046 let variant = item.to_state_proto(cx)?;
2047 Some(proto::View {
2048 id: Some(id),
2049 leader_id,
2050 variant: Some(variant),
2051 })
2052 }
2053 })
2054 })
2055 .collect(),
2056 })
2057 })
2058 }
2059
2060 async fn handle_unfollow(
2061 this: ViewHandle<Self>,
2062 envelope: TypedEnvelope<proto::Unfollow>,
2063 _: Arc<Client>,
2064 mut cx: AsyncAppContext,
2065 ) -> Result<()> {
2066 this.update(&mut cx, |this, cx| {
2067 this.leader_state
2068 .followers
2069 .remove(&envelope.original_sender_id()?);
2070 cx.notify();
2071 Ok(())
2072 })
2073 }
2074
2075 async fn handle_update_followers(
2076 this: ViewHandle<Self>,
2077 envelope: TypedEnvelope<proto::UpdateFollowers>,
2078 _: Arc<Client>,
2079 cx: AsyncAppContext,
2080 ) -> Result<()> {
2081 let leader_id = envelope.original_sender_id()?;
2082 this.read_with(&cx, |this, _| {
2083 this.leader_updates_tx
2084 .unbounded_send((leader_id, envelope.payload))
2085 })?;
2086 Ok(())
2087 }
2088
2089 async fn process_leader_update(
2090 this: ViewHandle<Self>,
2091 leader_id: PeerId,
2092 update: proto::UpdateFollowers,
2093 cx: &mut AsyncAppContext,
2094 ) -> Result<()> {
2095 match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
2096 proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
2097 this.update(cx, |this, _| {
2098 if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
2099 for state in state.values_mut() {
2100 state.active_view_id =
2101 if let Some(active_view_id) = update_active_view.id.clone() {
2102 Some(ViewId::from_proto(active_view_id)?)
2103 } else {
2104 None
2105 };
2106 }
2107 }
2108 anyhow::Ok(())
2109 })?;
2110 }
2111 proto::update_followers::Variant::UpdateView(update_view) => {
2112 let variant = update_view
2113 .variant
2114 .ok_or_else(|| anyhow!("missing update view variant"))?;
2115 let id = update_view
2116 .id
2117 .ok_or_else(|| anyhow!("missing update view id"))?;
2118 let mut tasks = Vec::new();
2119 this.update(cx, |this, cx| {
2120 let project = this.project.clone();
2121 if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
2122 for state in state.values_mut() {
2123 let view_id = ViewId::from_proto(id.clone())?;
2124 if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
2125 tasks.push(item.apply_update_proto(&project, variant.clone(), cx));
2126 }
2127 }
2128 }
2129 anyhow::Ok(())
2130 })?;
2131 try_join_all(tasks).await.log_err();
2132 }
2133 proto::update_followers::Variant::CreateView(view) => {
2134 let panes = this.read_with(cx, |this, _| {
2135 this.follower_states_by_leader
2136 .get(&leader_id)
2137 .into_iter()
2138 .flat_map(|states_by_pane| states_by_pane.keys())
2139 .cloned()
2140 .collect()
2141 });
2142 Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], cx).await?;
2143 }
2144 }
2145 this.update(cx, |this, cx| this.leader_updated(leader_id, cx));
2146 Ok(())
2147 }
2148
2149 async fn add_views_from_leader(
2150 this: ViewHandle<Self>,
2151 leader_id: PeerId,
2152 panes: Vec<ViewHandle<Pane>>,
2153 views: Vec<proto::View>,
2154 cx: &mut AsyncAppContext,
2155 ) -> Result<()> {
2156 let project = this.read_with(cx, |this, _| this.project.clone());
2157 let replica_id = project
2158 .read_with(cx, |project, _| {
2159 project
2160 .collaborators()
2161 .get(&leader_id)
2162 .map(|c| c.replica_id)
2163 })
2164 .ok_or_else(|| anyhow!("no such collaborator {}", leader_id))?;
2165
2166 let item_builders = cx.update(|cx| {
2167 cx.default_global::<FollowableItemBuilders>()
2168 .values()
2169 .map(|b| b.0)
2170 .collect::<Vec<_>>()
2171 });
2172
2173 let mut item_tasks_by_pane = HashMap::default();
2174 for pane in panes {
2175 let mut item_tasks = Vec::new();
2176 let mut leader_view_ids = Vec::new();
2177 for view in &views {
2178 let Some(id) = &view.id else { continue };
2179 let id = ViewId::from_proto(id.clone())?;
2180 let mut variant = view.variant.clone();
2181 if variant.is_none() {
2182 Err(anyhow!("missing variant"))?;
2183 }
2184 for build_item in &item_builders {
2185 let task = cx.update(|cx| {
2186 build_item(pane.clone(), project.clone(), id, &mut variant, cx)
2187 });
2188 if let Some(task) = task {
2189 item_tasks.push(task);
2190 leader_view_ids.push(id);
2191 break;
2192 } else {
2193 assert!(variant.is_some());
2194 }
2195 }
2196 }
2197
2198 item_tasks_by_pane.insert(pane, (item_tasks, leader_view_ids));
2199 }
2200
2201 for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane {
2202 let items = futures::future::try_join_all(item_tasks).await?;
2203 this.update(cx, |this, cx| {
2204 let state = this
2205 .follower_states_by_leader
2206 .get_mut(&leader_id)?
2207 .get_mut(&pane)?;
2208
2209 for (id, item) in leader_view_ids.into_iter().zip(items) {
2210 item.set_leader_replica_id(Some(replica_id), cx);
2211 state.items_by_leader_view_id.insert(id, item);
2212 }
2213
2214 Some(())
2215 });
2216 }
2217 Ok(())
2218 }
2219
2220 fn update_followers(
2221 &self,
2222 update: proto::update_followers::Variant,
2223 cx: &AppContext,
2224 ) -> Option<()> {
2225 let project_id = self.project.read(cx).remote_id()?;
2226 if !self.leader_state.followers.is_empty() {
2227 self.client
2228 .send(proto::UpdateFollowers {
2229 project_id,
2230 follower_ids: self.leader_state.followers.iter().copied().collect(),
2231 variant: Some(update),
2232 })
2233 .log_err();
2234 }
2235 None
2236 }
2237
2238 pub fn leader_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<PeerId> {
2239 self.follower_states_by_leader
2240 .iter()
2241 .find_map(|(leader_id, state)| {
2242 if state.contains_key(pane) {
2243 Some(*leader_id)
2244 } else {
2245 None
2246 }
2247 })
2248 }
2249
2250 fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
2251 cx.notify();
2252
2253 let call = self.active_call()?;
2254 let room = call.read(cx).room()?.read(cx);
2255 let participant = room.remote_participant_for_peer_id(leader_id)?;
2256 let mut items_to_activate = Vec::new();
2257 match participant.location {
2258 call::ParticipantLocation::SharedProject { project_id } => {
2259 if Some(project_id) == self.project.read(cx).remote_id() {
2260 for (pane, state) in self.follower_states_by_leader.get(&leader_id)? {
2261 if let Some(item) = state
2262 .active_view_id
2263 .and_then(|id| state.items_by_leader_view_id.get(&id))
2264 {
2265 items_to_activate.push((pane.clone(), item.boxed_clone()));
2266 } else {
2267 if let Some(shared_screen) =
2268 self.shared_screen_for_peer(leader_id, pane, cx)
2269 {
2270 items_to_activate.push((pane.clone(), Box::new(shared_screen)));
2271 }
2272 }
2273 }
2274 }
2275 }
2276 call::ParticipantLocation::UnsharedProject => {}
2277 call::ParticipantLocation::External => {
2278 for (pane, _) in self.follower_states_by_leader.get(&leader_id)? {
2279 if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
2280 items_to_activate.push((pane.clone(), Box::new(shared_screen)));
2281 }
2282 }
2283 }
2284 }
2285
2286 for (pane, item) in items_to_activate {
2287 let active_item_was_focused = pane
2288 .read(cx)
2289 .active_item()
2290 .map(|active_item| cx.is_child_focused(active_item.to_any()))
2291 .unwrap_or_default();
2292
2293 if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) {
2294 pane.update(cx, |pane, cx| pane.activate_item(index, false, false, cx));
2295 } else {
2296 Pane::add_item(self, &pane, item.boxed_clone(), false, false, None, cx);
2297 }
2298
2299 if active_item_was_focused {
2300 pane.update(cx, |pane, cx| pane.focus_active_item(cx));
2301 }
2302 }
2303
2304 None
2305 }
2306
2307 fn shared_screen_for_peer(
2308 &self,
2309 peer_id: PeerId,
2310 pane: &ViewHandle<Pane>,
2311 cx: &mut ViewContext<Self>,
2312 ) -> Option<ViewHandle<SharedScreen>> {
2313 let call = self.active_call()?;
2314 let room = call.read(cx).room()?.read(cx);
2315 let participant = room.remote_participant_for_peer_id(peer_id)?;
2316 let track = participant.tracks.values().next()?.clone();
2317 let user = participant.user.clone();
2318
2319 for item in pane.read(cx).items_of_type::<SharedScreen>() {
2320 if item.read(cx).peer_id == peer_id {
2321 return Some(item);
2322 }
2323 }
2324
2325 Some(cx.add_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx)))
2326 }
2327
2328 pub fn on_window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
2329 if active {
2330 cx.background()
2331 .spawn(persistence::DB.update_timestamp(self.database_id()))
2332 .detach();
2333 } else {
2334 for pane in &self.panes {
2335 pane.update(cx, |pane, cx| {
2336 if let Some(item) = pane.active_item() {
2337 item.workspace_deactivated(cx);
2338 }
2339 if matches!(
2340 cx.global::<Settings>().autosave,
2341 Autosave::OnWindowChange | Autosave::OnFocusChange
2342 ) {
2343 for item in pane.items() {
2344 Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
2345 .detach_and_log_err(cx);
2346 }
2347 }
2348 });
2349 }
2350 }
2351 }
2352
2353 fn active_call(&self) -> Option<&ModelHandle<ActiveCall>> {
2354 self.active_call.as_ref().map(|(call, _)| call)
2355 }
2356
2357 fn on_active_call_event(
2358 &mut self,
2359 _: ModelHandle<ActiveCall>,
2360 event: &call::room::Event,
2361 cx: &mut ViewContext<Self>,
2362 ) {
2363 match event {
2364 call::room::Event::ParticipantLocationChanged { participant_id }
2365 | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
2366 self.leader_updated(*participant_id, cx);
2367 }
2368 _ => {}
2369 }
2370 }
2371
2372 pub fn database_id(&self) -> WorkspaceId {
2373 self.database_id
2374 }
2375
2376 fn location(&self, cx: &AppContext) -> Option<WorkspaceLocation> {
2377 let project = self.project().read(cx);
2378
2379 if project.is_local() {
2380 Some(
2381 project
2382 .visible_worktrees(cx)
2383 .map(|worktree| worktree.read(cx).abs_path())
2384 .collect::<Vec<_>>()
2385 .into(),
2386 )
2387 } else {
2388 None
2389 }
2390 }
2391
2392 fn remove_panes(&mut self, member: Member, cx: &mut ViewContext<Workspace>) {
2393 match member {
2394 Member::Axis(PaneAxis { members, .. }) => {
2395 for child in members.iter() {
2396 self.remove_panes(child.clone(), cx)
2397 }
2398 }
2399 Member::Pane(pane) => self.remove_pane(pane.clone(), cx),
2400 }
2401 }
2402
2403 fn serialize_workspace(&self, cx: &AppContext) {
2404 fn serialize_pane_handle(
2405 pane_handle: &ViewHandle<Pane>,
2406 cx: &AppContext,
2407 ) -> SerializedPane {
2408 let (items, active) = {
2409 let pane = pane_handle.read(cx);
2410 let active_item_id = pane.active_item().map(|item| item.id());
2411 (
2412 pane.items()
2413 .filter_map(|item_handle| {
2414 Some(SerializedItem {
2415 kind: Arc::from(item_handle.serialized_item_kind()?),
2416 item_id: item_handle.id(),
2417 active: Some(item_handle.id()) == active_item_id,
2418 })
2419 })
2420 .collect::<Vec<_>>(),
2421 pane.is_active(),
2422 )
2423 };
2424
2425 SerializedPane::new(items, active)
2426 }
2427
2428 fn build_serialized_pane_group(
2429 pane_group: &Member,
2430 cx: &AppContext,
2431 ) -> SerializedPaneGroup {
2432 match pane_group {
2433 Member::Axis(PaneAxis { axis, members }) => SerializedPaneGroup::Group {
2434 axis: *axis,
2435 children: members
2436 .iter()
2437 .map(|member| build_serialized_pane_group(member, cx))
2438 .collect::<Vec<_>>(),
2439 },
2440 Member::Pane(pane_handle) => {
2441 SerializedPaneGroup::Pane(serialize_pane_handle(&pane_handle, cx))
2442 }
2443 }
2444 }
2445
2446 if let Some(location) = self.location(cx) {
2447 // Load bearing special case:
2448 // - with_local_workspace() relies on this to not have other stuff open
2449 // when you open your log
2450 if !location.paths().is_empty() {
2451 let dock_pane = serialize_pane_handle(self.dock.pane(), cx);
2452 let center_group = build_serialized_pane_group(&self.center.root, cx);
2453
2454 let serialized_workspace = SerializedWorkspace {
2455 id: self.database_id,
2456 location,
2457 dock_position: self.dock.position(),
2458 dock_pane,
2459 center_group,
2460 left_sidebar_open: self.left_sidebar.read(cx).is_open(),
2461 bounds: Default::default(),
2462 display: Default::default(),
2463 };
2464
2465 cx.background()
2466 .spawn(persistence::DB.save_workspace(serialized_workspace))
2467 .detach();
2468 }
2469 }
2470 }
2471
2472 fn load_from_serialized_workspace(
2473 workspace: WeakViewHandle<Workspace>,
2474 serialized_workspace: SerializedWorkspace,
2475 cx: &mut MutableAppContext,
2476 ) {
2477 cx.spawn(|mut cx| async move {
2478 if let Some(workspace) = workspace.upgrade(&cx) {
2479 let (project, dock_pane_handle, old_center_pane) =
2480 workspace.read_with(&cx, |workspace, _| {
2481 (
2482 workspace.project().clone(),
2483 workspace.dock_pane().clone(),
2484 workspace.last_active_center_pane.clone(),
2485 )
2486 });
2487
2488 serialized_workspace
2489 .dock_pane
2490 .deserialize_to(
2491 &project,
2492 &dock_pane_handle,
2493 serialized_workspace.id,
2494 &workspace,
2495 &mut cx,
2496 )
2497 .await;
2498
2499 // Traverse the splits tree and add to things
2500 let center_group = serialized_workspace
2501 .center_group
2502 .deserialize(&project, serialized_workspace.id, &workspace, &mut cx)
2503 .await;
2504
2505 // Remove old panes from workspace panes list
2506 workspace.update(&mut cx, |workspace, cx| {
2507 if let Some((center_group, active_pane)) = center_group {
2508 workspace.remove_panes(workspace.center.root.clone(), cx);
2509
2510 // Swap workspace center group
2511 workspace.center = PaneGroup::with_root(center_group);
2512
2513 // Change the focus to the workspace first so that we retrigger focus in on the pane.
2514 cx.focus_self();
2515
2516 if let Some(active_pane) = active_pane {
2517 cx.focus(active_pane);
2518 } else {
2519 cx.focus(workspace.panes.last().unwrap().clone());
2520 }
2521 } else {
2522 let old_center_handle = old_center_pane.and_then(|weak| weak.upgrade(cx));
2523 if let Some(old_center_handle) = old_center_handle {
2524 cx.focus(old_center_handle)
2525 } else {
2526 cx.focus_self()
2527 }
2528 }
2529
2530 if workspace.left_sidebar().read(cx).is_open()
2531 != serialized_workspace.left_sidebar_open
2532 {
2533 workspace.toggle_sidebar(SidebarSide::Left, cx);
2534 }
2535
2536 // Note that without after_window, the focus_self() and
2537 // the focus the dock generates start generating alternating
2538 // focus due to the deferred execution each triggering each other
2539 cx.after_window_update(move |workspace, cx| {
2540 Dock::set_dock_position(workspace, serialized_workspace.dock_position, cx);
2541 });
2542
2543 cx.notify();
2544 });
2545
2546 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
2547 workspace.read_with(&cx, |workspace, cx| workspace.serialize_workspace(cx))
2548 }
2549 })
2550 .detach();
2551 }
2552}
2553
2554fn notify_if_database_failed(workspace: &ViewHandle<Workspace>, cx: &mut AsyncAppContext) {
2555 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
2556 workspace.update(cx, |workspace, cx| {
2557 workspace.show_notification_once(0, cx, |cx| {
2558 cx.add_view(|_| {
2559 MessageNotification::new(
2560 indoc::indoc! {"
2561 Failed to load any database file :(
2562 "},
2563 OsOpen("https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml".to_string()),
2564 "Click to let us know about this error"
2565 )
2566 })
2567 });
2568 });
2569 } else {
2570 let backup_path = (*db::BACKUP_DB_PATH).read();
2571 if let Some(backup_path) = &*backup_path {
2572 workspace.update(cx, |workspace, cx| {
2573 workspace.show_notification_once(0, cx, |cx| {
2574 cx.add_view(|_| {
2575 let backup_path = backup_path.to_string_lossy();
2576 MessageNotification::new(
2577 format!(
2578 indoc::indoc! {"
2579 Database file was corrupted :(
2580 Old database backed up to:
2581 {}
2582 "},
2583 backup_path
2584 ),
2585 OsOpen(backup_path.to_string()),
2586 "Click to show old database in finder",
2587 )
2588 })
2589 });
2590 });
2591 }
2592 }
2593}
2594
2595impl Entity for Workspace {
2596 type Event = Event;
2597}
2598
2599impl View for Workspace {
2600 fn ui_name() -> &'static str {
2601 "Workspace"
2602 }
2603
2604 fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
2605 let theme = cx.global::<Settings>().theme.clone();
2606 Stack::new()
2607 .with_child(
2608 Flex::column()
2609 .with_child(self.render_titlebar(&theme, cx))
2610 .with_child(
2611 Stack::new()
2612 .with_child({
2613 let project = self.project.clone();
2614 Flex::row()
2615 .with_children(
2616 if self.left_sidebar.read(cx).active_item().is_some() {
2617 Some(
2618 ChildView::new(&self.left_sidebar, cx)
2619 .constrained()
2620 .dynamically(|constraint, cx| {
2621 SizeConstraint::new(
2622 Vector2F::new(20., constraint.min.y()),
2623 Vector2F::new(
2624 cx.window_size.x() * 0.8,
2625 constraint.max.y(),
2626 ),
2627 )
2628 })
2629 .boxed(),
2630 )
2631 } else {
2632 None
2633 },
2634 )
2635 .with_child(
2636 FlexItem::new(
2637 Flex::column()
2638 .with_child(
2639 FlexItem::new(self.center.render(
2640 &project,
2641 &theme,
2642 &self.follower_states_by_leader,
2643 self.active_call(),
2644 self.active_pane(),
2645 cx,
2646 ))
2647 .flex(1., true)
2648 .boxed(),
2649 )
2650 .with_children(self.dock.render(
2651 &theme,
2652 DockAnchor::Bottom,
2653 cx,
2654 ))
2655 .boxed(),
2656 )
2657 .flex(1., true)
2658 .boxed(),
2659 )
2660 .with_children(self.dock.render(&theme, DockAnchor::Right, cx))
2661 .with_children(
2662 if self.right_sidebar.read(cx).active_item().is_some() {
2663 Some(
2664 ChildView::new(&self.right_sidebar, cx)
2665 .constrained()
2666 .dynamically(|constraint, cx| {
2667 SizeConstraint::new(
2668 Vector2F::new(20., constraint.min.y()),
2669 Vector2F::new(
2670 cx.window_size.x() * 0.8,
2671 constraint.max.y(),
2672 ),
2673 )
2674 })
2675 .boxed(),
2676 )
2677 } else {
2678 None
2679 },
2680 )
2681 .boxed()
2682 })
2683 .with_child(
2684 Overlay::new(
2685 Stack::new()
2686 .with_children(self.dock.render(
2687 &theme,
2688 DockAnchor::Expanded,
2689 cx,
2690 ))
2691 .with_children(self.modal.as_ref().map(|modal| {
2692 ChildView::new(modal, cx)
2693 .contained()
2694 .with_style(theme.workspace.modal)
2695 .aligned()
2696 .top()
2697 .boxed()
2698 }))
2699 .with_children(
2700 self.render_notifications(&theme.workspace, cx),
2701 )
2702 .boxed(),
2703 )
2704 .boxed(),
2705 )
2706 .flex(1.0, true)
2707 .boxed(),
2708 )
2709 .with_child(ChildView::new(&self.status_bar, cx).boxed())
2710 .contained()
2711 .with_background_color(theme.workspace.background)
2712 .boxed(),
2713 )
2714 .with_children(DragAndDrop::render(cx))
2715 .with_children(self.render_disconnected_overlay(cx))
2716 .named("workspace")
2717 }
2718
2719 fn focus_in(&mut self, view: AnyViewHandle, cx: &mut ViewContext<Self>) {
2720 if cx.is_self_focused() {
2721 cx.focus(&self.active_pane);
2722 } else {
2723 for pane in self.panes() {
2724 let view = view.clone();
2725 if pane.update(cx, |_, cx| view.id() == cx.view_id() || cx.is_child(view)) {
2726 self.handle_pane_focused(pane.clone(), cx);
2727 break;
2728 }
2729 }
2730 }
2731 }
2732
2733 fn keymap_context(&self, _: &AppContext) -> KeymapContext {
2734 let mut keymap = Self::default_keymap_context();
2735 if self.active_pane() == self.dock_pane() {
2736 keymap.set.insert("Dock".into());
2737 }
2738 keymap
2739 }
2740}
2741
2742impl ViewId {
2743 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
2744 Ok(Self {
2745 creator: message
2746 .creator
2747 .ok_or_else(|| anyhow!("creator is missing"))?,
2748 id: message.id,
2749 })
2750 }
2751
2752 pub(crate) fn to_proto(&self) -> proto::ViewId {
2753 proto::ViewId {
2754 creator: Some(self.creator),
2755 id: self.id,
2756 }
2757 }
2758}
2759
2760pub trait WorkspaceHandle {
2761 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
2762}
2763
2764impl WorkspaceHandle for ViewHandle<Workspace> {
2765 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
2766 self.read(cx)
2767 .worktrees(cx)
2768 .flat_map(|worktree| {
2769 let worktree_id = worktree.read(cx).id();
2770 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
2771 worktree_id,
2772 path: f.path.clone(),
2773 })
2774 })
2775 .collect::<Vec<_>>()
2776 }
2777}
2778
2779impl std::fmt::Debug for OpenPaths {
2780 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2781 f.debug_struct("OpenPaths")
2782 .field("paths", &self.paths)
2783 .finish()
2784 }
2785}
2786
2787fn open(_: &Open, cx: &mut MutableAppContext) {
2788 let mut paths = cx.prompt_for_paths(PathPromptOptions {
2789 files: true,
2790 directories: true,
2791 multiple: true,
2792 });
2793 cx.spawn(|mut cx| async move {
2794 if let Some(paths) = paths.recv().await.flatten() {
2795 cx.update(|cx| cx.dispatch_global_action(OpenPaths { paths }));
2796 }
2797 })
2798 .detach();
2799}
2800
2801pub struct WorkspaceCreated(WeakViewHandle<Workspace>);
2802
2803pub fn activate_workspace_for_project(
2804 cx: &mut MutableAppContext,
2805 predicate: impl Fn(&mut Project, &mut ModelContext<Project>) -> bool,
2806) -> Option<ViewHandle<Workspace>> {
2807 for window_id in cx.window_ids().collect::<Vec<_>>() {
2808 if let Some(workspace_handle) = cx.root_view::<Workspace>(window_id) {
2809 let project = workspace_handle.read(cx).project.clone();
2810 if project.update(cx, &predicate) {
2811 cx.activate_window(window_id);
2812 return Some(workspace_handle);
2813 }
2814 }
2815 }
2816 None
2817}
2818
2819pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
2820 DB.last_workspace().await.log_err().flatten()
2821}
2822
2823#[allow(clippy::type_complexity)]
2824pub fn open_paths(
2825 abs_paths: &[PathBuf],
2826 app_state: &Arc<AppState>,
2827 cx: &mut MutableAppContext,
2828) -> Task<(
2829 ViewHandle<Workspace>,
2830 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
2831)> {
2832 log::info!("open paths {:?}", abs_paths);
2833
2834 // Open paths in existing workspace if possible
2835 let existing =
2836 activate_workspace_for_project(cx, |project, cx| project.contains_paths(abs_paths, cx));
2837
2838 let app_state = app_state.clone();
2839 let abs_paths = abs_paths.to_vec();
2840 cx.spawn(|mut cx| async move {
2841 if let Some(existing) = existing {
2842 (
2843 existing.clone(),
2844 existing
2845 .update(&mut cx, |workspace, cx| {
2846 workspace.open_paths(abs_paths, true, cx)
2847 })
2848 .await,
2849 )
2850 } else {
2851 let contains_directory =
2852 futures::future::join_all(abs_paths.iter().map(|path| app_state.fs.is_file(path)))
2853 .await
2854 .contains(&false);
2855
2856 cx.update(|cx| {
2857 let task = Workspace::new_local(abs_paths, app_state.clone(), cx);
2858
2859 cx.spawn(|mut cx| async move {
2860 let (workspace, items) = task.await;
2861
2862 workspace.update(&mut cx, |workspace, cx| {
2863 if contains_directory {
2864 workspace.toggle_sidebar(SidebarSide::Left, cx);
2865 }
2866 });
2867
2868 (workspace, items)
2869 })
2870 })
2871 .await
2872 }
2873 })
2874}
2875
2876pub fn open_new(app_state: &Arc<AppState>, cx: &mut MutableAppContext) -> Task<()> {
2877 let task = Workspace::new_local(Vec::new(), app_state.clone(), cx);
2878 cx.spawn(|mut cx| async move {
2879 let (workspace, opened_paths) = task.await;
2880
2881 workspace.update(&mut cx, |_, cx| {
2882 if opened_paths.is_empty() {
2883 cx.dispatch_action(NewFile);
2884 }
2885 })
2886 })
2887}
2888
2889fn parse_pixel_position_env_var(value: &str) -> Option<Vector2F> {
2890 let mut parts = value.split(',');
2891 let width: usize = parts.next()?.parse().ok()?;
2892 let height: usize = parts.next()?.parse().ok()?;
2893 Some(vec2f(width as f32, height as f32))
2894}
2895
2896#[cfg(test)]
2897mod tests {
2898 use std::{cell::RefCell, rc::Rc};
2899
2900 use crate::item::test::{TestItem, TestItemEvent, TestProjectItem};
2901
2902 use super::*;
2903 use fs::FakeFs;
2904 use gpui::{executor::Deterministic, TestAppContext, ViewContext};
2905 use project::{Project, ProjectEntryId};
2906 use serde_json::json;
2907
2908 pub fn default_item_factory(
2909 _workspace: &mut Workspace,
2910 _cx: &mut ViewContext<Workspace>,
2911 ) -> Option<Box<dyn ItemHandle>> {
2912 unimplemented!()
2913 }
2914
2915 #[gpui::test]
2916 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
2917 cx.foreground().forbid_parking();
2918 Settings::test_async(cx);
2919
2920 let fs = FakeFs::new(cx.background());
2921 let project = Project::test(fs, [], cx).await;
2922 let (_, workspace) = cx.add_window(|cx| {
2923 Workspace::new(
2924 Default::default(),
2925 0,
2926 project.clone(),
2927 default_item_factory,
2928 cx,
2929 )
2930 });
2931
2932 // Adding an item with no ambiguity renders the tab without detail.
2933 let item1 = cx.add_view(&workspace, |_| {
2934 let mut item = TestItem::new();
2935 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
2936 item
2937 });
2938 workspace.update(cx, |workspace, cx| {
2939 workspace.add_item(Box::new(item1.clone()), cx);
2940 });
2941 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), None));
2942
2943 // Adding an item that creates ambiguity increases the level of detail on
2944 // both tabs.
2945 let item2 = cx.add_view(&workspace, |_| {
2946 let mut item = TestItem::new();
2947 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
2948 item
2949 });
2950 workspace.update(cx, |workspace, cx| {
2951 workspace.add_item(Box::new(item2.clone()), cx);
2952 });
2953 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
2954 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
2955
2956 // Adding an item that creates ambiguity increases the level of detail only
2957 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
2958 // we stop at the highest detail available.
2959 let item3 = cx.add_view(&workspace, |_| {
2960 let mut item = TestItem::new();
2961 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
2962 item
2963 });
2964 workspace.update(cx, |workspace, cx| {
2965 workspace.add_item(Box::new(item3.clone()), cx);
2966 });
2967 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
2968 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
2969 item3.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
2970 }
2971
2972 #[gpui::test]
2973 async fn test_tracking_active_path(cx: &mut TestAppContext) {
2974 cx.foreground().forbid_parking();
2975 Settings::test_async(cx);
2976 let fs = FakeFs::new(cx.background());
2977 fs.insert_tree(
2978 "/root1",
2979 json!({
2980 "one.txt": "",
2981 "two.txt": "",
2982 }),
2983 )
2984 .await;
2985 fs.insert_tree(
2986 "/root2",
2987 json!({
2988 "three.txt": "",
2989 }),
2990 )
2991 .await;
2992
2993 let project = Project::test(fs, ["root1".as_ref()], cx).await;
2994 let (window_id, workspace) = cx.add_window(|cx| {
2995 Workspace::new(
2996 Default::default(),
2997 0,
2998 project.clone(),
2999 default_item_factory,
3000 cx,
3001 )
3002 });
3003 let worktree_id = project.read_with(cx, |project, cx| {
3004 project.worktrees(cx).next().unwrap().read(cx).id()
3005 });
3006
3007 let item1 = cx.add_view(&workspace, |cx| {
3008 TestItem::new().with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
3009 });
3010 let item2 = cx.add_view(&workspace, |cx| {
3011 TestItem::new().with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
3012 });
3013
3014 // Add an item to an empty pane
3015 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx));
3016 project.read_with(cx, |project, cx| {
3017 assert_eq!(
3018 project.active_entry(),
3019 project
3020 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
3021 .map(|e| e.id)
3022 );
3023 });
3024 assert_eq!(
3025 cx.current_window_title(window_id).as_deref(),
3026 Some("one.txt — root1")
3027 );
3028
3029 // Add a second item to a non-empty pane
3030 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
3031 assert_eq!(
3032 cx.current_window_title(window_id).as_deref(),
3033 Some("two.txt — root1")
3034 );
3035 project.read_with(cx, |project, cx| {
3036 assert_eq!(
3037 project.active_entry(),
3038 project
3039 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
3040 .map(|e| e.id)
3041 );
3042 });
3043
3044 // Close the active item
3045 workspace
3046 .update(cx, |workspace, cx| {
3047 Pane::close_active_item(workspace, &Default::default(), cx).unwrap()
3048 })
3049 .await
3050 .unwrap();
3051 assert_eq!(
3052 cx.current_window_title(window_id).as_deref(),
3053 Some("one.txt — root1")
3054 );
3055 project.read_with(cx, |project, cx| {
3056 assert_eq!(
3057 project.active_entry(),
3058 project
3059 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
3060 .map(|e| e.id)
3061 );
3062 });
3063
3064 // Add a project folder
3065 project
3066 .update(cx, |project, cx| {
3067 project.find_or_create_local_worktree("/root2", true, cx)
3068 })
3069 .await
3070 .unwrap();
3071 assert_eq!(
3072 cx.current_window_title(window_id).as_deref(),
3073 Some("one.txt — root1, root2")
3074 );
3075
3076 // Remove a project folder
3077 project
3078 .update(cx, |project, cx| project.remove_worktree(worktree_id, cx))
3079 .await;
3080 assert_eq!(
3081 cx.current_window_title(window_id).as_deref(),
3082 Some("one.txt — root2")
3083 );
3084 }
3085
3086 #[gpui::test]
3087 async fn test_close_window(cx: &mut TestAppContext) {
3088 cx.foreground().forbid_parking();
3089 Settings::test_async(cx);
3090 let fs = FakeFs::new(cx.background());
3091 fs.insert_tree("/root", json!({ "one": "" })).await;
3092
3093 let project = Project::test(fs, ["root".as_ref()], cx).await;
3094 let (window_id, workspace) = cx.add_window(|cx| {
3095 Workspace::new(
3096 Default::default(),
3097 0,
3098 project.clone(),
3099 default_item_factory,
3100 cx,
3101 )
3102 });
3103
3104 // When there are no dirty items, there's nothing to do.
3105 let item1 = cx.add_view(&workspace, |_| TestItem::new());
3106 workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
3107 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
3108 assert!(task.await.unwrap());
3109
3110 // When there are dirty untitled items, prompt to save each one. If the user
3111 // cancels any prompt, then abort.
3112 let item2 = cx.add_view(&workspace, |_| TestItem::new().with_dirty(true));
3113 let item3 = cx.add_view(&workspace, |cx| {
3114 TestItem::new()
3115 .with_dirty(true)
3116 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3117 });
3118 workspace.update(cx, |w, cx| {
3119 w.add_item(Box::new(item2.clone()), cx);
3120 w.add_item(Box::new(item3.clone()), cx);
3121 });
3122 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
3123 cx.foreground().run_until_parked();
3124 cx.simulate_prompt_answer(window_id, 2 /* cancel */);
3125 cx.foreground().run_until_parked();
3126 assert!(!cx.has_pending_prompt(window_id));
3127 assert!(!task.await.unwrap());
3128 }
3129
3130 #[gpui::test]
3131 async fn test_close_pane_items(cx: &mut TestAppContext) {
3132 cx.foreground().forbid_parking();
3133 Settings::test_async(cx);
3134 let fs = FakeFs::new(cx.background());
3135
3136 let project = Project::test(fs, None, cx).await;
3137 let (window_id, workspace) = cx.add_window(|cx| {
3138 Workspace::new(Default::default(), 0, project, default_item_factory, cx)
3139 });
3140
3141 let item1 = cx.add_view(&workspace, |cx| {
3142 TestItem::new()
3143 .with_dirty(true)
3144 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3145 });
3146 let item2 = cx.add_view(&workspace, |cx| {
3147 TestItem::new()
3148 .with_dirty(true)
3149 .with_conflict(true)
3150 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
3151 });
3152 let item3 = cx.add_view(&workspace, |cx| {
3153 TestItem::new()
3154 .with_dirty(true)
3155 .with_conflict(true)
3156 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
3157 });
3158 let item4 = cx.add_view(&workspace, |cx| {
3159 TestItem::new()
3160 .with_dirty(true)
3161 .with_project_items(&[TestProjectItem::new_untitled(cx)])
3162 });
3163 let pane = workspace.update(cx, |workspace, cx| {
3164 workspace.add_item(Box::new(item1.clone()), cx);
3165 workspace.add_item(Box::new(item2.clone()), cx);
3166 workspace.add_item(Box::new(item3.clone()), cx);
3167 workspace.add_item(Box::new(item4.clone()), cx);
3168 workspace.active_pane().clone()
3169 });
3170
3171 let close_items = workspace.update(cx, |workspace, cx| {
3172 pane.update(cx, |pane, cx| {
3173 pane.activate_item(1, true, true, cx);
3174 assert_eq!(pane.active_item().unwrap().id(), item2.id());
3175 });
3176
3177 let item1_id = item1.id();
3178 let item3_id = item3.id();
3179 let item4_id = item4.id();
3180 Pane::close_items(workspace, pane.clone(), cx, move |id| {
3181 [item1_id, item3_id, item4_id].contains(&id)
3182 })
3183 });
3184 cx.foreground().run_until_parked();
3185
3186 // There's a prompt to save item 1.
3187 pane.read_with(cx, |pane, _| {
3188 assert_eq!(pane.items_len(), 4);
3189 assert_eq!(pane.active_item().unwrap().id(), item1.id());
3190 });
3191 assert!(cx.has_pending_prompt(window_id));
3192
3193 // Confirm saving item 1.
3194 cx.simulate_prompt_answer(window_id, 0);
3195 cx.foreground().run_until_parked();
3196
3197 // Item 1 is saved. There's a prompt to save item 3.
3198 pane.read_with(cx, |pane, cx| {
3199 assert_eq!(item1.read(cx).save_count, 1);
3200 assert_eq!(item1.read(cx).save_as_count, 0);
3201 assert_eq!(item1.read(cx).reload_count, 0);
3202 assert_eq!(pane.items_len(), 3);
3203 assert_eq!(pane.active_item().unwrap().id(), item3.id());
3204 });
3205 assert!(cx.has_pending_prompt(window_id));
3206
3207 // Cancel saving item 3.
3208 cx.simulate_prompt_answer(window_id, 1);
3209 cx.foreground().run_until_parked();
3210
3211 // Item 3 is reloaded. There's a prompt to save item 4.
3212 pane.read_with(cx, |pane, cx| {
3213 assert_eq!(item3.read(cx).save_count, 0);
3214 assert_eq!(item3.read(cx).save_as_count, 0);
3215 assert_eq!(item3.read(cx).reload_count, 1);
3216 assert_eq!(pane.items_len(), 2);
3217 assert_eq!(pane.active_item().unwrap().id(), item4.id());
3218 });
3219 assert!(cx.has_pending_prompt(window_id));
3220
3221 // Confirm saving item 4.
3222 cx.simulate_prompt_answer(window_id, 0);
3223 cx.foreground().run_until_parked();
3224
3225 // There's a prompt for a path for item 4.
3226 cx.simulate_new_path_selection(|_| Some(Default::default()));
3227 close_items.await.unwrap();
3228
3229 // The requested items are closed.
3230 pane.read_with(cx, |pane, cx| {
3231 assert_eq!(item4.read(cx).save_count, 0);
3232 assert_eq!(item4.read(cx).save_as_count, 1);
3233 assert_eq!(item4.read(cx).reload_count, 0);
3234 assert_eq!(pane.items_len(), 1);
3235 assert_eq!(pane.active_item().unwrap().id(), item2.id());
3236 });
3237 }
3238
3239 #[gpui::test]
3240 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
3241 cx.foreground().forbid_parking();
3242 Settings::test_async(cx);
3243 let fs = FakeFs::new(cx.background());
3244
3245 let project = Project::test(fs, [], cx).await;
3246 let (window_id, workspace) = cx.add_window(|cx| {
3247 Workspace::new(Default::default(), 0, project, default_item_factory, cx)
3248 });
3249
3250 // Create several workspace items with single project entries, and two
3251 // workspace items with multiple project entries.
3252 let single_entry_items = (0..=4)
3253 .map(|project_entry_id| {
3254 cx.add_view(&workspace, |cx| {
3255 TestItem::new()
3256 .with_dirty(true)
3257 .with_project_items(&[TestProjectItem::new(
3258 project_entry_id,
3259 &format!("{project_entry_id}.txt"),
3260 cx,
3261 )])
3262 })
3263 })
3264 .collect::<Vec<_>>();
3265 let item_2_3 = cx.add_view(&workspace, |cx| {
3266 TestItem::new()
3267 .with_dirty(true)
3268 .with_singleton(false)
3269 .with_project_items(&[
3270 single_entry_items[2].read(cx).project_items[0].clone(),
3271 single_entry_items[3].read(cx).project_items[0].clone(),
3272 ])
3273 });
3274 let item_3_4 = cx.add_view(&workspace, |cx| {
3275 TestItem::new()
3276 .with_dirty(true)
3277 .with_singleton(false)
3278 .with_project_items(&[
3279 single_entry_items[3].read(cx).project_items[0].clone(),
3280 single_entry_items[4].read(cx).project_items[0].clone(),
3281 ])
3282 });
3283
3284 // Create two panes that contain the following project entries:
3285 // left pane:
3286 // multi-entry items: (2, 3)
3287 // single-entry items: 0, 1, 2, 3, 4
3288 // right pane:
3289 // single-entry items: 1
3290 // multi-entry items: (3, 4)
3291 let left_pane = workspace.update(cx, |workspace, cx| {
3292 let left_pane = workspace.active_pane().clone();
3293 workspace.add_item(Box::new(item_2_3.clone()), cx);
3294 for item in single_entry_items {
3295 workspace.add_item(Box::new(item), cx);
3296 }
3297 left_pane.update(cx, |pane, cx| {
3298 pane.activate_item(2, true, true, cx);
3299 });
3300
3301 workspace
3302 .split_pane(left_pane.clone(), SplitDirection::Right, cx)
3303 .unwrap();
3304
3305 left_pane
3306 });
3307
3308 //Need to cause an effect flush in order to respect new focus
3309 workspace.update(cx, |workspace, cx| {
3310 workspace.add_item(Box::new(item_3_4.clone()), cx);
3311 cx.focus(left_pane.clone());
3312 });
3313
3314 // When closing all of the items in the left pane, we should be prompted twice:
3315 // once for project entry 0, and once for project entry 2. After those two
3316 // prompts, the task should complete.
3317
3318 let close = workspace.update(cx, |workspace, cx| {
3319 Pane::close_items(workspace, left_pane.clone(), cx, |_| true)
3320 });
3321
3322 cx.foreground().run_until_parked();
3323 left_pane.read_with(cx, |pane, cx| {
3324 assert_eq!(
3325 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
3326 &[ProjectEntryId::from_proto(0)]
3327 );
3328 });
3329 cx.simulate_prompt_answer(window_id, 0);
3330
3331 cx.foreground().run_until_parked();
3332 left_pane.read_with(cx, |pane, cx| {
3333 assert_eq!(
3334 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
3335 &[ProjectEntryId::from_proto(2)]
3336 );
3337 });
3338 cx.simulate_prompt_answer(window_id, 0);
3339
3340 cx.foreground().run_until_parked();
3341 close.await.unwrap();
3342 left_pane.read_with(cx, |pane, _| {
3343 assert_eq!(pane.items_len(), 0);
3344 });
3345 }
3346
3347 #[gpui::test]
3348 async fn test_autosave(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
3349 deterministic.forbid_parking();
3350
3351 Settings::test_async(cx);
3352 let fs = FakeFs::new(cx.background());
3353
3354 let project = Project::test(fs, [], cx).await;
3355 let (window_id, workspace) = cx.add_window(|cx| {
3356 Workspace::new(Default::default(), 0, project, default_item_factory, cx)
3357 });
3358
3359 let item = cx.add_view(&workspace, |cx| {
3360 TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3361 });
3362 let item_id = item.id();
3363 workspace.update(cx, |workspace, cx| {
3364 workspace.add_item(Box::new(item.clone()), cx);
3365 });
3366
3367 // Autosave on window change.
3368 item.update(cx, |item, cx| {
3369 cx.update_global(|settings: &mut Settings, _| {
3370 settings.autosave = Autosave::OnWindowChange;
3371 });
3372 item.is_dirty = true;
3373 });
3374
3375 // Deactivating the window saves the file.
3376 cx.simulate_window_activation(None);
3377 deterministic.run_until_parked();
3378 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
3379
3380 // Autosave on focus change.
3381 item.update(cx, |item, cx| {
3382 cx.focus_self();
3383 cx.update_global(|settings: &mut Settings, _| {
3384 settings.autosave = Autosave::OnFocusChange;
3385 });
3386 item.is_dirty = true;
3387 });
3388
3389 // Blurring the item saves the file.
3390 item.update(cx, |_, cx| cx.blur());
3391 deterministic.run_until_parked();
3392 item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
3393
3394 // Deactivating the window still saves the file.
3395 cx.simulate_window_activation(Some(window_id));
3396 item.update(cx, |item, cx| {
3397 cx.focus_self();
3398 item.is_dirty = true;
3399 });
3400 cx.simulate_window_activation(None);
3401
3402 deterministic.run_until_parked();
3403 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
3404
3405 // Autosave after delay.
3406 item.update(cx, |item, cx| {
3407 cx.update_global(|settings: &mut Settings, _| {
3408 settings.autosave = Autosave::AfterDelay { milliseconds: 500 };
3409 });
3410 item.is_dirty = true;
3411 cx.emit(TestItemEvent::Edit);
3412 });
3413
3414 // Delay hasn't fully expired, so the file is still dirty and unsaved.
3415 deterministic.advance_clock(Duration::from_millis(250));
3416 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
3417
3418 // After delay expires, the file is saved.
3419 deterministic.advance_clock(Duration::from_millis(250));
3420 item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
3421
3422 // Autosave on focus change, ensuring closing the tab counts as such.
3423 item.update(cx, |item, cx| {
3424 cx.update_global(|settings: &mut Settings, _| {
3425 settings.autosave = Autosave::OnFocusChange;
3426 });
3427 item.is_dirty = true;
3428 });
3429
3430 workspace
3431 .update(cx, |workspace, cx| {
3432 let pane = workspace.active_pane().clone();
3433 Pane::close_items(workspace, pane, cx, move |id| id == item_id)
3434 })
3435 .await
3436 .unwrap();
3437 assert!(!cx.has_pending_prompt(window_id));
3438 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
3439
3440 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
3441 workspace.update(cx, |workspace, cx| {
3442 workspace.add_item(Box::new(item.clone()), cx);
3443 });
3444 item.update(cx, |item, cx| {
3445 item.project_items[0].update(cx, |item, _| {
3446 item.entry_id = None;
3447 });
3448 item.is_dirty = true;
3449 cx.blur();
3450 });
3451 deterministic.run_until_parked();
3452 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
3453
3454 // Ensure autosave is prevented for deleted files also when closing the buffer.
3455 let _close_items = workspace.update(cx, |workspace, cx| {
3456 let pane = workspace.active_pane().clone();
3457 Pane::close_items(workspace, pane, cx, move |id| id == item_id)
3458 });
3459 deterministic.run_until_parked();
3460 assert!(cx.has_pending_prompt(window_id));
3461 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
3462 }
3463
3464 #[gpui::test]
3465 async fn test_pane_navigation(
3466 deterministic: Arc<Deterministic>,
3467 cx: &mut gpui::TestAppContext,
3468 ) {
3469 deterministic.forbid_parking();
3470 Settings::test_async(cx);
3471 let fs = FakeFs::new(cx.background());
3472
3473 let project = Project::test(fs, [], cx).await;
3474 let (_, workspace) = cx.add_window(|cx| {
3475 Workspace::new(Default::default(), 0, project, default_item_factory, cx)
3476 });
3477
3478 let item = cx.add_view(&workspace, |cx| {
3479 TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3480 });
3481 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3482 let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone());
3483 let toolbar_notify_count = Rc::new(RefCell::new(0));
3484
3485 workspace.update(cx, |workspace, cx| {
3486 workspace.add_item(Box::new(item.clone()), cx);
3487 let toolbar_notification_count = toolbar_notify_count.clone();
3488 cx.observe(&toolbar, move |_, _, _| {
3489 *toolbar_notification_count.borrow_mut() += 1
3490 })
3491 .detach();
3492 });
3493
3494 pane.read_with(cx, |pane, _| {
3495 assert!(!pane.can_navigate_backward());
3496 assert!(!pane.can_navigate_forward());
3497 });
3498
3499 item.update(cx, |item, cx| {
3500 item.set_state("one".to_string(), cx);
3501 });
3502
3503 // Toolbar must be notified to re-render the navigation buttons
3504 assert_eq!(*toolbar_notify_count.borrow(), 1);
3505
3506 pane.read_with(cx, |pane, _| {
3507 assert!(pane.can_navigate_backward());
3508 assert!(!pane.can_navigate_forward());
3509 });
3510
3511 workspace
3512 .update(cx, |workspace, cx| {
3513 Pane::go_back(workspace, Some(pane.clone()), cx)
3514 })
3515 .await;
3516
3517 assert_eq!(*toolbar_notify_count.borrow(), 3);
3518 pane.read_with(cx, |pane, _| {
3519 assert!(!pane.can_navigate_backward());
3520 assert!(pane.can_navigate_forward());
3521 });
3522 }
3523}