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