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