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