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