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