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 new_pane = self.add_pane(cx);
1593 if let Some(clone) = item.clone_on_split(self.database_id(), cx.as_mut()) {
1594 Pane::add_item(self, &new_pane, clone, true, true, None, cx);
1595 }
1596 self.center.split(&pane, &new_pane, direction).unwrap();
1597 cx.notify();
1598 Some(new_pane)
1599 }
1600
1601 pub fn split_pane_with_item(&mut self, action: &SplitWithItem, cx: &mut ViewContext<Self>) {
1602 let Some(pane_to_split) = action.pane_to_split.upgrade(cx) else { return; };
1603 let Some(from) = action.from.upgrade(cx) else { return; };
1604 if &pane_to_split == self.dock_pane() {
1605 warn!("Can't split dock pane.");
1606 return;
1607 }
1608
1609 let new_pane = self.add_pane(cx);
1610 Pane::move_item(
1611 self,
1612 from.clone(),
1613 new_pane.clone(),
1614 action.item_id_to_move,
1615 0,
1616 cx,
1617 );
1618 self.center
1619 .split(&pane_to_split, &new_pane, action.split_direction)
1620 .unwrap();
1621 cx.notify();
1622 }
1623
1624 pub fn split_pane_with_project_entry(
1625 &mut self,
1626 action: &SplitWithProjectEntry,
1627 cx: &mut ViewContext<Self>,
1628 ) -> Option<Task<Result<()>>> {
1629 let pane_to_split = action.pane_to_split.upgrade(cx)?;
1630 if &pane_to_split == self.dock_pane() {
1631 warn!("Can't split dock pane.");
1632 return None;
1633 }
1634
1635 let new_pane = self.add_pane(cx);
1636 self.center
1637 .split(&pane_to_split, &new_pane, action.split_direction)
1638 .unwrap();
1639
1640 let path = self
1641 .project
1642 .read(cx)
1643 .path_for_entry(action.project_entry, cx)?;
1644 let task = self.open_path(path, Some(new_pane.downgrade()), true, cx);
1645 Some(cx.foreground().spawn(async move {
1646 task.await?;
1647 Ok(())
1648 }))
1649 }
1650
1651 fn remove_pane(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
1652 if self.center.remove(&pane).unwrap() {
1653 self.panes.retain(|p| p != &pane);
1654 cx.focus(self.panes.last().unwrap().clone());
1655 self.unfollow(&pane, cx);
1656 self.last_leaders_by_pane.remove(&pane.downgrade());
1657 for removed_item in pane.read(cx).items() {
1658 self.panes_by_item.remove(&removed_item.id());
1659 }
1660 if self.last_active_center_pane == Some(pane.downgrade()) {
1661 self.last_active_center_pane = None;
1662 }
1663
1664 cx.notify();
1665 } else {
1666 self.active_item_path_changed(cx);
1667 }
1668 }
1669
1670 pub fn panes(&self) -> &[ViewHandle<Pane>] {
1671 &self.panes
1672 }
1673
1674 fn pane(&self, pane_id: usize) -> Option<ViewHandle<Pane>> {
1675 self.panes.iter().find(|pane| pane.id() == pane_id).cloned()
1676 }
1677
1678 pub fn active_pane(&self) -> &ViewHandle<Pane> {
1679 &self.active_pane
1680 }
1681
1682 pub fn dock_pane(&self) -> &ViewHandle<Pane> {
1683 self.dock.pane()
1684 }
1685
1686 fn dock_active(&self) -> bool {
1687 &self.active_pane == self.dock.pane()
1688 }
1689
1690 fn project_remote_id_changed(&mut self, remote_id: Option<u64>, cx: &mut ViewContext<Self>) {
1691 if let Some(remote_id) = remote_id {
1692 self.remote_entity_subscription =
1693 Some(self.client.add_view_for_remote_entity(remote_id, cx));
1694 } else {
1695 self.remote_entity_subscription.take();
1696 }
1697 }
1698
1699 fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
1700 self.leader_state.followers.remove(&peer_id);
1701 if let Some(states_by_pane) = self.follower_states_by_leader.remove(&peer_id) {
1702 for state in states_by_pane.into_values() {
1703 for item in state.items_by_leader_view_id.into_values() {
1704 item.set_leader_replica_id(None, cx);
1705 }
1706 }
1707 }
1708 cx.notify();
1709 }
1710
1711 pub fn toggle_follow(
1712 &mut self,
1713 ToggleFollow(leader_id): &ToggleFollow,
1714 cx: &mut ViewContext<Self>,
1715 ) -> Option<Task<Result<()>>> {
1716 let leader_id = *leader_id;
1717 let pane = self.active_pane().clone();
1718
1719 if let Some(prev_leader_id) = self.unfollow(&pane, cx) {
1720 if leader_id == prev_leader_id {
1721 return None;
1722 }
1723 }
1724
1725 self.last_leaders_by_pane
1726 .insert(pane.downgrade(), leader_id);
1727 self.follower_states_by_leader
1728 .entry(leader_id)
1729 .or_default()
1730 .insert(pane.clone(), Default::default());
1731 cx.notify();
1732
1733 let project_id = self.project.read(cx).remote_id()?;
1734 let request = self.client.request(proto::Follow {
1735 project_id,
1736 leader_id: Some(leader_id),
1737 });
1738
1739 Some(cx.spawn_weak(|this, mut cx| async move {
1740 let response = request.await?;
1741 if let Some(this) = this.upgrade(&cx) {
1742 this.update(&mut cx, |this, _| {
1743 let state = this
1744 .follower_states_by_leader
1745 .get_mut(&leader_id)
1746 .and_then(|states_by_pane| states_by_pane.get_mut(&pane))
1747 .ok_or_else(|| anyhow!("following interrupted"))?;
1748 state.active_view_id = if let Some(active_view_id) = response.active_view_id {
1749 Some(ViewId::from_proto(active_view_id)?)
1750 } else {
1751 None
1752 };
1753 Ok::<_, anyhow::Error>(())
1754 })?;
1755 Self::add_views_from_leader(
1756 this.clone(),
1757 leader_id,
1758 vec![pane],
1759 response.views,
1760 &mut cx,
1761 )
1762 .await?;
1763 this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx));
1764 }
1765 Ok(())
1766 }))
1767 }
1768
1769 pub fn follow_next_collaborator(
1770 &mut self,
1771 _: &FollowNextCollaborator,
1772 cx: &mut ViewContext<Self>,
1773 ) -> Option<Task<Result<()>>> {
1774 let collaborators = self.project.read(cx).collaborators();
1775 let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
1776 let mut collaborators = collaborators.keys().copied();
1777 for peer_id in collaborators.by_ref() {
1778 if peer_id == leader_id {
1779 break;
1780 }
1781 }
1782 collaborators.next()
1783 } else if let Some(last_leader_id) =
1784 self.last_leaders_by_pane.get(&self.active_pane.downgrade())
1785 {
1786 if collaborators.contains_key(last_leader_id) {
1787 Some(*last_leader_id)
1788 } else {
1789 None
1790 }
1791 } else {
1792 None
1793 };
1794
1795 next_leader_id
1796 .or_else(|| collaborators.keys().copied().next())
1797 .and_then(|leader_id| self.toggle_follow(&ToggleFollow(leader_id), cx))
1798 }
1799
1800 pub fn unfollow(
1801 &mut self,
1802 pane: &ViewHandle<Pane>,
1803 cx: &mut ViewContext<Self>,
1804 ) -> Option<PeerId> {
1805 for (leader_id, states_by_pane) in &mut self.follower_states_by_leader {
1806 let leader_id = *leader_id;
1807 if let Some(state) = states_by_pane.remove(pane) {
1808 for (_, item) in state.items_by_leader_view_id {
1809 item.set_leader_replica_id(None, cx);
1810 }
1811
1812 if states_by_pane.is_empty() {
1813 self.follower_states_by_leader.remove(&leader_id);
1814 if let Some(project_id) = self.project.read(cx).remote_id() {
1815 self.client
1816 .send(proto::Unfollow {
1817 project_id,
1818 leader_id: Some(leader_id),
1819 })
1820 .log_err();
1821 }
1822 }
1823
1824 cx.notify();
1825 return Some(leader_id);
1826 }
1827 }
1828 None
1829 }
1830
1831 pub fn is_following(&self, peer_id: PeerId) -> bool {
1832 self.follower_states_by_leader.contains_key(&peer_id)
1833 }
1834
1835 pub fn is_followed(&self, peer_id: PeerId) -> bool {
1836 self.leader_state.followers.contains(&peer_id)
1837 }
1838
1839 fn render_titlebar(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
1840 let project = &self.project.read(cx);
1841 let mut worktree_root_names = String::new();
1842 for (i, name) in project.worktree_root_names(cx).enumerate() {
1843 if i > 0 {
1844 worktree_root_names.push_str(", ");
1845 }
1846 worktree_root_names.push_str(name);
1847 }
1848
1849 // TODO: There should be a better system in place for this
1850 // (https://github.com/zed-industries/zed/issues/1290)
1851 let is_fullscreen = cx.window_is_fullscreen(cx.window_id());
1852 let container_theme = if is_fullscreen {
1853 let mut container_theme = theme.workspace.titlebar.container;
1854 container_theme.padding.left = container_theme.padding.right;
1855 container_theme
1856 } else {
1857 theme.workspace.titlebar.container
1858 };
1859
1860 enum TitleBar {}
1861 ConstrainedBox::new(
1862 MouseEventHandler::<TitleBar>::new(0, cx, |_, cx| {
1863 Container::new(
1864 Stack::new()
1865 .with_child(
1866 Label::new(worktree_root_names, theme.workspace.titlebar.title.clone())
1867 .aligned()
1868 .left()
1869 .boxed(),
1870 )
1871 .with_children(
1872 self.titlebar_item
1873 .as_ref()
1874 .map(|item| ChildView::new(item, cx).aligned().right().boxed()),
1875 )
1876 .boxed(),
1877 )
1878 .with_style(container_theme)
1879 .boxed()
1880 })
1881 .on_click(MouseButton::Left, |event, cx| {
1882 if event.click_count == 2 {
1883 cx.zoom_window(cx.window_id());
1884 }
1885 })
1886 .boxed(),
1887 )
1888 .with_height(theme.workspace.titlebar.height)
1889 .named("titlebar")
1890 }
1891
1892 fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
1893 let active_entry = self.active_project_path(cx);
1894 self.project
1895 .update(cx, |project, cx| project.set_active_path(active_entry, cx));
1896 self.update_window_title(cx);
1897 }
1898
1899 fn update_window_title(&mut self, cx: &mut ViewContext<Self>) {
1900 let project = self.project().read(cx);
1901 let mut title = String::new();
1902
1903 if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
1904 let filename = path
1905 .path
1906 .file_name()
1907 .map(|s| s.to_string_lossy())
1908 .or_else(|| {
1909 Some(Cow::Borrowed(
1910 project
1911 .worktree_for_id(path.worktree_id, cx)?
1912 .read(cx)
1913 .root_name(),
1914 ))
1915 });
1916
1917 if let Some(filename) = filename {
1918 title.push_str(filename.as_ref());
1919 title.push_str(" — ");
1920 }
1921 }
1922
1923 for (i, name) in project.worktree_root_names(cx).enumerate() {
1924 if i > 0 {
1925 title.push_str(", ");
1926 }
1927 title.push_str(name);
1928 }
1929
1930 if title.is_empty() {
1931 title = "empty project".to_string();
1932 }
1933
1934 if project.is_remote() {
1935 title.push_str(" ↙");
1936 } else if project.is_shared() {
1937 title.push_str(" ↗");
1938 }
1939
1940 cx.set_window_title(&title);
1941 }
1942
1943 fn update_window_edited(&mut self, cx: &mut ViewContext<Self>) {
1944 let is_edited = !self.project.read(cx).is_read_only()
1945 && self
1946 .items(cx)
1947 .any(|item| item.has_conflict(cx) || item.is_dirty(cx));
1948 if is_edited != self.window_edited {
1949 self.window_edited = is_edited;
1950 cx.set_window_edited(self.window_edited)
1951 }
1952 }
1953
1954 fn render_disconnected_overlay(&self, cx: &mut RenderContext<Workspace>) -> Option<ElementBox> {
1955 if self.project.read(cx).is_read_only() {
1956 enum DisconnectedOverlay {}
1957 Some(
1958 MouseEventHandler::<DisconnectedOverlay>::new(0, cx, |_, cx| {
1959 let theme = &cx.global::<Settings>().theme;
1960 Label::new(
1961 "Your connection to the remote project has been lost.".to_string(),
1962 theme.workspace.disconnected_overlay.text.clone(),
1963 )
1964 .aligned()
1965 .contained()
1966 .with_style(theme.workspace.disconnected_overlay.container)
1967 .boxed()
1968 })
1969 .with_cursor_style(CursorStyle::Arrow)
1970 .capture_all()
1971 .boxed(),
1972 )
1973 } else {
1974 None
1975 }
1976 }
1977
1978 fn render_notifications(
1979 &self,
1980 theme: &theme::Workspace,
1981 cx: &AppContext,
1982 ) -> Option<ElementBox> {
1983 if self.notifications.is_empty() {
1984 None
1985 } else {
1986 Some(
1987 Flex::column()
1988 .with_children(self.notifications.iter().map(|(_, _, notification)| {
1989 ChildView::new(notification.as_ref(), cx)
1990 .contained()
1991 .with_style(theme.notification)
1992 .boxed()
1993 }))
1994 .constrained()
1995 .with_width(theme.notifications.width)
1996 .contained()
1997 .with_style(theme.notifications.container)
1998 .aligned()
1999 .bottom()
2000 .right()
2001 .boxed(),
2002 )
2003 }
2004 }
2005
2006 // RPC handlers
2007
2008 async fn handle_follow(
2009 this: ViewHandle<Self>,
2010 envelope: TypedEnvelope<proto::Follow>,
2011 _: Arc<Client>,
2012 mut cx: AsyncAppContext,
2013 ) -> Result<proto::FollowResponse> {
2014 this.update(&mut cx, |this, cx| {
2015 let client = &this.client;
2016 this.leader_state
2017 .followers
2018 .insert(envelope.original_sender_id()?);
2019
2020 let active_view_id = this.active_item(cx).and_then(|i| {
2021 Some(
2022 i.to_followable_item_handle(cx)?
2023 .remote_id(client, cx)?
2024 .to_proto(),
2025 )
2026 });
2027
2028 cx.notify();
2029
2030 Ok(proto::FollowResponse {
2031 active_view_id,
2032 views: this
2033 .panes()
2034 .iter()
2035 .flat_map(|pane| {
2036 let leader_id = this.leader_for_pane(pane);
2037 pane.read(cx).items().filter_map({
2038 let cx = &cx;
2039 move |item| {
2040 let item = item.to_followable_item_handle(cx)?;
2041 let id = item.remote_id(client, cx)?.to_proto();
2042 let variant = item.to_state_proto(cx)?;
2043 Some(proto::View {
2044 id: Some(id),
2045 leader_id,
2046 variant: Some(variant),
2047 })
2048 }
2049 })
2050 })
2051 .collect(),
2052 })
2053 })
2054 }
2055
2056 async fn handle_unfollow(
2057 this: ViewHandle<Self>,
2058 envelope: TypedEnvelope<proto::Unfollow>,
2059 _: Arc<Client>,
2060 mut cx: AsyncAppContext,
2061 ) -> Result<()> {
2062 this.update(&mut cx, |this, cx| {
2063 this.leader_state
2064 .followers
2065 .remove(&envelope.original_sender_id()?);
2066 cx.notify();
2067 Ok(())
2068 })
2069 }
2070
2071 async fn handle_update_followers(
2072 this: ViewHandle<Self>,
2073 envelope: TypedEnvelope<proto::UpdateFollowers>,
2074 _: Arc<Client>,
2075 cx: AsyncAppContext,
2076 ) -> Result<()> {
2077 let leader_id = envelope.original_sender_id()?;
2078 this.read_with(&cx, |this, _| {
2079 this.leader_updates_tx
2080 .unbounded_send((leader_id, envelope.payload))
2081 })?;
2082 Ok(())
2083 }
2084
2085 async fn process_leader_update(
2086 this: ViewHandle<Self>,
2087 leader_id: PeerId,
2088 update: proto::UpdateFollowers,
2089 cx: &mut AsyncAppContext,
2090 ) -> Result<()> {
2091 match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
2092 proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
2093 this.update(cx, |this, _| {
2094 if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
2095 for state in state.values_mut() {
2096 state.active_view_id =
2097 if let Some(active_view_id) = update_active_view.id.clone() {
2098 Some(ViewId::from_proto(active_view_id)?)
2099 } else {
2100 None
2101 };
2102 }
2103 }
2104 anyhow::Ok(())
2105 })?;
2106 }
2107 proto::update_followers::Variant::UpdateView(update_view) => {
2108 let variant = update_view
2109 .variant
2110 .ok_or_else(|| anyhow!("missing update view variant"))?;
2111 let id = update_view
2112 .id
2113 .ok_or_else(|| anyhow!("missing update view id"))?;
2114 let mut tasks = Vec::new();
2115 this.update(cx, |this, cx| {
2116 let project = this.project.clone();
2117 if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
2118 for state in state.values_mut() {
2119 let view_id = ViewId::from_proto(id.clone())?;
2120 if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
2121 tasks.push(item.apply_update_proto(&project, variant.clone(), cx));
2122 }
2123 }
2124 }
2125 anyhow::Ok(())
2126 })?;
2127 try_join_all(tasks).await.log_err();
2128 }
2129 proto::update_followers::Variant::CreateView(view) => {
2130 let panes = this.read_with(cx, |this, _| {
2131 this.follower_states_by_leader
2132 .get(&leader_id)
2133 .into_iter()
2134 .flat_map(|states_by_pane| states_by_pane.keys())
2135 .cloned()
2136 .collect()
2137 });
2138 Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], cx).await?;
2139 }
2140 }
2141 this.update(cx, |this, cx| this.leader_updated(leader_id, cx));
2142 Ok(())
2143 }
2144
2145 async fn add_views_from_leader(
2146 this: ViewHandle<Self>,
2147 leader_id: PeerId,
2148 panes: Vec<ViewHandle<Pane>>,
2149 views: Vec<proto::View>,
2150 cx: &mut AsyncAppContext,
2151 ) -> Result<()> {
2152 let project = this.read_with(cx, |this, _| this.project.clone());
2153 let replica_id = project
2154 .read_with(cx, |project, _| {
2155 project
2156 .collaborators()
2157 .get(&leader_id)
2158 .map(|c| c.replica_id)
2159 })
2160 .ok_or_else(|| anyhow!("no such collaborator {}", leader_id))?;
2161
2162 let item_builders = cx.update(|cx| {
2163 cx.default_global::<FollowableItemBuilders>()
2164 .values()
2165 .map(|b| b.0)
2166 .collect::<Vec<_>>()
2167 });
2168
2169 let mut item_tasks_by_pane = HashMap::default();
2170 for pane in panes {
2171 let mut item_tasks = Vec::new();
2172 let mut leader_view_ids = Vec::new();
2173 for view in &views {
2174 let Some(id) = &view.id else { continue };
2175 let id = ViewId::from_proto(id.clone())?;
2176 let mut variant = view.variant.clone();
2177 if variant.is_none() {
2178 Err(anyhow!("missing variant"))?;
2179 }
2180 for build_item in &item_builders {
2181 let task = cx.update(|cx| {
2182 build_item(pane.clone(), project.clone(), id, &mut variant, cx)
2183 });
2184 if let Some(task) = task {
2185 item_tasks.push(task);
2186 leader_view_ids.push(id);
2187 break;
2188 } else {
2189 assert!(variant.is_some());
2190 }
2191 }
2192 }
2193
2194 item_tasks_by_pane.insert(pane, (item_tasks, leader_view_ids));
2195 }
2196
2197 for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane {
2198 let items = futures::future::try_join_all(item_tasks).await?;
2199 this.update(cx, |this, cx| {
2200 let state = this
2201 .follower_states_by_leader
2202 .get_mut(&leader_id)?
2203 .get_mut(&pane)?;
2204
2205 for (id, item) in leader_view_ids.into_iter().zip(items) {
2206 item.set_leader_replica_id(Some(replica_id), cx);
2207 state.items_by_leader_view_id.insert(id, item);
2208 }
2209
2210 Some(())
2211 });
2212 }
2213 Ok(())
2214 }
2215
2216 fn update_followers(
2217 &self,
2218 update: proto::update_followers::Variant,
2219 cx: &AppContext,
2220 ) -> Option<()> {
2221 let project_id = self.project.read(cx).remote_id()?;
2222 if !self.leader_state.followers.is_empty() {
2223 self.client
2224 .send(proto::UpdateFollowers {
2225 project_id,
2226 follower_ids: self.leader_state.followers.iter().copied().collect(),
2227 variant: Some(update),
2228 })
2229 .log_err();
2230 }
2231 None
2232 }
2233
2234 pub fn leader_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<PeerId> {
2235 self.follower_states_by_leader
2236 .iter()
2237 .find_map(|(leader_id, state)| {
2238 if state.contains_key(pane) {
2239 Some(*leader_id)
2240 } else {
2241 None
2242 }
2243 })
2244 }
2245
2246 fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
2247 cx.notify();
2248
2249 let call = self.active_call()?;
2250 let room = call.read(cx).room()?.read(cx);
2251 let participant = room.remote_participant_for_peer_id(leader_id)?;
2252 let mut items_to_activate = Vec::new();
2253 match participant.location {
2254 call::ParticipantLocation::SharedProject { project_id } => {
2255 if Some(project_id) == self.project.read(cx).remote_id() {
2256 for (pane, state) in self.follower_states_by_leader.get(&leader_id)? {
2257 if let Some(item) = state
2258 .active_view_id
2259 .and_then(|id| state.items_by_leader_view_id.get(&id))
2260 {
2261 items_to_activate.push((pane.clone(), item.boxed_clone()));
2262 } else {
2263 if let Some(shared_screen) =
2264 self.shared_screen_for_peer(leader_id, pane, cx)
2265 {
2266 items_to_activate.push((pane.clone(), Box::new(shared_screen)));
2267 }
2268 }
2269 }
2270 }
2271 }
2272 call::ParticipantLocation::UnsharedProject => {}
2273 call::ParticipantLocation::External => {
2274 for (pane, _) in self.follower_states_by_leader.get(&leader_id)? {
2275 if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
2276 items_to_activate.push((pane.clone(), Box::new(shared_screen)));
2277 }
2278 }
2279 }
2280 }
2281
2282 for (pane, item) in items_to_activate {
2283 let active_item_was_focused = pane
2284 .read(cx)
2285 .active_item()
2286 .map(|active_item| cx.is_child_focused(active_item.to_any()))
2287 .unwrap_or_default();
2288
2289 if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) {
2290 pane.update(cx, |pane, cx| pane.activate_item(index, false, false, cx));
2291 } else {
2292 Pane::add_item(self, &pane, item.boxed_clone(), false, false, None, cx);
2293 }
2294
2295 if active_item_was_focused {
2296 pane.update(cx, |pane, cx| pane.focus_active_item(cx));
2297 }
2298 }
2299
2300 None
2301 }
2302
2303 fn shared_screen_for_peer(
2304 &self,
2305 peer_id: PeerId,
2306 pane: &ViewHandle<Pane>,
2307 cx: &mut ViewContext<Self>,
2308 ) -> Option<ViewHandle<SharedScreen>> {
2309 let call = self.active_call()?;
2310 let room = call.read(cx).room()?.read(cx);
2311 let participant = room.remote_participant_for_peer_id(peer_id)?;
2312 let track = participant.tracks.values().next()?.clone();
2313 let user = participant.user.clone();
2314
2315 for item in pane.read(cx).items_of_type::<SharedScreen>() {
2316 if item.read(cx).peer_id == peer_id {
2317 return Some(item);
2318 }
2319 }
2320
2321 Some(cx.add_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx)))
2322 }
2323
2324 pub fn on_window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
2325 if active {
2326 cx.background()
2327 .spawn(persistence::DB.update_timestamp(self.database_id()))
2328 .detach();
2329 } else {
2330 for pane in &self.panes {
2331 pane.update(cx, |pane, cx| {
2332 if let Some(item) = pane.active_item() {
2333 item.workspace_deactivated(cx);
2334 }
2335 if matches!(
2336 cx.global::<Settings>().autosave,
2337 Autosave::OnWindowChange | Autosave::OnFocusChange
2338 ) {
2339 for item in pane.items() {
2340 Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
2341 .detach_and_log_err(cx);
2342 }
2343 }
2344 });
2345 }
2346 }
2347 }
2348
2349 fn active_call(&self) -> Option<&ModelHandle<ActiveCall>> {
2350 self.active_call.as_ref().map(|(call, _)| call)
2351 }
2352
2353 fn on_active_call_event(
2354 &mut self,
2355 _: ModelHandle<ActiveCall>,
2356 event: &call::room::Event,
2357 cx: &mut ViewContext<Self>,
2358 ) {
2359 match event {
2360 call::room::Event::ParticipantLocationChanged { participant_id }
2361 | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
2362 self.leader_updated(*participant_id, cx);
2363 }
2364 _ => {}
2365 }
2366 }
2367
2368 pub fn database_id(&self) -> WorkspaceId {
2369 self.database_id
2370 }
2371
2372 fn location(&self, cx: &AppContext) -> Option<WorkspaceLocation> {
2373 let project = self.project().read(cx);
2374
2375 if project.is_local() {
2376 Some(
2377 project
2378 .visible_worktrees(cx)
2379 .map(|worktree| worktree.read(cx).abs_path())
2380 .collect::<Vec<_>>()
2381 .into(),
2382 )
2383 } else {
2384 None
2385 }
2386 }
2387
2388 fn remove_panes(&mut self, member: Member, cx: &mut ViewContext<Workspace>) {
2389 match member {
2390 Member::Axis(PaneAxis { members, .. }) => {
2391 for child in members.iter() {
2392 self.remove_panes(child.clone(), cx)
2393 }
2394 }
2395 Member::Pane(pane) => self.remove_pane(pane.clone(), cx),
2396 }
2397 }
2398
2399 fn serialize_workspace(&self, cx: &AppContext) {
2400 fn serialize_pane_handle(
2401 pane_handle: &ViewHandle<Pane>,
2402 cx: &AppContext,
2403 ) -> SerializedPane {
2404 let (items, active) = {
2405 let pane = pane_handle.read(cx);
2406 let active_item_id = pane.active_item().map(|item| item.id());
2407 (
2408 pane.items()
2409 .filter_map(|item_handle| {
2410 Some(SerializedItem {
2411 kind: Arc::from(item_handle.serialized_item_kind()?),
2412 item_id: item_handle.id(),
2413 active: Some(item_handle.id()) == active_item_id,
2414 })
2415 })
2416 .collect::<Vec<_>>(),
2417 pane.is_active(),
2418 )
2419 };
2420
2421 SerializedPane::new(items, active)
2422 }
2423
2424 fn build_serialized_pane_group(
2425 pane_group: &Member,
2426 cx: &AppContext,
2427 ) -> SerializedPaneGroup {
2428 match pane_group {
2429 Member::Axis(PaneAxis { axis, members }) => SerializedPaneGroup::Group {
2430 axis: *axis,
2431 children: members
2432 .iter()
2433 .map(|member| build_serialized_pane_group(member, cx))
2434 .collect::<Vec<_>>(),
2435 },
2436 Member::Pane(pane_handle) => {
2437 SerializedPaneGroup::Pane(serialize_pane_handle(&pane_handle, cx))
2438 }
2439 }
2440 }
2441
2442 if let Some(location) = self.location(cx) {
2443 // Load bearing special case:
2444 // - with_local_workspace() relies on this to not have other stuff open
2445 // when you open your log
2446 if !location.paths().is_empty() {
2447 let dock_pane = serialize_pane_handle(self.dock.pane(), cx);
2448 let center_group = build_serialized_pane_group(&self.center.root, cx);
2449
2450 let serialized_workspace = SerializedWorkspace {
2451 id: self.database_id,
2452 location,
2453 dock_position: self.dock.position(),
2454 dock_pane,
2455 center_group,
2456 left_sidebar_open: self.left_sidebar.read(cx).is_open(),
2457 bounds: Default::default(),
2458 display: Default::default(),
2459 };
2460
2461 cx.background()
2462 .spawn(persistence::DB.save_workspace(serialized_workspace))
2463 .detach();
2464 }
2465 }
2466 }
2467
2468 fn load_from_serialized_workspace(
2469 workspace: WeakViewHandle<Workspace>,
2470 serialized_workspace: SerializedWorkspace,
2471 cx: &mut MutableAppContext,
2472 ) {
2473 cx.spawn(|mut cx| async move {
2474 if let Some(workspace) = workspace.upgrade(&cx) {
2475 let (project, dock_pane_handle, old_center_pane) =
2476 workspace.read_with(&cx, |workspace, _| {
2477 (
2478 workspace.project().clone(),
2479 workspace.dock_pane().clone(),
2480 workspace.last_active_center_pane.clone(),
2481 )
2482 });
2483
2484 serialized_workspace
2485 .dock_pane
2486 .deserialize_to(
2487 &project,
2488 &dock_pane_handle,
2489 serialized_workspace.id,
2490 &workspace,
2491 &mut cx,
2492 )
2493 .await;
2494
2495 // Traverse the splits tree and add to things
2496 let center_group = serialized_workspace
2497 .center_group
2498 .deserialize(&project, serialized_workspace.id, &workspace, &mut cx)
2499 .await;
2500
2501 // Remove old panes from workspace panes list
2502 workspace.update(&mut cx, |workspace, cx| {
2503 if let Some((center_group, active_pane)) = center_group {
2504 workspace.remove_panes(workspace.center.root.clone(), cx);
2505
2506 // Swap workspace center group
2507 workspace.center = PaneGroup::with_root(center_group);
2508
2509 // Change the focus to the workspace first so that we retrigger focus in on the pane.
2510 cx.focus_self();
2511
2512 if let Some(active_pane) = active_pane {
2513 cx.focus(active_pane);
2514 } else {
2515 cx.focus(workspace.panes.last().unwrap().clone());
2516 }
2517 } else {
2518 let old_center_handle = old_center_pane.and_then(|weak| weak.upgrade(cx));
2519 if let Some(old_center_handle) = old_center_handle {
2520 cx.focus(old_center_handle)
2521 } else {
2522 cx.focus_self()
2523 }
2524 }
2525
2526 if workspace.left_sidebar().read(cx).is_open()
2527 != serialized_workspace.left_sidebar_open
2528 {
2529 workspace.toggle_sidebar(SidebarSide::Left, cx);
2530 }
2531
2532 // Note that without after_window, the focus_self() and
2533 // the focus the dock generates start generating alternating
2534 // focus due to the deferred execution each triggering each other
2535 cx.after_window_update(move |workspace, cx| {
2536 Dock::set_dock_position(workspace, serialized_workspace.dock_position, cx);
2537 });
2538
2539 cx.notify();
2540 });
2541
2542 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
2543 workspace.read_with(&cx, |workspace, cx| workspace.serialize_workspace(cx))
2544 }
2545 })
2546 .detach();
2547 }
2548}
2549
2550fn notify_if_database_failed(workspace: &ViewHandle<Workspace>, cx: &mut AsyncAppContext) {
2551 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
2552 workspace.update(cx, |workspace, cx| {
2553 workspace.show_notification_once(0, cx, |cx| {
2554 cx.add_view(|_| {
2555 MessageNotification::new(
2556 indoc::indoc! {"
2557 Failed to load any database file :(
2558 "},
2559 OsOpen("https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml".to_string()),
2560 "Click to let us know about this error"
2561 )
2562 })
2563 });
2564 });
2565 } else {
2566 let backup_path = (*db::BACKUP_DB_PATH).read();
2567 if let Some(backup_path) = &*backup_path {
2568 workspace.update(cx, |workspace, cx| {
2569 workspace.show_notification_once(0, cx, |cx| {
2570 cx.add_view(|_| {
2571 let backup_path = backup_path.to_string_lossy();
2572 MessageNotification::new(
2573 format!(
2574 indoc::indoc! {"
2575 Database file was corrupted :(
2576 Old database backed up to:
2577 {}
2578 "},
2579 backup_path
2580 ),
2581 OsOpen(backup_path.to_string()),
2582 "Click to show old database in finder",
2583 )
2584 })
2585 });
2586 });
2587 }
2588 }
2589}
2590
2591impl Entity for Workspace {
2592 type Event = Event;
2593}
2594
2595impl View for Workspace {
2596 fn ui_name() -> &'static str {
2597 "Workspace"
2598 }
2599
2600 fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
2601 let theme = cx.global::<Settings>().theme.clone();
2602 Stack::new()
2603 .with_child(
2604 Flex::column()
2605 .with_child(self.render_titlebar(&theme, cx))
2606 .with_child(
2607 Stack::new()
2608 .with_child({
2609 let project = self.project.clone();
2610 Flex::row()
2611 .with_children(
2612 if self.left_sidebar.read(cx).active_item().is_some() {
2613 Some(
2614 ChildView::new(&self.left_sidebar, cx)
2615 .constrained()
2616 .dynamically(|constraint, cx| {
2617 SizeConstraint::new(
2618 Vector2F::new(20., constraint.min.y()),
2619 Vector2F::new(
2620 cx.window_size.x() * 0.8,
2621 constraint.max.y(),
2622 ),
2623 )
2624 })
2625 .boxed(),
2626 )
2627 } else {
2628 None
2629 },
2630 )
2631 .with_child(
2632 FlexItem::new(
2633 Flex::column()
2634 .with_child(
2635 FlexItem::new(self.center.render(
2636 &project,
2637 &theme,
2638 &self.follower_states_by_leader,
2639 self.active_call(),
2640 self.active_pane(),
2641 cx,
2642 ))
2643 .flex(1., true)
2644 .boxed(),
2645 )
2646 .with_children(self.dock.render(
2647 &theme,
2648 DockAnchor::Bottom,
2649 cx,
2650 ))
2651 .boxed(),
2652 )
2653 .flex(1., true)
2654 .boxed(),
2655 )
2656 .with_children(self.dock.render(&theme, DockAnchor::Right, cx))
2657 .with_children(
2658 if self.right_sidebar.read(cx).active_item().is_some() {
2659 Some(
2660 ChildView::new(&self.right_sidebar, cx)
2661 .constrained()
2662 .dynamically(|constraint, cx| {
2663 SizeConstraint::new(
2664 Vector2F::new(20., constraint.min.y()),
2665 Vector2F::new(
2666 cx.window_size.x() * 0.8,
2667 constraint.max.y(),
2668 ),
2669 )
2670 })
2671 .boxed(),
2672 )
2673 } else {
2674 None
2675 },
2676 )
2677 .boxed()
2678 })
2679 .with_child(
2680 Overlay::new(
2681 Stack::new()
2682 .with_children(self.dock.render(
2683 &theme,
2684 DockAnchor::Expanded,
2685 cx,
2686 ))
2687 .with_children(self.modal.as_ref().map(|modal| {
2688 ChildView::new(modal, cx)
2689 .contained()
2690 .with_style(theme.workspace.modal)
2691 .aligned()
2692 .top()
2693 .boxed()
2694 }))
2695 .with_children(
2696 self.render_notifications(&theme.workspace, cx),
2697 )
2698 .boxed(),
2699 )
2700 .boxed(),
2701 )
2702 .flex(1.0, true)
2703 .boxed(),
2704 )
2705 .with_child(ChildView::new(&self.status_bar, cx).boxed())
2706 .contained()
2707 .with_background_color(theme.workspace.background)
2708 .boxed(),
2709 )
2710 .with_children(DragAndDrop::render(cx))
2711 .with_children(self.render_disconnected_overlay(cx))
2712 .named("workspace")
2713 }
2714
2715 fn focus_in(&mut self, view: AnyViewHandle, cx: &mut ViewContext<Self>) {
2716 if cx.is_self_focused() {
2717 cx.focus(&self.active_pane);
2718 } else {
2719 for pane in self.panes() {
2720 let view = view.clone();
2721 if pane.update(cx, |_, cx| view.id() == cx.view_id() || cx.is_child(view)) {
2722 self.handle_pane_focused(pane.clone(), cx);
2723 break;
2724 }
2725 }
2726 }
2727 }
2728
2729 fn keymap_context(&self, _: &AppContext) -> KeymapContext {
2730 let mut keymap = Self::default_keymap_context();
2731 if self.active_pane() == self.dock_pane() {
2732 keymap.set.insert("Dock".into());
2733 }
2734 keymap
2735 }
2736}
2737
2738impl ViewId {
2739 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
2740 Ok(Self {
2741 creator: message
2742 .creator
2743 .ok_or_else(|| anyhow!("creator is missing"))?,
2744 id: message.id,
2745 })
2746 }
2747
2748 pub(crate) fn to_proto(&self) -> proto::ViewId {
2749 proto::ViewId {
2750 creator: Some(self.creator),
2751 id: self.id,
2752 }
2753 }
2754}
2755
2756pub trait WorkspaceHandle {
2757 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
2758}
2759
2760impl WorkspaceHandle for ViewHandle<Workspace> {
2761 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
2762 self.read(cx)
2763 .worktrees(cx)
2764 .flat_map(|worktree| {
2765 let worktree_id = worktree.read(cx).id();
2766 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
2767 worktree_id,
2768 path: f.path.clone(),
2769 })
2770 })
2771 .collect::<Vec<_>>()
2772 }
2773}
2774
2775impl std::fmt::Debug for OpenPaths {
2776 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2777 f.debug_struct("OpenPaths")
2778 .field("paths", &self.paths)
2779 .finish()
2780 }
2781}
2782
2783fn open(_: &Open, cx: &mut MutableAppContext) {
2784 let mut paths = cx.prompt_for_paths(PathPromptOptions {
2785 files: true,
2786 directories: true,
2787 multiple: true,
2788 });
2789 cx.spawn(|mut cx| async move {
2790 if let Some(paths) = paths.recv().await.flatten() {
2791 cx.update(|cx| cx.dispatch_global_action(OpenPaths { paths }));
2792 }
2793 })
2794 .detach();
2795}
2796
2797pub struct WorkspaceCreated(WeakViewHandle<Workspace>);
2798
2799pub fn activate_workspace_for_project(
2800 cx: &mut MutableAppContext,
2801 predicate: impl Fn(&mut Project, &mut ModelContext<Project>) -> bool,
2802) -> Option<ViewHandle<Workspace>> {
2803 for window_id in cx.window_ids().collect::<Vec<_>>() {
2804 if let Some(workspace_handle) = cx.root_view::<Workspace>(window_id) {
2805 let project = workspace_handle.read(cx).project.clone();
2806 if project.update(cx, &predicate) {
2807 cx.activate_window(window_id);
2808 return Some(workspace_handle);
2809 }
2810 }
2811 }
2812 None
2813}
2814
2815pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
2816 DB.last_workspace().await.log_err().flatten()
2817}
2818
2819#[allow(clippy::type_complexity)]
2820pub fn open_paths(
2821 abs_paths: &[PathBuf],
2822 app_state: &Arc<AppState>,
2823 cx: &mut MutableAppContext,
2824) -> Task<(
2825 ViewHandle<Workspace>,
2826 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
2827)> {
2828 log::info!("open paths {:?}", abs_paths);
2829
2830 // Open paths in existing workspace if possible
2831 let existing =
2832 activate_workspace_for_project(cx, |project, cx| project.contains_paths(abs_paths, cx));
2833
2834 let app_state = app_state.clone();
2835 let abs_paths = abs_paths.to_vec();
2836 cx.spawn(|mut cx| async move {
2837 if let Some(existing) = existing {
2838 (
2839 existing.clone(),
2840 existing
2841 .update(&mut cx, |workspace, cx| {
2842 workspace.open_paths(abs_paths, true, cx)
2843 })
2844 .await,
2845 )
2846 } else {
2847 let contains_directory =
2848 futures::future::join_all(abs_paths.iter().map(|path| app_state.fs.is_file(path)))
2849 .await
2850 .contains(&false);
2851
2852 cx.update(|cx| {
2853 let task = Workspace::new_local(abs_paths, app_state.clone(), cx);
2854
2855 cx.spawn(|mut cx| async move {
2856 let (workspace, items) = task.await;
2857
2858 workspace.update(&mut cx, |workspace, cx| {
2859 if contains_directory {
2860 workspace.toggle_sidebar(SidebarSide::Left, cx);
2861 }
2862 });
2863
2864 (workspace, items)
2865 })
2866 })
2867 .await
2868 }
2869 })
2870}
2871
2872pub fn open_new(app_state: &Arc<AppState>, cx: &mut MutableAppContext) -> Task<()> {
2873 let task = Workspace::new_local(Vec::new(), app_state.clone(), cx);
2874 cx.spawn(|mut cx| async move {
2875 let (workspace, opened_paths) = task.await;
2876
2877 workspace.update(&mut cx, |_, cx| {
2878 if opened_paths.is_empty() {
2879 cx.dispatch_action(NewFile);
2880 }
2881 })
2882 })
2883}
2884
2885fn parse_pixel_position_env_var(value: &str) -> Option<Vector2F> {
2886 let mut parts = value.split(',');
2887 let width: usize = parts.next()?.parse().ok()?;
2888 let height: usize = parts.next()?.parse().ok()?;
2889 Some(vec2f(width as f32, height as f32))
2890}
2891
2892#[cfg(test)]
2893mod tests {
2894 use std::{cell::RefCell, rc::Rc};
2895
2896 use crate::item::test::{TestItem, TestItemEvent, TestProjectItem};
2897
2898 use super::*;
2899 use fs::FakeFs;
2900 use gpui::{executor::Deterministic, TestAppContext, ViewContext};
2901 use project::{Project, ProjectEntryId};
2902 use serde_json::json;
2903
2904 pub fn default_item_factory(
2905 _workspace: &mut Workspace,
2906 _cx: &mut ViewContext<Workspace>,
2907 ) -> Option<Box<dyn ItemHandle>> {
2908 unimplemented!()
2909 }
2910
2911 #[gpui::test]
2912 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
2913 cx.foreground().forbid_parking();
2914 Settings::test_async(cx);
2915
2916 let fs = FakeFs::new(cx.background());
2917 let project = Project::test(fs, [], cx).await;
2918 let (_, workspace) = cx.add_window(|cx| {
2919 Workspace::new(
2920 Default::default(),
2921 0,
2922 project.clone(),
2923 default_item_factory,
2924 cx,
2925 )
2926 });
2927
2928 // Adding an item with no ambiguity renders the tab without detail.
2929 let item1 = cx.add_view(&workspace, |_| {
2930 let mut item = TestItem::new();
2931 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
2932 item
2933 });
2934 workspace.update(cx, |workspace, cx| {
2935 workspace.add_item(Box::new(item1.clone()), cx);
2936 });
2937 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), None));
2938
2939 // Adding an item that creates ambiguity increases the level of detail on
2940 // both tabs.
2941 let item2 = cx.add_view(&workspace, |_| {
2942 let mut item = TestItem::new();
2943 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
2944 item
2945 });
2946 workspace.update(cx, |workspace, cx| {
2947 workspace.add_item(Box::new(item2.clone()), cx);
2948 });
2949 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
2950 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
2951
2952 // Adding an item that creates ambiguity increases the level of detail only
2953 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
2954 // we stop at the highest detail available.
2955 let item3 = cx.add_view(&workspace, |_| {
2956 let mut item = TestItem::new();
2957 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
2958 item
2959 });
2960 workspace.update(cx, |workspace, cx| {
2961 workspace.add_item(Box::new(item3.clone()), cx);
2962 });
2963 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
2964 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
2965 item3.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
2966 }
2967
2968 #[gpui::test]
2969 async fn test_tracking_active_path(cx: &mut TestAppContext) {
2970 cx.foreground().forbid_parking();
2971 Settings::test_async(cx);
2972 let fs = FakeFs::new(cx.background());
2973 fs.insert_tree(
2974 "/root1",
2975 json!({
2976 "one.txt": "",
2977 "two.txt": "",
2978 }),
2979 )
2980 .await;
2981 fs.insert_tree(
2982 "/root2",
2983 json!({
2984 "three.txt": "",
2985 }),
2986 )
2987 .await;
2988
2989 let project = Project::test(fs, ["root1".as_ref()], cx).await;
2990 let (window_id, workspace) = cx.add_window(|cx| {
2991 Workspace::new(
2992 Default::default(),
2993 0,
2994 project.clone(),
2995 default_item_factory,
2996 cx,
2997 )
2998 });
2999 let worktree_id = project.read_with(cx, |project, cx| {
3000 project.worktrees(cx).next().unwrap().read(cx).id()
3001 });
3002
3003 let item1 = cx.add_view(&workspace, |cx| {
3004 TestItem::new().with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
3005 });
3006 let item2 = cx.add_view(&workspace, |cx| {
3007 TestItem::new().with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
3008 });
3009
3010 // Add an item to an empty pane
3011 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx));
3012 project.read_with(cx, |project, cx| {
3013 assert_eq!(
3014 project.active_entry(),
3015 project
3016 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
3017 .map(|e| e.id)
3018 );
3019 });
3020 assert_eq!(
3021 cx.current_window_title(window_id).as_deref(),
3022 Some("one.txt — root1")
3023 );
3024
3025 // Add a second item to a non-empty pane
3026 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
3027 assert_eq!(
3028 cx.current_window_title(window_id).as_deref(),
3029 Some("two.txt — root1")
3030 );
3031 project.read_with(cx, |project, cx| {
3032 assert_eq!(
3033 project.active_entry(),
3034 project
3035 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
3036 .map(|e| e.id)
3037 );
3038 });
3039
3040 // Close the active item
3041 workspace
3042 .update(cx, |workspace, cx| {
3043 Pane::close_active_item(workspace, &Default::default(), cx).unwrap()
3044 })
3045 .await
3046 .unwrap();
3047 assert_eq!(
3048 cx.current_window_title(window_id).as_deref(),
3049 Some("one.txt — root1")
3050 );
3051 project.read_with(cx, |project, cx| {
3052 assert_eq!(
3053 project.active_entry(),
3054 project
3055 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
3056 .map(|e| e.id)
3057 );
3058 });
3059
3060 // Add a project folder
3061 project
3062 .update(cx, |project, cx| {
3063 project.find_or_create_local_worktree("/root2", true, cx)
3064 })
3065 .await
3066 .unwrap();
3067 assert_eq!(
3068 cx.current_window_title(window_id).as_deref(),
3069 Some("one.txt — root1, root2")
3070 );
3071
3072 // Remove a project folder
3073 project
3074 .update(cx, |project, cx| project.remove_worktree(worktree_id, cx))
3075 .await;
3076 assert_eq!(
3077 cx.current_window_title(window_id).as_deref(),
3078 Some("one.txt — root2")
3079 );
3080 }
3081
3082 #[gpui::test]
3083 async fn test_close_window(cx: &mut TestAppContext) {
3084 cx.foreground().forbid_parking();
3085 Settings::test_async(cx);
3086 let fs = FakeFs::new(cx.background());
3087 fs.insert_tree("/root", json!({ "one": "" })).await;
3088
3089 let project = Project::test(fs, ["root".as_ref()], cx).await;
3090 let (window_id, workspace) = cx.add_window(|cx| {
3091 Workspace::new(
3092 Default::default(),
3093 0,
3094 project.clone(),
3095 default_item_factory,
3096 cx,
3097 )
3098 });
3099
3100 // When there are no dirty items, there's nothing to do.
3101 let item1 = cx.add_view(&workspace, |_| TestItem::new());
3102 workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
3103 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
3104 assert!(task.await.unwrap());
3105
3106 // When there are dirty untitled items, prompt to save each one. If the user
3107 // cancels any prompt, then abort.
3108 let item2 = cx.add_view(&workspace, |_| TestItem::new().with_dirty(true));
3109 let item3 = cx.add_view(&workspace, |cx| {
3110 TestItem::new()
3111 .with_dirty(true)
3112 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3113 });
3114 workspace.update(cx, |w, cx| {
3115 w.add_item(Box::new(item2.clone()), cx);
3116 w.add_item(Box::new(item3.clone()), cx);
3117 });
3118 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
3119 cx.foreground().run_until_parked();
3120 cx.simulate_prompt_answer(window_id, 2 /* cancel */);
3121 cx.foreground().run_until_parked();
3122 assert!(!cx.has_pending_prompt(window_id));
3123 assert!(!task.await.unwrap());
3124 }
3125
3126 #[gpui::test]
3127 async fn test_close_pane_items(cx: &mut TestAppContext) {
3128 cx.foreground().forbid_parking();
3129 Settings::test_async(cx);
3130 let fs = FakeFs::new(cx.background());
3131
3132 let project = Project::test(fs, None, cx).await;
3133 let (window_id, workspace) = cx.add_window(|cx| {
3134 Workspace::new(Default::default(), 0, project, default_item_factory, cx)
3135 });
3136
3137 let item1 = cx.add_view(&workspace, |cx| {
3138 TestItem::new()
3139 .with_dirty(true)
3140 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3141 });
3142 let item2 = cx.add_view(&workspace, |cx| {
3143 TestItem::new()
3144 .with_dirty(true)
3145 .with_conflict(true)
3146 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
3147 });
3148 let item3 = cx.add_view(&workspace, |cx| {
3149 TestItem::new()
3150 .with_dirty(true)
3151 .with_conflict(true)
3152 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
3153 });
3154 let item4 = cx.add_view(&workspace, |cx| {
3155 TestItem::new()
3156 .with_dirty(true)
3157 .with_project_items(&[TestProjectItem::new_untitled(cx)])
3158 });
3159 let pane = workspace.update(cx, |workspace, cx| {
3160 workspace.add_item(Box::new(item1.clone()), cx);
3161 workspace.add_item(Box::new(item2.clone()), cx);
3162 workspace.add_item(Box::new(item3.clone()), cx);
3163 workspace.add_item(Box::new(item4.clone()), cx);
3164 workspace.active_pane().clone()
3165 });
3166
3167 let close_items = workspace.update(cx, |workspace, cx| {
3168 pane.update(cx, |pane, cx| {
3169 pane.activate_item(1, true, true, cx);
3170 assert_eq!(pane.active_item().unwrap().id(), item2.id());
3171 });
3172
3173 let item1_id = item1.id();
3174 let item3_id = item3.id();
3175 let item4_id = item4.id();
3176 Pane::close_items(workspace, pane.clone(), cx, move |id| {
3177 [item1_id, item3_id, item4_id].contains(&id)
3178 })
3179 });
3180 cx.foreground().run_until_parked();
3181
3182 // There's a prompt to save item 1.
3183 pane.read_with(cx, |pane, _| {
3184 assert_eq!(pane.items_len(), 4);
3185 assert_eq!(pane.active_item().unwrap().id(), item1.id());
3186 });
3187 assert!(cx.has_pending_prompt(window_id));
3188
3189 // Confirm saving item 1.
3190 cx.simulate_prompt_answer(window_id, 0);
3191 cx.foreground().run_until_parked();
3192
3193 // Item 1 is saved. There's a prompt to save item 3.
3194 pane.read_with(cx, |pane, cx| {
3195 assert_eq!(item1.read(cx).save_count, 1);
3196 assert_eq!(item1.read(cx).save_as_count, 0);
3197 assert_eq!(item1.read(cx).reload_count, 0);
3198 assert_eq!(pane.items_len(), 3);
3199 assert_eq!(pane.active_item().unwrap().id(), item3.id());
3200 });
3201 assert!(cx.has_pending_prompt(window_id));
3202
3203 // Cancel saving item 3.
3204 cx.simulate_prompt_answer(window_id, 1);
3205 cx.foreground().run_until_parked();
3206
3207 // Item 3 is reloaded. There's a prompt to save item 4.
3208 pane.read_with(cx, |pane, cx| {
3209 assert_eq!(item3.read(cx).save_count, 0);
3210 assert_eq!(item3.read(cx).save_as_count, 0);
3211 assert_eq!(item3.read(cx).reload_count, 1);
3212 assert_eq!(pane.items_len(), 2);
3213 assert_eq!(pane.active_item().unwrap().id(), item4.id());
3214 });
3215 assert!(cx.has_pending_prompt(window_id));
3216
3217 // Confirm saving item 4.
3218 cx.simulate_prompt_answer(window_id, 0);
3219 cx.foreground().run_until_parked();
3220
3221 // There's a prompt for a path for item 4.
3222 cx.simulate_new_path_selection(|_| Some(Default::default()));
3223 close_items.await.unwrap();
3224
3225 // The requested items are closed.
3226 pane.read_with(cx, |pane, cx| {
3227 assert_eq!(item4.read(cx).save_count, 0);
3228 assert_eq!(item4.read(cx).save_as_count, 1);
3229 assert_eq!(item4.read(cx).reload_count, 0);
3230 assert_eq!(pane.items_len(), 1);
3231 assert_eq!(pane.active_item().unwrap().id(), item2.id());
3232 });
3233 }
3234
3235 #[gpui::test]
3236 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
3237 cx.foreground().forbid_parking();
3238 Settings::test_async(cx);
3239 let fs = FakeFs::new(cx.background());
3240
3241 let project = Project::test(fs, [], cx).await;
3242 let (window_id, workspace) = cx.add_window(|cx| {
3243 Workspace::new(Default::default(), 0, project, default_item_factory, cx)
3244 });
3245
3246 // Create several workspace items with single project entries, and two
3247 // workspace items with multiple project entries.
3248 let single_entry_items = (0..=4)
3249 .map(|project_entry_id| {
3250 cx.add_view(&workspace, |cx| {
3251 TestItem::new()
3252 .with_dirty(true)
3253 .with_project_items(&[TestProjectItem::new(
3254 project_entry_id,
3255 &format!("{project_entry_id}.txt"),
3256 cx,
3257 )])
3258 })
3259 })
3260 .collect::<Vec<_>>();
3261 let item_2_3 = cx.add_view(&workspace, |cx| {
3262 TestItem::new()
3263 .with_dirty(true)
3264 .with_singleton(false)
3265 .with_project_items(&[
3266 single_entry_items[2].read(cx).project_items[0].clone(),
3267 single_entry_items[3].read(cx).project_items[0].clone(),
3268 ])
3269 });
3270 let item_3_4 = cx.add_view(&workspace, |cx| {
3271 TestItem::new()
3272 .with_dirty(true)
3273 .with_singleton(false)
3274 .with_project_items(&[
3275 single_entry_items[3].read(cx).project_items[0].clone(),
3276 single_entry_items[4].read(cx).project_items[0].clone(),
3277 ])
3278 });
3279
3280 // Create two panes that contain the following project entries:
3281 // left pane:
3282 // multi-entry items: (2, 3)
3283 // single-entry items: 0, 1, 2, 3, 4
3284 // right pane:
3285 // single-entry items: 1
3286 // multi-entry items: (3, 4)
3287 let left_pane = workspace.update(cx, |workspace, cx| {
3288 let left_pane = workspace.active_pane().clone();
3289 workspace.add_item(Box::new(item_2_3.clone()), cx);
3290 for item in single_entry_items {
3291 workspace.add_item(Box::new(item), cx);
3292 }
3293 left_pane.update(cx, |pane, cx| {
3294 pane.activate_item(2, true, true, cx);
3295 });
3296
3297 workspace
3298 .split_pane(left_pane.clone(), SplitDirection::Right, cx)
3299 .unwrap();
3300
3301 left_pane
3302 });
3303
3304 //Need to cause an effect flush in order to respect new focus
3305 workspace.update(cx, |workspace, cx| {
3306 workspace.add_item(Box::new(item_3_4.clone()), cx);
3307 cx.focus(left_pane.clone());
3308 });
3309
3310 // When closing all of the items in the left pane, we should be prompted twice:
3311 // once for project entry 0, and once for project entry 2. After those two
3312 // prompts, the task should complete.
3313
3314 let close = workspace.update(cx, |workspace, cx| {
3315 Pane::close_items(workspace, left_pane.clone(), cx, |_| true)
3316 });
3317
3318 cx.foreground().run_until_parked();
3319 left_pane.read_with(cx, |pane, cx| {
3320 assert_eq!(
3321 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
3322 &[ProjectEntryId::from_proto(0)]
3323 );
3324 });
3325 cx.simulate_prompt_answer(window_id, 0);
3326
3327 cx.foreground().run_until_parked();
3328 left_pane.read_with(cx, |pane, cx| {
3329 assert_eq!(
3330 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
3331 &[ProjectEntryId::from_proto(2)]
3332 );
3333 });
3334 cx.simulate_prompt_answer(window_id, 0);
3335
3336 cx.foreground().run_until_parked();
3337 close.await.unwrap();
3338 left_pane.read_with(cx, |pane, _| {
3339 assert_eq!(pane.items_len(), 0);
3340 });
3341 }
3342
3343 #[gpui::test]
3344 async fn test_autosave(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
3345 deterministic.forbid_parking();
3346
3347 Settings::test_async(cx);
3348 let fs = FakeFs::new(cx.background());
3349
3350 let project = Project::test(fs, [], cx).await;
3351 let (window_id, workspace) = cx.add_window(|cx| {
3352 Workspace::new(Default::default(), 0, project, default_item_factory, cx)
3353 });
3354
3355 let item = cx.add_view(&workspace, |cx| {
3356 TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3357 });
3358 let item_id = item.id();
3359 workspace.update(cx, |workspace, cx| {
3360 workspace.add_item(Box::new(item.clone()), cx);
3361 });
3362
3363 // Autosave on window change.
3364 item.update(cx, |item, cx| {
3365 cx.update_global(|settings: &mut Settings, _| {
3366 settings.autosave = Autosave::OnWindowChange;
3367 });
3368 item.is_dirty = true;
3369 });
3370
3371 // Deactivating the window saves the file.
3372 cx.simulate_window_activation(None);
3373 deterministic.run_until_parked();
3374 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
3375
3376 // Autosave on focus change.
3377 item.update(cx, |item, cx| {
3378 cx.focus_self();
3379 cx.update_global(|settings: &mut Settings, _| {
3380 settings.autosave = Autosave::OnFocusChange;
3381 });
3382 item.is_dirty = true;
3383 });
3384
3385 // Blurring the item saves the file.
3386 item.update(cx, |_, cx| cx.blur());
3387 deterministic.run_until_parked();
3388 item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
3389
3390 // Deactivating the window still saves the file.
3391 cx.simulate_window_activation(Some(window_id));
3392 item.update(cx, |item, cx| {
3393 cx.focus_self();
3394 item.is_dirty = true;
3395 });
3396 cx.simulate_window_activation(None);
3397
3398 deterministic.run_until_parked();
3399 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
3400
3401 // Autosave after delay.
3402 item.update(cx, |item, cx| {
3403 cx.update_global(|settings: &mut Settings, _| {
3404 settings.autosave = Autosave::AfterDelay { milliseconds: 500 };
3405 });
3406 item.is_dirty = true;
3407 cx.emit(TestItemEvent::Edit);
3408 });
3409
3410 // Delay hasn't fully expired, so the file is still dirty and unsaved.
3411 deterministic.advance_clock(Duration::from_millis(250));
3412 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
3413
3414 // After delay expires, the file is saved.
3415 deterministic.advance_clock(Duration::from_millis(250));
3416 item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
3417
3418 // Autosave on focus change, ensuring closing the tab counts as such.
3419 item.update(cx, |item, cx| {
3420 cx.update_global(|settings: &mut Settings, _| {
3421 settings.autosave = Autosave::OnFocusChange;
3422 });
3423 item.is_dirty = true;
3424 });
3425
3426 workspace
3427 .update(cx, |workspace, cx| {
3428 let pane = workspace.active_pane().clone();
3429 Pane::close_items(workspace, pane, cx, move |id| id == item_id)
3430 })
3431 .await
3432 .unwrap();
3433 assert!(!cx.has_pending_prompt(window_id));
3434 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
3435
3436 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
3437 workspace.update(cx, |workspace, cx| {
3438 workspace.add_item(Box::new(item.clone()), cx);
3439 });
3440 item.update(cx, |item, cx| {
3441 item.project_items[0].update(cx, |item, _| {
3442 item.entry_id = None;
3443 });
3444 item.is_dirty = true;
3445 cx.blur();
3446 });
3447 deterministic.run_until_parked();
3448 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
3449
3450 // Ensure autosave is prevented for deleted files also when closing the buffer.
3451 let _close_items = workspace.update(cx, |workspace, cx| {
3452 let pane = workspace.active_pane().clone();
3453 Pane::close_items(workspace, pane, cx, move |id| id == item_id)
3454 });
3455 deterministic.run_until_parked();
3456 assert!(cx.has_pending_prompt(window_id));
3457 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
3458 }
3459
3460 #[gpui::test]
3461 async fn test_pane_navigation(
3462 deterministic: Arc<Deterministic>,
3463 cx: &mut gpui::TestAppContext,
3464 ) {
3465 deterministic.forbid_parking();
3466 Settings::test_async(cx);
3467 let fs = FakeFs::new(cx.background());
3468
3469 let project = Project::test(fs, [], cx).await;
3470 let (_, workspace) = cx.add_window(|cx| {
3471 Workspace::new(Default::default(), 0, project, default_item_factory, cx)
3472 });
3473
3474 let item = cx.add_view(&workspace, |cx| {
3475 TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3476 });
3477 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3478 let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone());
3479 let toolbar_notify_count = Rc::new(RefCell::new(0));
3480
3481 workspace.update(cx, |workspace, cx| {
3482 workspace.add_item(Box::new(item.clone()), cx);
3483 let toolbar_notification_count = toolbar_notify_count.clone();
3484 cx.observe(&toolbar, move |_, _, _| {
3485 *toolbar_notification_count.borrow_mut() += 1
3486 })
3487 .detach();
3488 });
3489
3490 pane.read_with(cx, |pane, _| {
3491 assert!(!pane.can_navigate_backward());
3492 assert!(!pane.can_navigate_forward());
3493 });
3494
3495 item.update(cx, |item, cx| {
3496 item.set_state("one".to_string(), cx);
3497 });
3498
3499 // Toolbar must be notified to re-render the navigation buttons
3500 assert_eq!(*toolbar_notify_count.borrow(), 1);
3501
3502 pane.read_with(cx, |pane, _| {
3503 assert!(pane.can_navigate_backward());
3504 assert!(!pane.can_navigate_forward());
3505 });
3506
3507 workspace
3508 .update(cx, |workspace, cx| {
3509 Pane::go_back(workspace, Some(pane.clone()), cx)
3510 })
3511 .await;
3512
3513 assert_eq!(*toolbar_notify_count.borrow(), 3);
3514 pane.read_with(cx, |pane, _| {
3515 assert!(!pane.can_navigate_backward());
3516 assert!(pane.can_navigate_forward());
3517 });
3518 }
3519}