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