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