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 if project.read(cx).is_local() {
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
1072 .update(&mut cx, |call, cx| call.hang_up(cx))
1073 .await
1074 .log_err();
1075 }
1076 }
1077 }
1078
1079 Ok(this
1080 .update(&mut cx, |this, cx| this.save_all_internal(true, cx))
1081 .await?)
1082 })
1083 }
1084
1085 fn save_all(&mut self, _: &SaveAll, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
1086 let save_all = self.save_all_internal(false, cx);
1087 Some(cx.foreground().spawn(async move {
1088 save_all.await?;
1089 Ok(())
1090 }))
1091 }
1092
1093 fn save_all_internal(
1094 &mut self,
1095 should_prompt_to_save: bool,
1096 cx: &mut ViewContext<Self>,
1097 ) -> Task<Result<bool>> {
1098 if self.project.read(cx).is_read_only() {
1099 return Task::ready(Ok(true));
1100 }
1101
1102 let dirty_items = self
1103 .panes
1104 .iter()
1105 .flat_map(|pane| {
1106 pane.read(cx).items().filter_map(|item| {
1107 if item.is_dirty(cx) {
1108 Some((pane.clone(), item.boxed_clone()))
1109 } else {
1110 None
1111 }
1112 })
1113 })
1114 .collect::<Vec<_>>();
1115
1116 let project = self.project.clone();
1117 cx.spawn_weak(|_, mut cx| async move {
1118 for (pane, item) in dirty_items {
1119 let (singleton, project_entry_ids) =
1120 cx.read(|cx| (item.is_singleton(cx), item.project_entry_ids(cx)));
1121 if singleton || !project_entry_ids.is_empty() {
1122 if let Some(ix) =
1123 pane.read_with(&cx, |pane, _| pane.index_for_item(item.as_ref()))
1124 {
1125 if !Pane::save_item(
1126 project.clone(),
1127 &pane,
1128 ix,
1129 &*item,
1130 should_prompt_to_save,
1131 &mut cx,
1132 )
1133 .await?
1134 {
1135 return Ok(false);
1136 }
1137 }
1138 }
1139 }
1140 Ok(true)
1141 })
1142 }
1143
1144 #[allow(clippy::type_complexity)]
1145 pub fn open_paths(
1146 &mut self,
1147 mut abs_paths: Vec<PathBuf>,
1148 visible: bool,
1149 cx: &mut ViewContext<Self>,
1150 ) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
1151 let fs = self.fs.clone();
1152
1153 // Sort the paths to ensure we add worktrees for parents before their children.
1154 abs_paths.sort_unstable();
1155 cx.spawn(|this, mut cx| async move {
1156 let mut project_paths = Vec::new();
1157 for path in &abs_paths {
1158 project_paths.push(
1159 this.update(&mut cx, |this, cx| {
1160 Workspace::project_path_for_path(this.project.clone(), path, visible, cx)
1161 })
1162 .await
1163 .log_err(),
1164 );
1165 }
1166
1167 let tasks = abs_paths
1168 .iter()
1169 .cloned()
1170 .zip(project_paths.into_iter())
1171 .map(|(abs_path, project_path)| {
1172 let this = this.clone();
1173 cx.spawn(|mut cx| {
1174 let fs = fs.clone();
1175 async move {
1176 let (_worktree, project_path) = project_path?;
1177 if fs.is_file(&abs_path).await {
1178 Some(
1179 this.update(&mut cx, |this, cx| {
1180 this.open_path(project_path, None, true, cx)
1181 })
1182 .await,
1183 )
1184 } else {
1185 None
1186 }
1187 }
1188 })
1189 })
1190 .collect::<Vec<_>>();
1191
1192 futures::future::join_all(tasks).await
1193 })
1194 }
1195
1196 fn add_folder_to_project(&mut self, _: &AddFolderToProject, cx: &mut ViewContext<Self>) {
1197 let mut paths = cx.prompt_for_paths(PathPromptOptions {
1198 files: false,
1199 directories: true,
1200 multiple: true,
1201 });
1202 cx.spawn(|this, mut cx| async move {
1203 if let Some(paths) = paths.recv().await.flatten() {
1204 let results = this
1205 .update(&mut cx, |this, cx| this.open_paths(paths, true, cx))
1206 .await;
1207 for result in results.into_iter().flatten() {
1208 result.log_err();
1209 }
1210 }
1211 })
1212 .detach();
1213 }
1214
1215 fn remove_folder_from_project(
1216 &mut self,
1217 RemoveWorktreeFromProject(worktree_id): &RemoveWorktreeFromProject,
1218 cx: &mut ViewContext<Self>,
1219 ) {
1220 let future = self
1221 .project
1222 .update(cx, |project, cx| project.remove_worktree(*worktree_id, cx));
1223 cx.foreground().spawn(future).detach();
1224 }
1225
1226 fn project_path_for_path(
1227 project: ModelHandle<Project>,
1228 abs_path: &Path,
1229 visible: bool,
1230 cx: &mut MutableAppContext,
1231 ) -> Task<Result<(ModelHandle<Worktree>, ProjectPath)>> {
1232 let entry = project.update(cx, |project, cx| {
1233 project.find_or_create_local_worktree(abs_path, visible, cx)
1234 });
1235 cx.spawn(|cx| async move {
1236 let (worktree, path) = entry.await?;
1237 let worktree_id = worktree.read_with(&cx, |t, _| t.id());
1238 Ok((
1239 worktree,
1240 ProjectPath {
1241 worktree_id,
1242 path: path.into(),
1243 },
1244 ))
1245 })
1246 }
1247
1248 /// Returns the modal that was toggled closed if it was open.
1249 pub fn toggle_modal<V, F>(
1250 &mut self,
1251 cx: &mut ViewContext<Self>,
1252 add_view: F,
1253 ) -> Option<ViewHandle<V>>
1254 where
1255 V: 'static + View,
1256 F: FnOnce(&mut Self, &mut ViewContext<Self>) -> ViewHandle<V>,
1257 {
1258 cx.notify();
1259 // Whatever modal was visible is getting clobbered. If its the same type as V, then return
1260 // it. Otherwise, create a new modal and set it as active.
1261 let already_open_modal = self.modal.take().and_then(|modal| modal.downcast::<V>());
1262 if let Some(already_open_modal) = already_open_modal {
1263 cx.focus_self();
1264 Some(already_open_modal)
1265 } else {
1266 let modal = add_view(self, cx);
1267 cx.focus(&modal);
1268 self.modal = Some(modal.into());
1269 None
1270 }
1271 }
1272
1273 pub fn modal<V: 'static + View>(&self) -> Option<ViewHandle<V>> {
1274 self.modal
1275 .as_ref()
1276 .and_then(|modal| modal.clone().downcast::<V>())
1277 }
1278
1279 pub fn dismiss_modal(&mut self, cx: &mut ViewContext<Self>) {
1280 if self.modal.take().is_some() {
1281 cx.focus(&self.active_pane);
1282 cx.notify();
1283 }
1284 }
1285
1286 pub fn items<'a>(
1287 &'a self,
1288 cx: &'a AppContext,
1289 ) -> impl 'a + Iterator<Item = &Box<dyn ItemHandle>> {
1290 self.panes.iter().flat_map(|pane| pane.read(cx).items())
1291 }
1292
1293 pub fn item_of_type<T: Item>(&self, cx: &AppContext) -> Option<ViewHandle<T>> {
1294 self.items_of_type(cx).max_by_key(|item| item.id())
1295 }
1296
1297 pub fn items_of_type<'a, T: Item>(
1298 &'a self,
1299 cx: &'a AppContext,
1300 ) -> impl 'a + Iterator<Item = ViewHandle<T>> {
1301 self.panes
1302 .iter()
1303 .flat_map(|pane| pane.read(cx).items_of_type())
1304 }
1305
1306 pub fn active_item(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
1307 self.active_pane().read(cx).active_item()
1308 }
1309
1310 fn active_project_path(&self, cx: &ViewContext<Self>) -> Option<ProjectPath> {
1311 self.active_item(cx).and_then(|item| item.project_path(cx))
1312 }
1313
1314 pub fn save_active_item(
1315 &mut self,
1316 force_name_change: bool,
1317 cx: &mut ViewContext<Self>,
1318 ) -> Task<Result<()>> {
1319 let project = self.project.clone();
1320 if let Some(item) = self.active_item(cx) {
1321 if !force_name_change && item.can_save(cx) {
1322 if item.has_conflict(cx.as_ref()) {
1323 const CONFLICT_MESSAGE: &str = "This file has changed on disk since you started editing it. Do you want to overwrite it?";
1324
1325 let mut answer = cx.prompt(
1326 PromptLevel::Warning,
1327 CONFLICT_MESSAGE,
1328 &["Overwrite", "Cancel"],
1329 );
1330 cx.spawn(|_, mut cx| async move {
1331 let answer = answer.recv().await;
1332 if answer == Some(0) {
1333 cx.update(|cx| item.save(project, cx)).await?;
1334 }
1335 Ok(())
1336 })
1337 } else {
1338 item.save(project, cx)
1339 }
1340 } else if item.is_singleton(cx) {
1341 let worktree = self.worktrees(cx).next();
1342 let start_abs_path = worktree
1343 .and_then(|w| w.read(cx).as_local())
1344 .map_or(Path::new(""), |w| w.abs_path())
1345 .to_path_buf();
1346 let mut abs_path = cx.prompt_for_new_path(&start_abs_path);
1347 cx.spawn(|_, mut cx| async move {
1348 if let Some(abs_path) = abs_path.recv().await.flatten() {
1349 cx.update(|cx| item.save_as(project, abs_path, cx)).await?;
1350 }
1351 Ok(())
1352 })
1353 } else {
1354 Task::ready(Ok(()))
1355 }
1356 } else {
1357 Task::ready(Ok(()))
1358 }
1359 }
1360
1361 pub fn toggle_sidebar(&mut self, sidebar_side: SidebarSide, cx: &mut ViewContext<Self>) {
1362 let sidebar = match sidebar_side {
1363 SidebarSide::Left => &mut self.left_sidebar,
1364 SidebarSide::Right => &mut self.right_sidebar,
1365 };
1366 let open = sidebar.update(cx, |sidebar, cx| {
1367 let open = !sidebar.is_open();
1368 sidebar.set_open(open, cx);
1369 open
1370 });
1371
1372 if open {
1373 Dock::hide_on_sidebar_shown(self, sidebar_side, cx);
1374 }
1375
1376 self.serialize_workspace(cx);
1377
1378 cx.focus_self();
1379 cx.notify();
1380 }
1381
1382 pub fn toggle_sidebar_item(&mut self, action: &ToggleSidebarItem, cx: &mut ViewContext<Self>) {
1383 let sidebar = match action.sidebar_side {
1384 SidebarSide::Left => &mut self.left_sidebar,
1385 SidebarSide::Right => &mut self.right_sidebar,
1386 };
1387 let active_item = sidebar.update(cx, move |sidebar, cx| {
1388 if sidebar.is_open() && sidebar.active_item_ix() == action.item_index {
1389 sidebar.set_open(false, cx);
1390 None
1391 } else {
1392 sidebar.set_open(true, cx);
1393 sidebar.activate_item(action.item_index, cx);
1394 sidebar.active_item().cloned()
1395 }
1396 });
1397
1398 if let Some(active_item) = active_item {
1399 Dock::hide_on_sidebar_shown(self, action.sidebar_side, cx);
1400
1401 if active_item.is_focused(cx) {
1402 cx.focus_self();
1403 } else {
1404 cx.focus(active_item.to_any());
1405 }
1406 } else {
1407 cx.focus_self();
1408 }
1409
1410 self.serialize_workspace(cx);
1411
1412 cx.notify();
1413 }
1414
1415 pub fn toggle_sidebar_item_focus(
1416 &mut self,
1417 sidebar_side: SidebarSide,
1418 item_index: usize,
1419 cx: &mut ViewContext<Self>,
1420 ) {
1421 let sidebar = match sidebar_side {
1422 SidebarSide::Left => &mut self.left_sidebar,
1423 SidebarSide::Right => &mut self.right_sidebar,
1424 };
1425 let active_item = sidebar.update(cx, |sidebar, cx| {
1426 sidebar.set_open(true, cx);
1427 sidebar.activate_item(item_index, cx);
1428 sidebar.active_item().cloned()
1429 });
1430 if let Some(active_item) = active_item {
1431 Dock::hide_on_sidebar_shown(self, sidebar_side, cx);
1432
1433 if active_item.is_focused(cx) {
1434 cx.focus_self();
1435 } else {
1436 cx.focus(active_item.to_any());
1437 }
1438 }
1439
1440 self.serialize_workspace(cx);
1441
1442 cx.notify();
1443 }
1444
1445 pub fn focus_center(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
1446 cx.focus_self();
1447 cx.notify();
1448 }
1449
1450 fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
1451 let pane =
1452 cx.add_view(|cx| Pane::new(self.weak_handle().id(), None, self.background_actions, cx));
1453 let pane_id = pane.id();
1454 cx.subscribe(&pane, move |this, _, event, cx| {
1455 this.handle_pane_event(pane_id, event, cx)
1456 })
1457 .detach();
1458 self.panes.push(pane.clone());
1459 cx.focus(pane.clone());
1460 cx.emit(Event::PaneAdded(pane.clone()));
1461 pane
1462 }
1463
1464 pub fn add_item_to_center(
1465 &mut self,
1466 item: Box<dyn ItemHandle>,
1467 cx: &mut ViewContext<Self>,
1468 ) -> bool {
1469 if let Some(center_pane) = self.last_active_center_pane.clone() {
1470 if let Some(center_pane) = center_pane.upgrade(cx) {
1471 Pane::add_item(self, ¢er_pane, item, true, true, None, cx);
1472 true
1473 } else {
1474 false
1475 }
1476 } else {
1477 false
1478 }
1479 }
1480
1481 pub fn add_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
1482 let active_pane = self.active_pane().clone();
1483 Pane::add_item(self, &active_pane, item, true, true, None, cx);
1484 }
1485
1486 pub fn open_path(
1487 &mut self,
1488 path: impl Into<ProjectPath>,
1489 pane: Option<WeakViewHandle<Pane>>,
1490 focus_item: bool,
1491 cx: &mut ViewContext<Self>,
1492 ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
1493 let pane = pane.unwrap_or_else(|| {
1494 if !self.dock_active() {
1495 self.active_pane().downgrade()
1496 } else {
1497 self.last_active_center_pane.clone().unwrap_or_else(|| {
1498 self.panes
1499 .first()
1500 .expect("There must be an active pane")
1501 .downgrade()
1502 })
1503 }
1504 });
1505
1506 let task = self.load_path(path.into(), cx);
1507 cx.spawn(|this, mut cx| async move {
1508 let (project_entry_id, build_item) = task.await?;
1509 let pane = pane
1510 .upgrade(&cx)
1511 .ok_or_else(|| anyhow!("pane was closed"))?;
1512 this.update(&mut cx, |this, cx| {
1513 Ok(Pane::open_item(
1514 this,
1515 pane,
1516 project_entry_id,
1517 focus_item,
1518 cx,
1519 build_item,
1520 ))
1521 })
1522 })
1523 }
1524
1525 pub(crate) fn load_path(
1526 &mut self,
1527 path: ProjectPath,
1528 cx: &mut ViewContext<Self>,
1529 ) -> Task<
1530 Result<(
1531 ProjectEntryId,
1532 impl 'static + FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
1533 )>,
1534 > {
1535 let project = self.project().clone();
1536 let project_item = project.update(cx, |project, cx| project.open_path(path, cx));
1537 cx.as_mut().spawn(|mut cx| async move {
1538 let (project_entry_id, project_item) = project_item.await?;
1539 let build_item = cx.update(|cx| {
1540 cx.default_global::<ProjectItemBuilders>()
1541 .get(&project_item.model_type())
1542 .ok_or_else(|| anyhow!("no item builder for project item"))
1543 .cloned()
1544 })?;
1545 let build_item =
1546 move |cx: &mut ViewContext<Pane>| build_item(project, project_item, cx);
1547 Ok((project_entry_id, build_item))
1548 })
1549 }
1550
1551 pub fn open_project_item<T>(
1552 &mut self,
1553 project_item: ModelHandle<T::Item>,
1554 cx: &mut ViewContext<Self>,
1555 ) -> ViewHandle<T>
1556 where
1557 T: ProjectItem,
1558 {
1559 use project::Item as _;
1560
1561 let entry_id = project_item.read(cx).entry_id(cx);
1562 if let Some(item) = entry_id
1563 .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id, cx))
1564 .and_then(|item| item.downcast())
1565 {
1566 self.activate_item(&item, cx);
1567 return item;
1568 }
1569
1570 let item = cx.add_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
1571 self.add_item(Box::new(item.clone()), cx);
1572 item
1573 }
1574
1575 pub fn open_shared_screen(&mut self, action: &OpenSharedScreen, cx: &mut ViewContext<Self>) {
1576 if let Some(shared_screen) =
1577 self.shared_screen_for_peer(action.peer_id, &self.active_pane, cx)
1578 {
1579 let pane = self.active_pane.clone();
1580 Pane::add_item(self, &pane, Box::new(shared_screen), false, true, None, cx);
1581 }
1582 }
1583
1584 pub fn activate_item(&mut self, item: &dyn ItemHandle, cx: &mut ViewContext<Self>) -> bool {
1585 let result = self.panes.iter().find_map(|pane| {
1586 pane.read(cx)
1587 .index_for_item(item)
1588 .map(|ix| (pane.clone(), ix))
1589 });
1590 if let Some((pane, ix)) = result {
1591 pane.update(cx, |pane, cx| pane.activate_item(ix, true, true, cx));
1592 true
1593 } else {
1594 false
1595 }
1596 }
1597
1598 fn activate_pane_at_index(&mut self, action: &ActivatePane, cx: &mut ViewContext<Self>) {
1599 let panes = self.center.panes();
1600 if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
1601 cx.focus(pane);
1602 } else {
1603 self.split_pane(self.active_pane.clone(), SplitDirection::Right, cx);
1604 }
1605 }
1606
1607 pub fn activate_next_pane(&mut self, cx: &mut ViewContext<Self>) {
1608 let panes = self.center.panes();
1609 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
1610 let next_ix = (ix + 1) % panes.len();
1611 let next_pane = panes[next_ix].clone();
1612 cx.focus(next_pane);
1613 }
1614 }
1615
1616 pub fn activate_previous_pane(&mut self, cx: &mut ViewContext<Self>) {
1617 let panes = self.center.panes();
1618 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
1619 let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
1620 let prev_pane = panes[prev_ix].clone();
1621 cx.focus(prev_pane);
1622 }
1623 }
1624
1625 fn handle_pane_focused(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
1626 if self.active_pane != pane {
1627 self.active_pane
1628 .update(cx, |pane, cx| pane.set_active(false, cx));
1629 self.active_pane = pane.clone();
1630 self.active_pane
1631 .update(cx, |pane, cx| pane.set_active(true, cx));
1632 self.status_bar.update(cx, |status_bar, cx| {
1633 status_bar.set_active_pane(&self.active_pane, cx);
1634 });
1635 self.active_item_path_changed(cx);
1636
1637 if &pane == self.dock_pane() {
1638 Dock::show(self, true, cx);
1639 } else {
1640 self.last_active_center_pane = Some(pane.downgrade());
1641 if self.dock.is_anchored_at(DockAnchor::Expanded) {
1642 Dock::hide(self, cx);
1643 }
1644 }
1645 cx.notify();
1646 }
1647
1648 self.update_followers(
1649 proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView {
1650 id: self.active_item(cx).and_then(|item| {
1651 item.to_followable_item_handle(cx)?
1652 .remote_id(&self.client, cx)
1653 .map(|id| id.to_proto())
1654 }),
1655 leader_id: self.leader_for_pane(&pane),
1656 }),
1657 cx,
1658 );
1659 }
1660
1661 fn handle_pane_event(
1662 &mut self,
1663 pane_id: usize,
1664 event: &pane::Event,
1665 cx: &mut ViewContext<Self>,
1666 ) {
1667 if let Some(pane) = self.pane(pane_id) {
1668 let is_dock = &pane == self.dock.pane();
1669 match event {
1670 pane::Event::Split(direction) if !is_dock => {
1671 self.split_pane(pane, *direction, cx);
1672 }
1673 pane::Event::Remove if !is_dock => self.remove_pane(pane, cx),
1674 pane::Event::Remove if is_dock => Dock::hide(self, cx),
1675 pane::Event::ActivateItem { local } => {
1676 if *local {
1677 self.unfollow(&pane, cx);
1678 }
1679 if &pane == self.active_pane() {
1680 self.active_item_path_changed(cx);
1681 }
1682 }
1683 pane::Event::ChangeItemTitle => {
1684 if pane == self.active_pane {
1685 self.active_item_path_changed(cx);
1686 }
1687 self.update_window_edited(cx);
1688 }
1689 pane::Event::RemoveItem { item_id } => {
1690 self.update_window_edited(cx);
1691 if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) {
1692 if entry.get().id() == pane.id() {
1693 entry.remove();
1694 }
1695 }
1696 }
1697 _ => {}
1698 }
1699
1700 self.serialize_workspace(cx);
1701 } else if self.dock.visible_pane().is_none() {
1702 error!("pane {} not found", pane_id);
1703 }
1704 }
1705
1706 pub fn split_pane(
1707 &mut self,
1708 pane: ViewHandle<Pane>,
1709 direction: SplitDirection,
1710 cx: &mut ViewContext<Self>,
1711 ) -> Option<ViewHandle<Pane>> {
1712 if &pane == self.dock_pane() {
1713 warn!("Can't split dock pane.");
1714 return None;
1715 }
1716
1717 let item = pane.read(cx).active_item()?;
1718 let maybe_pane_handle =
1719 if let Some(clone) = item.clone_on_split(self.database_id(), cx.as_mut()) {
1720 let new_pane = self.add_pane(cx);
1721 Pane::add_item(self, &new_pane, clone, true, true, None, cx);
1722 self.center.split(&pane, &new_pane, direction).unwrap();
1723 Some(new_pane)
1724 } else {
1725 None
1726 };
1727 cx.notify();
1728 maybe_pane_handle
1729 }
1730
1731 pub fn split_pane_with_item(&mut self, action: &SplitWithItem, cx: &mut ViewContext<Self>) {
1732 let Some(pane_to_split) = action.pane_to_split.upgrade(cx) else { return; };
1733 let Some(from) = action.from.upgrade(cx) else { return; };
1734 if &pane_to_split == self.dock_pane() {
1735 warn!("Can't split dock pane.");
1736 return;
1737 }
1738
1739 let new_pane = self.add_pane(cx);
1740 Pane::move_item(
1741 self,
1742 from.clone(),
1743 new_pane.clone(),
1744 action.item_id_to_move,
1745 0,
1746 cx,
1747 );
1748 self.center
1749 .split(&pane_to_split, &new_pane, action.split_direction)
1750 .unwrap();
1751 cx.notify();
1752 }
1753
1754 pub fn split_pane_with_project_entry(
1755 &mut self,
1756 action: &SplitWithProjectEntry,
1757 cx: &mut ViewContext<Self>,
1758 ) -> Option<Task<Result<()>>> {
1759 let pane_to_split = action.pane_to_split.upgrade(cx)?;
1760 if &pane_to_split == self.dock_pane() {
1761 warn!("Can't split dock pane.");
1762 return None;
1763 }
1764
1765 let new_pane = self.add_pane(cx);
1766 self.center
1767 .split(&pane_to_split, &new_pane, action.split_direction)
1768 .unwrap();
1769
1770 let path = self
1771 .project
1772 .read(cx)
1773 .path_for_entry(action.project_entry, cx)?;
1774 let task = self.open_path(path, Some(new_pane.downgrade()), true, cx);
1775 Some(cx.foreground().spawn(async move {
1776 task.await?;
1777 Ok(())
1778 }))
1779 }
1780
1781 fn remove_pane(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
1782 if self.center.remove(&pane).unwrap() {
1783 self.panes.retain(|p| p != &pane);
1784 cx.focus(self.panes.last().unwrap().clone());
1785 self.unfollow(&pane, cx);
1786 self.last_leaders_by_pane.remove(&pane.downgrade());
1787 for removed_item in pane.read(cx).items() {
1788 self.panes_by_item.remove(&removed_item.id());
1789 }
1790 if self.last_active_center_pane == Some(pane.downgrade()) {
1791 self.last_active_center_pane = None;
1792 }
1793
1794 cx.notify();
1795 } else {
1796 self.active_item_path_changed(cx);
1797 }
1798 }
1799
1800 pub fn panes(&self) -> &[ViewHandle<Pane>] {
1801 &self.panes
1802 }
1803
1804 fn pane(&self, pane_id: usize) -> Option<ViewHandle<Pane>> {
1805 self.panes.iter().find(|pane| pane.id() == pane_id).cloned()
1806 }
1807
1808 pub fn active_pane(&self) -> &ViewHandle<Pane> {
1809 &self.active_pane
1810 }
1811
1812 pub fn dock_pane(&self) -> &ViewHandle<Pane> {
1813 self.dock.pane()
1814 }
1815
1816 fn dock_active(&self) -> bool {
1817 &self.active_pane == self.dock.pane()
1818 }
1819
1820 fn project_remote_id_changed(&mut self, remote_id: Option<u64>, cx: &mut ViewContext<Self>) {
1821 if let Some(remote_id) = remote_id {
1822 self.remote_entity_subscription =
1823 Some(self.client.add_view_for_remote_entity(remote_id, cx));
1824 } else {
1825 self.remote_entity_subscription.take();
1826 }
1827 }
1828
1829 fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
1830 self.leader_state.followers.remove(&peer_id);
1831 if let Some(states_by_pane) = self.follower_states_by_leader.remove(&peer_id) {
1832 for state in states_by_pane.into_values() {
1833 for item in state.items_by_leader_view_id.into_values() {
1834 item.set_leader_replica_id(None, cx);
1835 }
1836 }
1837 }
1838 cx.notify();
1839 }
1840
1841 pub fn toggle_follow(
1842 &mut self,
1843 ToggleFollow(leader_id): &ToggleFollow,
1844 cx: &mut ViewContext<Self>,
1845 ) -> Option<Task<Result<()>>> {
1846 let leader_id = *leader_id;
1847 let pane = self.active_pane().clone();
1848
1849 if let Some(prev_leader_id) = self.unfollow(&pane, cx) {
1850 if leader_id == prev_leader_id {
1851 return None;
1852 }
1853 }
1854
1855 self.last_leaders_by_pane
1856 .insert(pane.downgrade(), leader_id);
1857 self.follower_states_by_leader
1858 .entry(leader_id)
1859 .or_default()
1860 .insert(pane.clone(), Default::default());
1861 cx.notify();
1862
1863 let project_id = self.project.read(cx).remote_id()?;
1864 let request = self.client.request(proto::Follow {
1865 project_id,
1866 leader_id: Some(leader_id),
1867 });
1868
1869 Some(cx.spawn_weak(|this, mut cx| async move {
1870 let response = request.await?;
1871 if let Some(this) = this.upgrade(&cx) {
1872 this.update(&mut cx, |this, _| {
1873 let state = this
1874 .follower_states_by_leader
1875 .get_mut(&leader_id)
1876 .and_then(|states_by_pane| states_by_pane.get_mut(&pane))
1877 .ok_or_else(|| anyhow!("following interrupted"))?;
1878 state.active_view_id = if let Some(active_view_id) = response.active_view_id {
1879 Some(ViewId::from_proto(active_view_id)?)
1880 } else {
1881 None
1882 };
1883 Ok::<_, anyhow::Error>(())
1884 })?;
1885 Self::add_views_from_leader(
1886 this.clone(),
1887 leader_id,
1888 vec![pane],
1889 response.views,
1890 &mut cx,
1891 )
1892 .await?;
1893 this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx));
1894 }
1895 Ok(())
1896 }))
1897 }
1898
1899 pub fn follow_next_collaborator(
1900 &mut self,
1901 _: &FollowNextCollaborator,
1902 cx: &mut ViewContext<Self>,
1903 ) -> Option<Task<Result<()>>> {
1904 let collaborators = self.project.read(cx).collaborators();
1905 let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
1906 let mut collaborators = collaborators.keys().copied();
1907 for peer_id in collaborators.by_ref() {
1908 if peer_id == leader_id {
1909 break;
1910 }
1911 }
1912 collaborators.next()
1913 } else if let Some(last_leader_id) =
1914 self.last_leaders_by_pane.get(&self.active_pane.downgrade())
1915 {
1916 if collaborators.contains_key(last_leader_id) {
1917 Some(*last_leader_id)
1918 } else {
1919 None
1920 }
1921 } else {
1922 None
1923 };
1924
1925 next_leader_id
1926 .or_else(|| collaborators.keys().copied().next())
1927 .and_then(|leader_id| self.toggle_follow(&ToggleFollow(leader_id), cx))
1928 }
1929
1930 pub fn unfollow(
1931 &mut self,
1932 pane: &ViewHandle<Pane>,
1933 cx: &mut ViewContext<Self>,
1934 ) -> Option<PeerId> {
1935 for (leader_id, states_by_pane) in &mut self.follower_states_by_leader {
1936 let leader_id = *leader_id;
1937 if let Some(state) = states_by_pane.remove(pane) {
1938 for (_, item) in state.items_by_leader_view_id {
1939 item.set_leader_replica_id(None, cx);
1940 }
1941
1942 if states_by_pane.is_empty() {
1943 self.follower_states_by_leader.remove(&leader_id);
1944 if let Some(project_id) = self.project.read(cx).remote_id() {
1945 self.client
1946 .send(proto::Unfollow {
1947 project_id,
1948 leader_id: Some(leader_id),
1949 })
1950 .log_err();
1951 }
1952 }
1953
1954 cx.notify();
1955 return Some(leader_id);
1956 }
1957 }
1958 None
1959 }
1960
1961 pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
1962 self.follower_states_by_leader.contains_key(&peer_id)
1963 }
1964
1965 pub fn is_followed_by(&self, peer_id: PeerId) -> bool {
1966 self.leader_state.followers.contains(&peer_id)
1967 }
1968
1969 fn render_titlebar(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
1970 // TODO: There should be a better system in place for this
1971 // (https://github.com/zed-industries/zed/issues/1290)
1972 let is_fullscreen = cx.window_is_fullscreen(cx.window_id());
1973 let container_theme = if is_fullscreen {
1974 let mut container_theme = theme.workspace.titlebar.container;
1975 container_theme.padding.left = container_theme.padding.right;
1976 container_theme
1977 } else {
1978 theme.workspace.titlebar.container
1979 };
1980
1981 enum TitleBar {}
1982 ConstrainedBox::new(
1983 MouseEventHandler::<TitleBar>::new(0, cx, |_, cx| {
1984 Container::new(
1985 Stack::new()
1986 .with_children(
1987 self.titlebar_item
1988 .as_ref()
1989 .map(|item| ChildView::new(item, cx).boxed()),
1990 )
1991 .boxed(),
1992 )
1993 .with_style(container_theme)
1994 .boxed()
1995 })
1996 .on_click(MouseButton::Left, |event, cx| {
1997 if event.click_count == 2 {
1998 cx.zoom_window(cx.window_id());
1999 }
2000 })
2001 .boxed(),
2002 )
2003 .with_height(theme.workspace.titlebar.height)
2004 .named("titlebar")
2005 }
2006
2007 fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
2008 let active_entry = self.active_project_path(cx);
2009 self.project
2010 .update(cx, |project, cx| project.set_active_path(active_entry, cx));
2011 self.update_window_title(cx);
2012 }
2013
2014 fn update_window_title(&mut self, cx: &mut ViewContext<Self>) {
2015 let project = self.project().read(cx);
2016 let mut title = String::new();
2017
2018 if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
2019 let filename = path
2020 .path
2021 .file_name()
2022 .map(|s| s.to_string_lossy())
2023 .or_else(|| {
2024 Some(Cow::Borrowed(
2025 project
2026 .worktree_for_id(path.worktree_id, cx)?
2027 .read(cx)
2028 .root_name(),
2029 ))
2030 });
2031
2032 if let Some(filename) = filename {
2033 title.push_str(filename.as_ref());
2034 title.push_str(" — ");
2035 }
2036 }
2037
2038 for (i, name) in project.worktree_root_names(cx).enumerate() {
2039 if i > 0 {
2040 title.push_str(", ");
2041 }
2042 title.push_str(name);
2043 }
2044
2045 if title.is_empty() {
2046 title = "empty project".to_string();
2047 }
2048
2049 if project.is_remote() {
2050 title.push_str(" ↙");
2051 } else if project.is_shared() {
2052 title.push_str(" ↗");
2053 }
2054
2055 cx.set_window_title(&title);
2056 }
2057
2058 fn update_window_edited(&mut self, cx: &mut ViewContext<Self>) {
2059 let is_edited = !self.project.read(cx).is_read_only()
2060 && self
2061 .items(cx)
2062 .any(|item| item.has_conflict(cx) || item.is_dirty(cx));
2063 if is_edited != self.window_edited {
2064 self.window_edited = is_edited;
2065 cx.set_window_edited(self.window_edited)
2066 }
2067 }
2068
2069 fn render_disconnected_overlay(&self, cx: &mut RenderContext<Workspace>) -> Option<ElementBox> {
2070 if self.project.read(cx).is_read_only() {
2071 enum DisconnectedOverlay {}
2072 Some(
2073 MouseEventHandler::<DisconnectedOverlay>::new(0, cx, |_, cx| {
2074 let theme = &cx.global::<Settings>().theme;
2075 Label::new(
2076 "Your connection to the remote project has been lost.",
2077 theme.workspace.disconnected_overlay.text.clone(),
2078 )
2079 .aligned()
2080 .contained()
2081 .with_style(theme.workspace.disconnected_overlay.container)
2082 .boxed()
2083 })
2084 .with_cursor_style(CursorStyle::Arrow)
2085 .capture_all()
2086 .boxed(),
2087 )
2088 } else {
2089 None
2090 }
2091 }
2092
2093 fn render_notifications(
2094 &self,
2095 theme: &theme::Workspace,
2096 cx: &AppContext,
2097 ) -> Option<ElementBox> {
2098 if self.notifications.is_empty() {
2099 None
2100 } else {
2101 Some(
2102 Flex::column()
2103 .with_children(self.notifications.iter().map(|(_, _, notification)| {
2104 ChildView::new(notification.as_ref(), cx)
2105 .contained()
2106 .with_style(theme.notification)
2107 .boxed()
2108 }))
2109 .constrained()
2110 .with_width(theme.notifications.width)
2111 .contained()
2112 .with_style(theme.notifications.container)
2113 .aligned()
2114 .bottom()
2115 .right()
2116 .boxed(),
2117 )
2118 }
2119 }
2120
2121 // RPC handlers
2122
2123 async fn handle_follow(
2124 this: ViewHandle<Self>,
2125 envelope: TypedEnvelope<proto::Follow>,
2126 _: Arc<Client>,
2127 mut cx: AsyncAppContext,
2128 ) -> Result<proto::FollowResponse> {
2129 this.update(&mut cx, |this, cx| {
2130 let client = &this.client;
2131 this.leader_state
2132 .followers
2133 .insert(envelope.original_sender_id()?);
2134
2135 let active_view_id = this.active_item(cx).and_then(|i| {
2136 Some(
2137 i.to_followable_item_handle(cx)?
2138 .remote_id(client, cx)?
2139 .to_proto(),
2140 )
2141 });
2142
2143 cx.notify();
2144
2145 Ok(proto::FollowResponse {
2146 active_view_id,
2147 views: this
2148 .panes()
2149 .iter()
2150 .flat_map(|pane| {
2151 let leader_id = this.leader_for_pane(pane);
2152 pane.read(cx).items().filter_map({
2153 let cx = &cx;
2154 move |item| {
2155 let item = item.to_followable_item_handle(cx)?;
2156 let id = item.remote_id(client, cx)?.to_proto();
2157 let variant = item.to_state_proto(cx)?;
2158 Some(proto::View {
2159 id: Some(id),
2160 leader_id,
2161 variant: Some(variant),
2162 })
2163 }
2164 })
2165 })
2166 .collect(),
2167 })
2168 })
2169 }
2170
2171 async fn handle_unfollow(
2172 this: ViewHandle<Self>,
2173 envelope: TypedEnvelope<proto::Unfollow>,
2174 _: Arc<Client>,
2175 mut cx: AsyncAppContext,
2176 ) -> Result<()> {
2177 this.update(&mut cx, |this, cx| {
2178 this.leader_state
2179 .followers
2180 .remove(&envelope.original_sender_id()?);
2181 cx.notify();
2182 Ok(())
2183 })
2184 }
2185
2186 async fn handle_update_followers(
2187 this: ViewHandle<Self>,
2188 envelope: TypedEnvelope<proto::UpdateFollowers>,
2189 _: Arc<Client>,
2190 cx: AsyncAppContext,
2191 ) -> Result<()> {
2192 let leader_id = envelope.original_sender_id()?;
2193 this.read_with(&cx, |this, _| {
2194 this.leader_updates_tx
2195 .unbounded_send((leader_id, envelope.payload))
2196 })?;
2197 Ok(())
2198 }
2199
2200 async fn process_leader_update(
2201 this: ViewHandle<Self>,
2202 leader_id: PeerId,
2203 update: proto::UpdateFollowers,
2204 cx: &mut AsyncAppContext,
2205 ) -> Result<()> {
2206 match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
2207 proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
2208 this.update(cx, |this, _| {
2209 if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
2210 for state in state.values_mut() {
2211 state.active_view_id =
2212 if let Some(active_view_id) = update_active_view.id.clone() {
2213 Some(ViewId::from_proto(active_view_id)?)
2214 } else {
2215 None
2216 };
2217 }
2218 }
2219 anyhow::Ok(())
2220 })?;
2221 }
2222 proto::update_followers::Variant::UpdateView(update_view) => {
2223 let variant = update_view
2224 .variant
2225 .ok_or_else(|| anyhow!("missing update view variant"))?;
2226 let id = update_view
2227 .id
2228 .ok_or_else(|| anyhow!("missing update view id"))?;
2229 let mut tasks = Vec::new();
2230 this.update(cx, |this, cx| {
2231 let project = this.project.clone();
2232 if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
2233 for state in state.values_mut() {
2234 let view_id = ViewId::from_proto(id.clone())?;
2235 if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
2236 tasks.push(item.apply_update_proto(&project, variant.clone(), cx));
2237 }
2238 }
2239 }
2240 anyhow::Ok(())
2241 })?;
2242 try_join_all(tasks).await.log_err();
2243 }
2244 proto::update_followers::Variant::CreateView(view) => {
2245 let panes = this.read_with(cx, |this, _| {
2246 this.follower_states_by_leader
2247 .get(&leader_id)
2248 .into_iter()
2249 .flat_map(|states_by_pane| states_by_pane.keys())
2250 .cloned()
2251 .collect()
2252 });
2253 Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], cx).await?;
2254 }
2255 }
2256 this.update(cx, |this, cx| this.leader_updated(leader_id, cx));
2257 Ok(())
2258 }
2259
2260 async fn add_views_from_leader(
2261 this: ViewHandle<Self>,
2262 leader_id: PeerId,
2263 panes: Vec<ViewHandle<Pane>>,
2264 views: Vec<proto::View>,
2265 cx: &mut AsyncAppContext,
2266 ) -> Result<()> {
2267 let project = this.read_with(cx, |this, _| this.project.clone());
2268 let replica_id = project
2269 .read_with(cx, |project, _| {
2270 project
2271 .collaborators()
2272 .get(&leader_id)
2273 .map(|c| c.replica_id)
2274 })
2275 .ok_or_else(|| anyhow!("no such collaborator {}", leader_id))?;
2276
2277 let item_builders = cx.update(|cx| {
2278 cx.default_global::<FollowableItemBuilders>()
2279 .values()
2280 .map(|b| b.0)
2281 .collect::<Vec<_>>()
2282 });
2283
2284 let mut item_tasks_by_pane = HashMap::default();
2285 for pane in panes {
2286 let mut item_tasks = Vec::new();
2287 let mut leader_view_ids = Vec::new();
2288 for view in &views {
2289 let Some(id) = &view.id else { continue };
2290 let id = ViewId::from_proto(id.clone())?;
2291 let mut variant = view.variant.clone();
2292 if variant.is_none() {
2293 Err(anyhow!("missing variant"))?;
2294 }
2295 for build_item in &item_builders {
2296 let task = cx.update(|cx| {
2297 build_item(pane.clone(), project.clone(), id, &mut variant, cx)
2298 });
2299 if let Some(task) = task {
2300 item_tasks.push(task);
2301 leader_view_ids.push(id);
2302 break;
2303 } else {
2304 assert!(variant.is_some());
2305 }
2306 }
2307 }
2308
2309 item_tasks_by_pane.insert(pane, (item_tasks, leader_view_ids));
2310 }
2311
2312 for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane {
2313 let items = futures::future::try_join_all(item_tasks).await?;
2314 this.update(cx, |this, cx| {
2315 let state = this
2316 .follower_states_by_leader
2317 .get_mut(&leader_id)?
2318 .get_mut(&pane)?;
2319
2320 for (id, item) in leader_view_ids.into_iter().zip(items) {
2321 item.set_leader_replica_id(Some(replica_id), cx);
2322 state.items_by_leader_view_id.insert(id, item);
2323 }
2324
2325 Some(())
2326 });
2327 }
2328 Ok(())
2329 }
2330
2331 fn update_followers(
2332 &self,
2333 update: proto::update_followers::Variant,
2334 cx: &AppContext,
2335 ) -> Option<()> {
2336 let project_id = self.project.read(cx).remote_id()?;
2337 if !self.leader_state.followers.is_empty() {
2338 self.client
2339 .send(proto::UpdateFollowers {
2340 project_id,
2341 follower_ids: self.leader_state.followers.iter().copied().collect(),
2342 variant: Some(update),
2343 })
2344 .log_err();
2345 }
2346 None
2347 }
2348
2349 pub fn leader_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<PeerId> {
2350 self.follower_states_by_leader
2351 .iter()
2352 .find_map(|(leader_id, state)| {
2353 if state.contains_key(pane) {
2354 Some(*leader_id)
2355 } else {
2356 None
2357 }
2358 })
2359 }
2360
2361 fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
2362 cx.notify();
2363
2364 let call = self.active_call()?;
2365 let room = call.read(cx).room()?.read(cx);
2366 let participant = room.remote_participant_for_peer_id(leader_id)?;
2367 let mut items_to_activate = Vec::new();
2368 match participant.location {
2369 call::ParticipantLocation::SharedProject { project_id } => {
2370 if Some(project_id) == self.project.read(cx).remote_id() {
2371 for (pane, state) in self.follower_states_by_leader.get(&leader_id)? {
2372 if let Some(item) = state
2373 .active_view_id
2374 .and_then(|id| state.items_by_leader_view_id.get(&id))
2375 {
2376 items_to_activate.push((pane.clone(), item.boxed_clone()));
2377 } else {
2378 if let Some(shared_screen) =
2379 self.shared_screen_for_peer(leader_id, pane, cx)
2380 {
2381 items_to_activate.push((pane.clone(), Box::new(shared_screen)));
2382 }
2383 }
2384 }
2385 }
2386 }
2387 call::ParticipantLocation::UnsharedProject => {}
2388 call::ParticipantLocation::External => {
2389 for (pane, _) in self.follower_states_by_leader.get(&leader_id)? {
2390 if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
2391 items_to_activate.push((pane.clone(), Box::new(shared_screen)));
2392 }
2393 }
2394 }
2395 }
2396
2397 for (pane, item) in items_to_activate {
2398 let active_item_was_focused = pane
2399 .read(cx)
2400 .active_item()
2401 .map(|active_item| cx.is_child_focused(active_item.to_any()))
2402 .unwrap_or_default();
2403
2404 if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) {
2405 pane.update(cx, |pane, cx| pane.activate_item(index, false, false, cx));
2406 } else {
2407 Pane::add_item(self, &pane, item.boxed_clone(), false, false, None, cx);
2408 }
2409
2410 if active_item_was_focused {
2411 pane.update(cx, |pane, cx| pane.focus_active_item(cx));
2412 }
2413 }
2414
2415 None
2416 }
2417
2418 fn shared_screen_for_peer(
2419 &self,
2420 peer_id: PeerId,
2421 pane: &ViewHandle<Pane>,
2422 cx: &mut ViewContext<Self>,
2423 ) -> Option<ViewHandle<SharedScreen>> {
2424 let call = self.active_call()?;
2425 let room = call.read(cx).room()?.read(cx);
2426 let participant = room.remote_participant_for_peer_id(peer_id)?;
2427 let track = participant.tracks.values().next()?.clone();
2428 let user = participant.user.clone();
2429
2430 for item in pane.read(cx).items_of_type::<SharedScreen>() {
2431 if item.read(cx).peer_id == peer_id {
2432 return Some(item);
2433 }
2434 }
2435
2436 Some(cx.add_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx)))
2437 }
2438
2439 pub fn on_window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
2440 if active {
2441 cx.background()
2442 .spawn(persistence::DB.update_timestamp(self.database_id()))
2443 .detach();
2444 } else {
2445 for pane in &self.panes {
2446 pane.update(cx, |pane, cx| {
2447 if let Some(item) = pane.active_item() {
2448 item.workspace_deactivated(cx);
2449 }
2450 if matches!(
2451 cx.global::<Settings>().autosave,
2452 Autosave::OnWindowChange | Autosave::OnFocusChange
2453 ) {
2454 for item in pane.items() {
2455 Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
2456 .detach_and_log_err(cx);
2457 }
2458 }
2459 });
2460 }
2461 }
2462 }
2463
2464 fn active_call(&self) -> Option<&ModelHandle<ActiveCall>> {
2465 self.active_call.as_ref().map(|(call, _)| call)
2466 }
2467
2468 fn on_active_call_event(
2469 &mut self,
2470 _: ModelHandle<ActiveCall>,
2471 event: &call::room::Event,
2472 cx: &mut ViewContext<Self>,
2473 ) {
2474 match event {
2475 call::room::Event::ParticipantLocationChanged { participant_id }
2476 | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
2477 self.leader_updated(*participant_id, cx);
2478 }
2479 _ => {}
2480 }
2481 }
2482
2483 pub fn database_id(&self) -> WorkspaceId {
2484 self.database_id
2485 }
2486
2487 fn location(&self, cx: &AppContext) -> Option<WorkspaceLocation> {
2488 let project = self.project().read(cx);
2489
2490 if project.is_local() {
2491 Some(
2492 project
2493 .visible_worktrees(cx)
2494 .map(|worktree| worktree.read(cx).abs_path())
2495 .collect::<Vec<_>>()
2496 .into(),
2497 )
2498 } else {
2499 None
2500 }
2501 }
2502
2503 fn remove_panes(&mut self, member: Member, cx: &mut ViewContext<Workspace>) {
2504 match member {
2505 Member::Axis(PaneAxis { members, .. }) => {
2506 for child in members.iter() {
2507 self.remove_panes(child.clone(), cx)
2508 }
2509 }
2510 Member::Pane(pane) => self.remove_pane(pane.clone(), cx),
2511 }
2512 }
2513
2514 fn serialize_workspace(&self, cx: &AppContext) {
2515 fn serialize_pane_handle(
2516 pane_handle: &ViewHandle<Pane>,
2517 cx: &AppContext,
2518 ) -> SerializedPane {
2519 let (items, active) = {
2520 let pane = pane_handle.read(cx);
2521 let active_item_id = pane.active_item().map(|item| item.id());
2522 (
2523 pane.items()
2524 .filter_map(|item_handle| {
2525 Some(SerializedItem {
2526 kind: Arc::from(item_handle.serialized_item_kind()?),
2527 item_id: item_handle.id(),
2528 active: Some(item_handle.id()) == active_item_id,
2529 })
2530 })
2531 .collect::<Vec<_>>(),
2532 pane.is_active(),
2533 )
2534 };
2535
2536 SerializedPane::new(items, active)
2537 }
2538
2539 fn build_serialized_pane_group(
2540 pane_group: &Member,
2541 cx: &AppContext,
2542 ) -> SerializedPaneGroup {
2543 match pane_group {
2544 Member::Axis(PaneAxis { axis, members }) => SerializedPaneGroup::Group {
2545 axis: *axis,
2546 children: members
2547 .iter()
2548 .map(|member| build_serialized_pane_group(member, cx))
2549 .collect::<Vec<_>>(),
2550 },
2551 Member::Pane(pane_handle) => {
2552 SerializedPaneGroup::Pane(serialize_pane_handle(&pane_handle, cx))
2553 }
2554 }
2555 }
2556
2557 if let Some(location) = self.location(cx) {
2558 // Load bearing special case:
2559 // - with_local_workspace() relies on this to not have other stuff open
2560 // when you open your log
2561 if !location.paths().is_empty() {
2562 let dock_pane = serialize_pane_handle(self.dock.pane(), cx);
2563 let center_group = build_serialized_pane_group(&self.center.root, cx);
2564
2565 let serialized_workspace = SerializedWorkspace {
2566 id: self.database_id,
2567 location,
2568 dock_position: self.dock.position(),
2569 dock_pane,
2570 center_group,
2571 left_sidebar_open: self.left_sidebar.read(cx).is_open(),
2572 bounds: Default::default(),
2573 display: Default::default(),
2574 };
2575
2576 cx.background()
2577 .spawn(persistence::DB.save_workspace(serialized_workspace))
2578 .detach();
2579 }
2580 }
2581 }
2582
2583 fn load_from_serialized_workspace(
2584 workspace: WeakViewHandle<Workspace>,
2585 serialized_workspace: SerializedWorkspace,
2586 cx: &mut MutableAppContext,
2587 ) {
2588 cx.spawn(|mut cx| async move {
2589 if let Some(workspace) = workspace.upgrade(&cx) {
2590 let (project, dock_pane_handle, old_center_pane) =
2591 workspace.read_with(&cx, |workspace, _| {
2592 (
2593 workspace.project().clone(),
2594 workspace.dock_pane().clone(),
2595 workspace.last_active_center_pane.clone(),
2596 )
2597 });
2598
2599 serialized_workspace
2600 .dock_pane
2601 .deserialize_to(
2602 &project,
2603 &dock_pane_handle,
2604 serialized_workspace.id,
2605 &workspace,
2606 &mut cx,
2607 )
2608 .await;
2609
2610 // Traverse the splits tree and add to things
2611 let center_group = serialized_workspace
2612 .center_group
2613 .deserialize(&project, serialized_workspace.id, &workspace, &mut cx)
2614 .await;
2615
2616 // Remove old panes from workspace panes list
2617 workspace.update(&mut cx, |workspace, cx| {
2618 if let Some((center_group, active_pane)) = center_group {
2619 workspace.remove_panes(workspace.center.root.clone(), cx);
2620
2621 // Swap workspace center group
2622 workspace.center = PaneGroup::with_root(center_group);
2623
2624 // Change the focus to the workspace first so that we retrigger focus in on the pane.
2625 cx.focus_self();
2626
2627 if let Some(active_pane) = active_pane {
2628 cx.focus(active_pane);
2629 } else {
2630 cx.focus(workspace.panes.last().unwrap().clone());
2631 }
2632 } else {
2633 let old_center_handle = old_center_pane.and_then(|weak| weak.upgrade(cx));
2634 if let Some(old_center_handle) = old_center_handle {
2635 cx.focus(old_center_handle)
2636 } else {
2637 cx.focus_self()
2638 }
2639 }
2640
2641 if workspace.left_sidebar().read(cx).is_open()
2642 != serialized_workspace.left_sidebar_open
2643 {
2644 workspace.toggle_sidebar(SidebarSide::Left, cx);
2645 }
2646
2647 // Note that without after_window, the focus_self() and
2648 // the focus the dock generates start generating alternating
2649 // focus due to the deferred execution each triggering each other
2650 cx.after_window_update(move |workspace, cx| {
2651 Dock::set_dock_position(
2652 workspace,
2653 serialized_workspace.dock_position,
2654 true,
2655 cx,
2656 );
2657 });
2658
2659 cx.notify();
2660 });
2661
2662 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
2663 workspace.read_with(&cx, |workspace, cx| workspace.serialize_workspace(cx))
2664 }
2665 })
2666 .detach();
2667 }
2668
2669 #[cfg(any(test, feature = "test-support"))]
2670 pub fn test_new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
2671 Self::new(None, 0, project, |_, _| None, || &[], cx)
2672 }
2673}
2674
2675fn notify_if_database_failed(workspace: &ViewHandle<Workspace>, cx: &mut AsyncAppContext) {
2676 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
2677 workspace.update(cx, |workspace, cx| {
2678 workspace.show_notification_once(0, cx, |cx| {
2679 cx.add_view(|_| {
2680 MessageNotification::new(
2681 indoc::indoc! {"
2682 Failed to load any database file :(
2683 "},
2684 OsOpen("https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml".to_string()),
2685 "Click to let us know about this error"
2686 )
2687 })
2688 });
2689 });
2690 } else {
2691 let backup_path = (*db::BACKUP_DB_PATH).read();
2692 if let Some(backup_path) = &*backup_path {
2693 workspace.update(cx, |workspace, cx| {
2694 workspace.show_notification_once(0, cx, |cx| {
2695 cx.add_view(|_| {
2696 let backup_path = backup_path.to_string_lossy();
2697 MessageNotification::new(
2698 format!(
2699 indoc::indoc! {"
2700 Database file was corrupted :(
2701 Old database backed up to:
2702 {}
2703 "},
2704 backup_path
2705 ),
2706 OsOpen(backup_path.to_string()),
2707 "Click to show old database in finder",
2708 )
2709 })
2710 });
2711 });
2712 }
2713 }
2714}
2715
2716impl Entity for Workspace {
2717 type Event = Event;
2718}
2719
2720impl View for Workspace {
2721 fn ui_name() -> &'static str {
2722 "Workspace"
2723 }
2724
2725 fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
2726 let theme = cx.global::<Settings>().theme.clone();
2727 Stack::new()
2728 .with_child(
2729 Flex::column()
2730 .with_child(self.render_titlebar(&theme, cx))
2731 .with_child(
2732 Stack::new()
2733 .with_child({
2734 let project = self.project.clone();
2735 Flex::row()
2736 .with_children(
2737 if self.left_sidebar.read(cx).active_item().is_some() {
2738 Some(
2739 ChildView::new(&self.left_sidebar, cx)
2740 .constrained()
2741 .dynamically(|constraint, cx| {
2742 SizeConstraint::new(
2743 Vector2F::new(20., constraint.min.y()),
2744 Vector2F::new(
2745 cx.window_size.x() * 0.8,
2746 constraint.max.y(),
2747 ),
2748 )
2749 })
2750 .boxed(),
2751 )
2752 } else {
2753 None
2754 },
2755 )
2756 .with_child(
2757 FlexItem::new(
2758 Flex::column()
2759 .with_child(
2760 FlexItem::new(self.center.render(
2761 &project,
2762 &theme,
2763 &self.follower_states_by_leader,
2764 self.active_call(),
2765 self.active_pane(),
2766 cx,
2767 ))
2768 .flex(1., true)
2769 .boxed(),
2770 )
2771 .with_children(self.dock.render(
2772 &theme,
2773 DockAnchor::Bottom,
2774 cx,
2775 ))
2776 .boxed(),
2777 )
2778 .flex(1., true)
2779 .boxed(),
2780 )
2781 .with_children(self.dock.render(&theme, DockAnchor::Right, cx))
2782 .with_children(
2783 if self.right_sidebar.read(cx).active_item().is_some() {
2784 Some(
2785 ChildView::new(&self.right_sidebar, cx)
2786 .constrained()
2787 .dynamically(|constraint, cx| {
2788 SizeConstraint::new(
2789 Vector2F::new(20., constraint.min.y()),
2790 Vector2F::new(
2791 cx.window_size.x() * 0.8,
2792 constraint.max.y(),
2793 ),
2794 )
2795 })
2796 .boxed(),
2797 )
2798 } else {
2799 None
2800 },
2801 )
2802 .boxed()
2803 })
2804 .with_child(
2805 Overlay::new(
2806 Stack::new()
2807 .with_children(self.dock.render(
2808 &theme,
2809 DockAnchor::Expanded,
2810 cx,
2811 ))
2812 .with_children(self.modal.as_ref().map(|modal| {
2813 ChildView::new(modal, cx)
2814 .contained()
2815 .with_style(theme.workspace.modal)
2816 .aligned()
2817 .top()
2818 .boxed()
2819 }))
2820 .with_children(
2821 self.render_notifications(&theme.workspace, cx),
2822 )
2823 .boxed(),
2824 )
2825 .boxed(),
2826 )
2827 .flex(1.0, true)
2828 .boxed(),
2829 )
2830 .with_child(ChildView::new(&self.status_bar, cx).boxed())
2831 .contained()
2832 .with_background_color(theme.workspace.background)
2833 .boxed(),
2834 )
2835 .with_children(DragAndDrop::render(cx))
2836 .with_children(self.render_disconnected_overlay(cx))
2837 .named("workspace")
2838 }
2839
2840 fn focus_in(&mut self, view: AnyViewHandle, cx: &mut ViewContext<Self>) {
2841 if cx.is_self_focused() {
2842 cx.focus(&self.active_pane);
2843 } else {
2844 for pane in self.panes() {
2845 let view = view.clone();
2846 if pane.update(cx, |_, cx| view.id() == cx.view_id() || cx.is_child(view)) {
2847 self.handle_pane_focused(pane.clone(), cx);
2848 break;
2849 }
2850 }
2851 }
2852 }
2853
2854 fn keymap_context(&self, _: &AppContext) -> KeymapContext {
2855 Self::default_keymap_context()
2856 }
2857}
2858
2859impl ViewId {
2860 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
2861 Ok(Self {
2862 creator: message
2863 .creator
2864 .ok_or_else(|| anyhow!("creator is missing"))?,
2865 id: message.id,
2866 })
2867 }
2868
2869 pub(crate) fn to_proto(&self) -> proto::ViewId {
2870 proto::ViewId {
2871 creator: Some(self.creator),
2872 id: self.id,
2873 }
2874 }
2875}
2876
2877pub trait WorkspaceHandle {
2878 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
2879}
2880
2881impl WorkspaceHandle for ViewHandle<Workspace> {
2882 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
2883 self.read(cx)
2884 .worktrees(cx)
2885 .flat_map(|worktree| {
2886 let worktree_id = worktree.read(cx).id();
2887 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
2888 worktree_id,
2889 path: f.path.clone(),
2890 })
2891 })
2892 .collect::<Vec<_>>()
2893 }
2894}
2895
2896impl std::fmt::Debug for OpenPaths {
2897 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2898 f.debug_struct("OpenPaths")
2899 .field("paths", &self.paths)
2900 .finish()
2901 }
2902}
2903
2904pub struct WorkspaceCreated(WeakViewHandle<Workspace>);
2905
2906pub fn activate_workspace_for_project(
2907 cx: &mut MutableAppContext,
2908 predicate: impl Fn(&mut Project, &mut ModelContext<Project>) -> bool,
2909) -> Option<ViewHandle<Workspace>> {
2910 for window_id in cx.window_ids().collect::<Vec<_>>() {
2911 if let Some(workspace_handle) = cx.root_view::<Workspace>(window_id) {
2912 let project = workspace_handle.read(cx).project.clone();
2913 if project.update(cx, &predicate) {
2914 cx.activate_window(window_id);
2915 return Some(workspace_handle);
2916 }
2917 }
2918 }
2919 None
2920}
2921
2922pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
2923 DB.last_workspace().await.log_err().flatten()
2924}
2925
2926#[allow(clippy::type_complexity)]
2927pub fn open_paths(
2928 abs_paths: &[PathBuf],
2929 app_state: &Arc<AppState>,
2930 requesting_window_id: Option<usize>,
2931 cx: &mut MutableAppContext,
2932) -> Task<(
2933 ViewHandle<Workspace>,
2934 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
2935)> {
2936 log::info!("open paths {:?}", abs_paths);
2937
2938 // Open paths in existing workspace if possible
2939 let existing =
2940 activate_workspace_for_project(cx, |project, cx| project.contains_paths(abs_paths, cx));
2941
2942 let app_state = app_state.clone();
2943 let abs_paths = abs_paths.to_vec();
2944 cx.spawn(|mut cx| async move {
2945 if let Some(existing) = existing {
2946 (
2947 existing.clone(),
2948 existing
2949 .update(&mut cx, |workspace, cx| {
2950 workspace.open_paths(abs_paths, true, cx)
2951 })
2952 .await,
2953 )
2954 } else {
2955 let contains_directory =
2956 futures::future::join_all(abs_paths.iter().map(|path| app_state.fs.is_file(path)))
2957 .await
2958 .contains(&false);
2959
2960 cx.update(|cx| {
2961 let task =
2962 Workspace::new_local(abs_paths, app_state.clone(), requesting_window_id, cx);
2963
2964 cx.spawn(|mut cx| async move {
2965 let (workspace, items) = task.await;
2966
2967 workspace.update(&mut cx, |workspace, cx| {
2968 if contains_directory {
2969 workspace.toggle_sidebar(SidebarSide::Left, cx);
2970 }
2971 });
2972
2973 (workspace, items)
2974 })
2975 })
2976 .await
2977 }
2978 })
2979}
2980
2981pub fn open_new(
2982 app_state: &Arc<AppState>,
2983 cx: &mut MutableAppContext,
2984 init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static,
2985) -> Task<()> {
2986 let task = Workspace::new_local(Vec::new(), app_state.clone(), None, cx);
2987 cx.spawn(|mut cx| async move {
2988 let (workspace, opened_paths) = task.await;
2989
2990 workspace.update(&mut cx, |workspace, cx| {
2991 if opened_paths.is_empty() {
2992 init(workspace, cx)
2993 }
2994 })
2995 })
2996}
2997
2998fn parse_pixel_position_env_var(value: &str) -> Option<Vector2F> {
2999 let mut parts = value.split(',');
3000 let width: usize = parts.next()?.parse().ok()?;
3001 let height: usize = parts.next()?.parse().ok()?;
3002 Some(vec2f(width as f32, height as f32))
3003}
3004
3005#[cfg(test)]
3006mod tests {
3007 use std::{cell::RefCell, rc::Rc};
3008
3009 use crate::item::test::{TestItem, TestItemEvent, TestProjectItem};
3010
3011 use super::*;
3012 use fs::FakeFs;
3013 use gpui::{executor::Deterministic, TestAppContext};
3014 use project::{Project, ProjectEntryId};
3015 use serde_json::json;
3016
3017 #[gpui::test]
3018 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
3019 cx.foreground().forbid_parking();
3020 Settings::test_async(cx);
3021
3022 let fs = FakeFs::new(cx.background());
3023 let project = Project::test(fs, [], cx).await;
3024 let (_, workspace) = cx.add_window(|cx| {
3025 Workspace::new(
3026 Default::default(),
3027 0,
3028 project.clone(),
3029 |_, _| None,
3030 || &[],
3031 cx,
3032 )
3033 });
3034
3035 // Adding an item with no ambiguity renders the tab without detail.
3036 let item1 = cx.add_view(&workspace, |_| {
3037 let mut item = TestItem::new();
3038 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
3039 item
3040 });
3041 workspace.update(cx, |workspace, cx| {
3042 workspace.add_item(Box::new(item1.clone()), cx);
3043 });
3044 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), None));
3045
3046 // Adding an item that creates ambiguity increases the level of detail on
3047 // both tabs.
3048 let item2 = cx.add_view(&workspace, |_| {
3049 let mut item = TestItem::new();
3050 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
3051 item
3052 });
3053 workspace.update(cx, |workspace, cx| {
3054 workspace.add_item(Box::new(item2.clone()), cx);
3055 });
3056 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
3057 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
3058
3059 // Adding an item that creates ambiguity increases the level of detail only
3060 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
3061 // we stop at the highest detail available.
3062 let item3 = cx.add_view(&workspace, |_| {
3063 let mut item = TestItem::new();
3064 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
3065 item
3066 });
3067 workspace.update(cx, |workspace, cx| {
3068 workspace.add_item(Box::new(item3.clone()), cx);
3069 });
3070 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
3071 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
3072 item3.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
3073 }
3074
3075 #[gpui::test]
3076 async fn test_tracking_active_path(cx: &mut TestAppContext) {
3077 cx.foreground().forbid_parking();
3078 Settings::test_async(cx);
3079 let fs = FakeFs::new(cx.background());
3080 fs.insert_tree(
3081 "/root1",
3082 json!({
3083 "one.txt": "",
3084 "two.txt": "",
3085 }),
3086 )
3087 .await;
3088 fs.insert_tree(
3089 "/root2",
3090 json!({
3091 "three.txt": "",
3092 }),
3093 )
3094 .await;
3095
3096 let project = Project::test(fs, ["root1".as_ref()], cx).await;
3097 let (window_id, workspace) = cx.add_window(|cx| {
3098 Workspace::new(
3099 Default::default(),
3100 0,
3101 project.clone(),
3102 |_, _| None,
3103 || &[],
3104 cx,
3105 )
3106 });
3107 let worktree_id = project.read_with(cx, |project, cx| {
3108 project.worktrees(cx).next().unwrap().read(cx).id()
3109 });
3110
3111 let item1 = cx.add_view(&workspace, |cx| {
3112 TestItem::new().with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
3113 });
3114 let item2 = cx.add_view(&workspace, |cx| {
3115 TestItem::new().with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
3116 });
3117
3118 // Add an item to an empty pane
3119 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx));
3120 project.read_with(cx, |project, cx| {
3121 assert_eq!(
3122 project.active_entry(),
3123 project
3124 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
3125 .map(|e| e.id)
3126 );
3127 });
3128 assert_eq!(
3129 cx.current_window_title(window_id).as_deref(),
3130 Some("one.txt — root1")
3131 );
3132
3133 // Add a second item to a non-empty pane
3134 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
3135 assert_eq!(
3136 cx.current_window_title(window_id).as_deref(),
3137 Some("two.txt — root1")
3138 );
3139 project.read_with(cx, |project, cx| {
3140 assert_eq!(
3141 project.active_entry(),
3142 project
3143 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
3144 .map(|e| e.id)
3145 );
3146 });
3147
3148 // Close the active item
3149 workspace
3150 .update(cx, |workspace, cx| {
3151 Pane::close_active_item(workspace, &Default::default(), cx).unwrap()
3152 })
3153 .await
3154 .unwrap();
3155 assert_eq!(
3156 cx.current_window_title(window_id).as_deref(),
3157 Some("one.txt — root1")
3158 );
3159 project.read_with(cx, |project, cx| {
3160 assert_eq!(
3161 project.active_entry(),
3162 project
3163 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
3164 .map(|e| e.id)
3165 );
3166 });
3167
3168 // Add a project folder
3169 project
3170 .update(cx, |project, cx| {
3171 project.find_or_create_local_worktree("/root2", true, cx)
3172 })
3173 .await
3174 .unwrap();
3175 assert_eq!(
3176 cx.current_window_title(window_id).as_deref(),
3177 Some("one.txt — root1, root2")
3178 );
3179
3180 // Remove a project folder
3181 project
3182 .update(cx, |project, cx| project.remove_worktree(worktree_id, cx))
3183 .await;
3184 assert_eq!(
3185 cx.current_window_title(window_id).as_deref(),
3186 Some("one.txt — root2")
3187 );
3188 }
3189
3190 #[gpui::test]
3191 async fn test_close_window(cx: &mut TestAppContext) {
3192 cx.foreground().forbid_parking();
3193 Settings::test_async(cx);
3194 let fs = FakeFs::new(cx.background());
3195 fs.insert_tree("/root", json!({ "one": "" })).await;
3196
3197 let project = Project::test(fs, ["root".as_ref()], cx).await;
3198 let (window_id, workspace) = cx.add_window(|cx| {
3199 Workspace::new(
3200 Default::default(),
3201 0,
3202 project.clone(),
3203 |_, _| None,
3204 || &[],
3205 cx,
3206 )
3207 });
3208
3209 // When there are no dirty items, there's nothing to do.
3210 let item1 = cx.add_view(&workspace, |_| TestItem::new());
3211 workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
3212 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
3213 assert!(task.await.unwrap());
3214
3215 // When there are dirty untitled items, prompt to save each one. If the user
3216 // cancels any prompt, then abort.
3217 let item2 = cx.add_view(&workspace, |_| TestItem::new().with_dirty(true));
3218 let item3 = cx.add_view(&workspace, |cx| {
3219 TestItem::new()
3220 .with_dirty(true)
3221 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3222 });
3223 workspace.update(cx, |w, cx| {
3224 w.add_item(Box::new(item2.clone()), cx);
3225 w.add_item(Box::new(item3.clone()), cx);
3226 });
3227 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
3228 cx.foreground().run_until_parked();
3229 cx.simulate_prompt_answer(window_id, 2 /* cancel */);
3230 cx.foreground().run_until_parked();
3231 assert!(!cx.has_pending_prompt(window_id));
3232 assert!(!task.await.unwrap());
3233 }
3234
3235 #[gpui::test]
3236 async fn test_close_pane_items(cx: &mut TestAppContext) {
3237 cx.foreground().forbid_parking();
3238 Settings::test_async(cx);
3239 let fs = FakeFs::new(cx.background());
3240
3241 let project = Project::test(fs, None, cx).await;
3242 let (window_id, workspace) = cx.add_window(|cx| {
3243 Workspace::new(Default::default(), 0, project, |_, _| None, || &[], cx)
3244 });
3245
3246 let item1 = cx.add_view(&workspace, |cx| {
3247 TestItem::new()
3248 .with_dirty(true)
3249 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3250 });
3251 let item2 = cx.add_view(&workspace, |cx| {
3252 TestItem::new()
3253 .with_dirty(true)
3254 .with_conflict(true)
3255 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
3256 });
3257 let item3 = cx.add_view(&workspace, |cx| {
3258 TestItem::new()
3259 .with_dirty(true)
3260 .with_conflict(true)
3261 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
3262 });
3263 let item4 = cx.add_view(&workspace, |cx| {
3264 TestItem::new()
3265 .with_dirty(true)
3266 .with_project_items(&[TestProjectItem::new_untitled(cx)])
3267 });
3268 let pane = workspace.update(cx, |workspace, cx| {
3269 workspace.add_item(Box::new(item1.clone()), cx);
3270 workspace.add_item(Box::new(item2.clone()), cx);
3271 workspace.add_item(Box::new(item3.clone()), cx);
3272 workspace.add_item(Box::new(item4.clone()), cx);
3273 workspace.active_pane().clone()
3274 });
3275
3276 let close_items = workspace.update(cx, |workspace, cx| {
3277 pane.update(cx, |pane, cx| {
3278 pane.activate_item(1, true, true, cx);
3279 assert_eq!(pane.active_item().unwrap().id(), item2.id());
3280 });
3281
3282 let item1_id = item1.id();
3283 let item3_id = item3.id();
3284 let item4_id = item4.id();
3285 Pane::close_items(workspace, pane.clone(), cx, move |id| {
3286 [item1_id, item3_id, item4_id].contains(&id)
3287 })
3288 });
3289 cx.foreground().run_until_parked();
3290
3291 // There's a prompt to save item 1.
3292 pane.read_with(cx, |pane, _| {
3293 assert_eq!(pane.items_len(), 4);
3294 assert_eq!(pane.active_item().unwrap().id(), item1.id());
3295 });
3296 assert!(cx.has_pending_prompt(window_id));
3297
3298 // Confirm saving item 1.
3299 cx.simulate_prompt_answer(window_id, 0);
3300 cx.foreground().run_until_parked();
3301
3302 // Item 1 is saved. There's a prompt to save item 3.
3303 pane.read_with(cx, |pane, cx| {
3304 assert_eq!(item1.read(cx).save_count, 1);
3305 assert_eq!(item1.read(cx).save_as_count, 0);
3306 assert_eq!(item1.read(cx).reload_count, 0);
3307 assert_eq!(pane.items_len(), 3);
3308 assert_eq!(pane.active_item().unwrap().id(), item3.id());
3309 });
3310 assert!(cx.has_pending_prompt(window_id));
3311
3312 // Cancel saving item 3.
3313 cx.simulate_prompt_answer(window_id, 1);
3314 cx.foreground().run_until_parked();
3315
3316 // Item 3 is reloaded. There's a prompt to save item 4.
3317 pane.read_with(cx, |pane, cx| {
3318 assert_eq!(item3.read(cx).save_count, 0);
3319 assert_eq!(item3.read(cx).save_as_count, 0);
3320 assert_eq!(item3.read(cx).reload_count, 1);
3321 assert_eq!(pane.items_len(), 2);
3322 assert_eq!(pane.active_item().unwrap().id(), item4.id());
3323 });
3324 assert!(cx.has_pending_prompt(window_id));
3325
3326 // Confirm saving item 4.
3327 cx.simulate_prompt_answer(window_id, 0);
3328 cx.foreground().run_until_parked();
3329
3330 // There's a prompt for a path for item 4.
3331 cx.simulate_new_path_selection(|_| Some(Default::default()));
3332 close_items.await.unwrap();
3333
3334 // The requested items are closed.
3335 pane.read_with(cx, |pane, cx| {
3336 assert_eq!(item4.read(cx).save_count, 0);
3337 assert_eq!(item4.read(cx).save_as_count, 1);
3338 assert_eq!(item4.read(cx).reload_count, 0);
3339 assert_eq!(pane.items_len(), 1);
3340 assert_eq!(pane.active_item().unwrap().id(), item2.id());
3341 });
3342 }
3343
3344 #[gpui::test]
3345 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
3346 cx.foreground().forbid_parking();
3347 Settings::test_async(cx);
3348 let fs = FakeFs::new(cx.background());
3349
3350 let project = Project::test(fs, [], cx).await;
3351 let (window_id, workspace) = cx.add_window(|cx| {
3352 Workspace::new(Default::default(), 0, project, |_, _| None, || &[], cx)
3353 });
3354
3355 // Create several workspace items with single project entries, and two
3356 // workspace items with multiple project entries.
3357 let single_entry_items = (0..=4)
3358 .map(|project_entry_id| {
3359 cx.add_view(&workspace, |cx| {
3360 TestItem::new()
3361 .with_dirty(true)
3362 .with_project_items(&[TestProjectItem::new(
3363 project_entry_id,
3364 &format!("{project_entry_id}.txt"),
3365 cx,
3366 )])
3367 })
3368 })
3369 .collect::<Vec<_>>();
3370 let item_2_3 = cx.add_view(&workspace, |cx| {
3371 TestItem::new()
3372 .with_dirty(true)
3373 .with_singleton(false)
3374 .with_project_items(&[
3375 single_entry_items[2].read(cx).project_items[0].clone(),
3376 single_entry_items[3].read(cx).project_items[0].clone(),
3377 ])
3378 });
3379 let item_3_4 = cx.add_view(&workspace, |cx| {
3380 TestItem::new()
3381 .with_dirty(true)
3382 .with_singleton(false)
3383 .with_project_items(&[
3384 single_entry_items[3].read(cx).project_items[0].clone(),
3385 single_entry_items[4].read(cx).project_items[0].clone(),
3386 ])
3387 });
3388
3389 // Create two panes that contain the following project entries:
3390 // left pane:
3391 // multi-entry items: (2, 3)
3392 // single-entry items: 0, 1, 2, 3, 4
3393 // right pane:
3394 // single-entry items: 1
3395 // multi-entry items: (3, 4)
3396 let left_pane = workspace.update(cx, |workspace, cx| {
3397 let left_pane = workspace.active_pane().clone();
3398 workspace.add_item(Box::new(item_2_3.clone()), cx);
3399 for item in single_entry_items {
3400 workspace.add_item(Box::new(item), cx);
3401 }
3402 left_pane.update(cx, |pane, cx| {
3403 pane.activate_item(2, true, true, cx);
3404 });
3405
3406 workspace
3407 .split_pane(left_pane.clone(), SplitDirection::Right, cx)
3408 .unwrap();
3409
3410 left_pane
3411 });
3412
3413 //Need to cause an effect flush in order to respect new focus
3414 workspace.update(cx, |workspace, cx| {
3415 workspace.add_item(Box::new(item_3_4.clone()), cx);
3416 cx.focus(left_pane.clone());
3417 });
3418
3419 // When closing all of the items in the left pane, we should be prompted twice:
3420 // once for project entry 0, and once for project entry 2. After those two
3421 // prompts, the task should complete.
3422
3423 let close = workspace.update(cx, |workspace, cx| {
3424 Pane::close_items(workspace, left_pane.clone(), cx, |_| true)
3425 });
3426
3427 cx.foreground().run_until_parked();
3428 left_pane.read_with(cx, |pane, cx| {
3429 assert_eq!(
3430 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
3431 &[ProjectEntryId::from_proto(0)]
3432 );
3433 });
3434 cx.simulate_prompt_answer(window_id, 0);
3435
3436 cx.foreground().run_until_parked();
3437 left_pane.read_with(cx, |pane, cx| {
3438 assert_eq!(
3439 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
3440 &[ProjectEntryId::from_proto(2)]
3441 );
3442 });
3443 cx.simulate_prompt_answer(window_id, 0);
3444
3445 cx.foreground().run_until_parked();
3446 close.await.unwrap();
3447 left_pane.read_with(cx, |pane, _| {
3448 assert_eq!(pane.items_len(), 0);
3449 });
3450 }
3451
3452 #[gpui::test]
3453 async fn test_autosave(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
3454 deterministic.forbid_parking();
3455
3456 Settings::test_async(cx);
3457 let fs = FakeFs::new(cx.background());
3458
3459 let project = Project::test(fs, [], cx).await;
3460 let (window_id, workspace) = cx.add_window(|cx| {
3461 Workspace::new(Default::default(), 0, project, |_, _| None, || &[], cx)
3462 });
3463
3464 let item = cx.add_view(&workspace, |cx| {
3465 TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3466 });
3467 let item_id = item.id();
3468 workspace.update(cx, |workspace, cx| {
3469 workspace.add_item(Box::new(item.clone()), cx);
3470 });
3471
3472 // Autosave on window change.
3473 item.update(cx, |item, cx| {
3474 cx.update_global(|settings: &mut Settings, _| {
3475 settings.autosave = Autosave::OnWindowChange;
3476 });
3477 item.is_dirty = true;
3478 });
3479
3480 // Deactivating the window saves the file.
3481 cx.simulate_window_activation(None);
3482 deterministic.run_until_parked();
3483 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
3484
3485 // Autosave on focus change.
3486 item.update(cx, |item, cx| {
3487 cx.focus_self();
3488 cx.update_global(|settings: &mut Settings, _| {
3489 settings.autosave = Autosave::OnFocusChange;
3490 });
3491 item.is_dirty = true;
3492 });
3493
3494 // Blurring the item saves the file.
3495 item.update(cx, |_, cx| cx.blur());
3496 deterministic.run_until_parked();
3497 item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
3498
3499 // Deactivating the window still saves the file.
3500 cx.simulate_window_activation(Some(window_id));
3501 item.update(cx, |item, cx| {
3502 cx.focus_self();
3503 item.is_dirty = true;
3504 });
3505 cx.simulate_window_activation(None);
3506
3507 deterministic.run_until_parked();
3508 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
3509
3510 // Autosave after delay.
3511 item.update(cx, |item, cx| {
3512 cx.update_global(|settings: &mut Settings, _| {
3513 settings.autosave = Autosave::AfterDelay { milliseconds: 500 };
3514 });
3515 item.is_dirty = true;
3516 cx.emit(TestItemEvent::Edit);
3517 });
3518
3519 // Delay hasn't fully expired, so the file is still dirty and unsaved.
3520 deterministic.advance_clock(Duration::from_millis(250));
3521 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
3522
3523 // After delay expires, the file is saved.
3524 deterministic.advance_clock(Duration::from_millis(250));
3525 item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
3526
3527 // Autosave on focus change, ensuring closing the tab counts as such.
3528 item.update(cx, |item, cx| {
3529 cx.update_global(|settings: &mut Settings, _| {
3530 settings.autosave = Autosave::OnFocusChange;
3531 });
3532 item.is_dirty = true;
3533 });
3534
3535 workspace
3536 .update(cx, |workspace, cx| {
3537 let pane = workspace.active_pane().clone();
3538 Pane::close_items(workspace, pane, cx, move |id| id == item_id)
3539 })
3540 .await
3541 .unwrap();
3542 assert!(!cx.has_pending_prompt(window_id));
3543 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
3544
3545 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
3546 workspace.update(cx, |workspace, cx| {
3547 workspace.add_item(Box::new(item.clone()), cx);
3548 });
3549 item.update(cx, |item, cx| {
3550 item.project_items[0].update(cx, |item, _| {
3551 item.entry_id = None;
3552 });
3553 item.is_dirty = true;
3554 cx.blur();
3555 });
3556 deterministic.run_until_parked();
3557 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
3558
3559 // Ensure autosave is prevented for deleted files also when closing the buffer.
3560 let _close_items = workspace.update(cx, |workspace, cx| {
3561 let pane = workspace.active_pane().clone();
3562 Pane::close_items(workspace, pane, cx, move |id| id == item_id)
3563 });
3564 deterministic.run_until_parked();
3565 assert!(cx.has_pending_prompt(window_id));
3566 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
3567 }
3568
3569 #[gpui::test]
3570 async fn test_pane_navigation(
3571 deterministic: Arc<Deterministic>,
3572 cx: &mut gpui::TestAppContext,
3573 ) {
3574 deterministic.forbid_parking();
3575 Settings::test_async(cx);
3576 let fs = FakeFs::new(cx.background());
3577
3578 let project = Project::test(fs, [], cx).await;
3579 let (_, workspace) = cx.add_window(|cx| {
3580 Workspace::new(Default::default(), 0, project, |_, _| None, || &[], cx)
3581 });
3582
3583 let item = cx.add_view(&workspace, |cx| {
3584 TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3585 });
3586 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3587 let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone());
3588 let toolbar_notify_count = Rc::new(RefCell::new(0));
3589
3590 workspace.update(cx, |workspace, cx| {
3591 workspace.add_item(Box::new(item.clone()), cx);
3592 let toolbar_notification_count = toolbar_notify_count.clone();
3593 cx.observe(&toolbar, move |_, _, _| {
3594 *toolbar_notification_count.borrow_mut() += 1
3595 })
3596 .detach();
3597 });
3598
3599 pane.read_with(cx, |pane, _| {
3600 assert!(!pane.can_navigate_backward());
3601 assert!(!pane.can_navigate_forward());
3602 });
3603
3604 item.update(cx, |item, cx| {
3605 item.set_state("one".to_string(), cx);
3606 });
3607
3608 // Toolbar must be notified to re-render the navigation buttons
3609 assert_eq!(*toolbar_notify_count.borrow(), 1);
3610
3611 pane.read_with(cx, |pane, _| {
3612 assert!(pane.can_navigate_backward());
3613 assert!(!pane.can_navigate_forward());
3614 });
3615
3616 workspace
3617 .update(cx, |workspace, cx| {
3618 Pane::go_back(workspace, Some(pane.clone()), cx)
3619 })
3620 .await;
3621
3622 assert_eq!(*toolbar_notify_count.borrow(), 3);
3623 pane.read_with(cx, |pane, _| {
3624 assert!(!pane.can_navigate_backward());
3625 assert!(pane.can_navigate_forward());
3626 });
3627 }
3628}