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