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