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