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