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