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