1pub mod dock;
2pub mod item;
3pub mod notifications;
4pub mod pane;
5pub mod pane_group;
6mod persistence;
7pub mod searchable;
8pub mod shared_screen;
9mod status_bar;
10mod toolbar;
11mod workspace_settings;
12
13use anyhow::{anyhow, Context, Result};
14use call::ActiveCall;
15use channel::ChannelStore;
16use client::{
17 proto::{self, PeerId},
18 Client, TypedEnvelope, UserStore,
19};
20use collections::{hash_map, HashMap, HashSet};
21use drag_and_drop::DragAndDrop;
22use futures::{
23 channel::{mpsc, oneshot},
24 future::try_join_all,
25 FutureExt, StreamExt,
26};
27use gpui::{
28 actions,
29 elements::*,
30 geometry::{
31 rect::RectF,
32 vector::{vec2f, Vector2F},
33 },
34 impl_actions,
35 platform::{
36 CursorStyle, ModifiersChangedEvent, MouseButton, PathPromptOptions, Platform, PromptLevel,
37 WindowBounds, WindowOptions,
38 },
39 AnyModelHandle, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity,
40 ModelContext, ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle,
41 WeakViewHandle, WindowContext, WindowHandle,
42};
43use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem};
44use itertools::Itertools;
45use language::{LanguageRegistry, Rope};
46use std::{
47 any::TypeId,
48 borrow::Cow,
49 cmp, env,
50 future::Future,
51 path::{Path, PathBuf},
52 rc::Rc,
53 str,
54 sync::{atomic::AtomicUsize, Arc},
55 time::Duration,
56};
57
58use crate::{
59 notifications::{simple_message_notification::MessageNotification, NotificationTracker},
60 persistence::model::{
61 DockData, DockStructure, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
62 },
63};
64use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle};
65use lazy_static::lazy_static;
66use notifications::{NotificationHandle, NotifyResultExt};
67pub use pane::*;
68pub use pane_group::*;
69use persistence::{model::SerializedItem, DB};
70pub use persistence::{
71 model::{ItemId, WorkspaceLocation},
72 WorkspaceDb, DB as WORKSPACE_DB,
73};
74use postage::prelude::Stream;
75use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
76use serde::Deserialize;
77use shared_screen::SharedScreen;
78use status_bar::StatusBar;
79pub use status_bar::StatusItemView;
80use theme::{Theme, ThemeSettings};
81pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
82use util::{async_iife, ResultExt};
83pub use workspace_settings::{AutosaveSetting, GitGutterSetting, WorkspaceSettings};
84
85lazy_static! {
86 static ref ZED_WINDOW_SIZE: Option<Vector2F> = env::var("ZED_WINDOW_SIZE")
87 .ok()
88 .as_deref()
89 .and_then(parse_pixel_position_env_var);
90 static ref ZED_WINDOW_POSITION: Option<Vector2F> = env::var("ZED_WINDOW_POSITION")
91 .ok()
92 .as_deref()
93 .and_then(parse_pixel_position_env_var);
94}
95
96pub trait Modal: View {
97 fn has_focus(&self) -> bool;
98 fn dismiss_on_event(event: &Self::Event) -> bool;
99}
100
101trait ModalHandle {
102 fn as_any(&self) -> &AnyViewHandle;
103 fn has_focus(&self, cx: &WindowContext) -> bool;
104}
105
106impl<T: Modal> ModalHandle for ViewHandle<T> {
107 fn as_any(&self) -> &AnyViewHandle {
108 self
109 }
110
111 fn has_focus(&self, cx: &WindowContext) -> bool {
112 self.read(cx).has_focus()
113 }
114}
115
116#[derive(Clone, PartialEq)]
117pub struct RemoveWorktreeFromProject(pub WorktreeId);
118
119actions!(
120 workspace,
121 [
122 Open,
123 NewFile,
124 NewWindow,
125 CloseWindow,
126 CloseInactiveTabsAndPanes,
127 AddFolderToProject,
128 Unfollow,
129 SaveAs,
130 ReloadActiveItem,
131 ActivatePreviousPane,
132 ActivateNextPane,
133 FollowNextCollaborator,
134 NewTerminal,
135 NewCenterTerminal,
136 ToggleTerminalFocus,
137 NewSearch,
138 Feedback,
139 Restart,
140 Welcome,
141 ToggleZoom,
142 ToggleLeftDock,
143 ToggleRightDock,
144 ToggleBottomDock,
145 CloseAllDocks,
146 ]
147);
148
149#[derive(Clone, PartialEq)]
150pub struct OpenPaths {
151 pub paths: Vec<PathBuf>,
152}
153
154#[derive(Clone, Deserialize, PartialEq)]
155pub struct ActivatePane(pub usize);
156
157#[derive(Clone, Deserialize, PartialEq)]
158pub struct ActivatePaneInDirection(pub SplitDirection);
159
160#[derive(Clone, Deserialize, PartialEq)]
161pub struct SwapPaneInDirection(pub SplitDirection);
162
163#[derive(Clone, Deserialize, PartialEq)]
164pub struct NewFileInDirection(pub SplitDirection);
165
166#[derive(Clone, PartialEq, Debug, Deserialize)]
167#[serde(rename_all = "camelCase")]
168pub struct SaveAll {
169 pub save_intent: Option<SaveIntent>,
170}
171
172#[derive(Clone, PartialEq, Debug, Deserialize)]
173#[serde(rename_all = "camelCase")]
174pub struct Save {
175 pub save_intent: Option<SaveIntent>,
176}
177
178#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
179#[serde(rename_all = "camelCase")]
180pub struct CloseAllItemsAndPanes {
181 pub save_intent: Option<SaveIntent>,
182}
183
184#[derive(Deserialize)]
185pub struct Toast {
186 id: usize,
187 msg: Cow<'static, str>,
188 #[serde(skip)]
189 on_click: Option<(Cow<'static, str>, Arc<dyn Fn(&mut WindowContext)>)>,
190}
191
192impl Toast {
193 pub fn new<I: Into<Cow<'static, str>>>(id: usize, msg: I) -> Self {
194 Toast {
195 id,
196 msg: msg.into(),
197 on_click: None,
198 }
199 }
200
201 pub fn on_click<F, M>(mut self, message: M, on_click: F) -> Self
202 where
203 M: Into<Cow<'static, str>>,
204 F: Fn(&mut WindowContext) + 'static,
205 {
206 self.on_click = Some((message.into(), Arc::new(on_click)));
207 self
208 }
209}
210
211impl PartialEq for Toast {
212 fn eq(&self, other: &Self) -> bool {
213 self.id == other.id
214 && self.msg == other.msg
215 && self.on_click.is_some() == other.on_click.is_some()
216 }
217}
218
219impl Clone for Toast {
220 fn clone(&self) -> Self {
221 Toast {
222 id: self.id,
223 msg: self.msg.to_owned(),
224 on_click: self.on_click.clone(),
225 }
226 }
227}
228
229#[derive(Clone, Deserialize, PartialEq)]
230pub struct OpenTerminal {
231 pub working_directory: PathBuf,
232}
233
234impl_actions!(
235 workspace,
236 [
237 ActivatePane,
238 ActivatePaneInDirection,
239 SwapPaneInDirection,
240 NewFileInDirection,
241 Toast,
242 OpenTerminal,
243 SaveAll,
244 Save,
245 CloseAllItemsAndPanes,
246 ]
247);
248
249pub type WorkspaceId = i64;
250
251pub fn init_settings(cx: &mut AppContext) {
252 settings::register::<WorkspaceSettings>(cx);
253 settings::register::<item::ItemSettings>(cx);
254}
255
256pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
257 init_settings(cx);
258 pane::init(cx);
259 notifications::init(cx);
260
261 cx.add_global_action({
262 let app_state = Arc::downgrade(&app_state);
263 move |_: &Open, cx: &mut AppContext| {
264 let mut paths = cx.prompt_for_paths(PathPromptOptions {
265 files: true,
266 directories: true,
267 multiple: true,
268 });
269
270 if let Some(app_state) = app_state.upgrade() {
271 cx.spawn(move |mut cx| async move {
272 if let Some(paths) = paths.recv().await.flatten() {
273 cx.update(|cx| {
274 open_paths(&paths, &app_state, None, cx).detach_and_log_err(cx)
275 });
276 }
277 })
278 .detach();
279 }
280 }
281 });
282 cx.add_async_action(Workspace::open);
283
284 cx.add_async_action(Workspace::follow_next_collaborator);
285 cx.add_async_action(Workspace::close);
286 cx.add_async_action(Workspace::close_inactive_items_and_panes);
287 cx.add_async_action(Workspace::close_all_items_and_panes);
288 cx.add_global_action(Workspace::close_global);
289 cx.add_global_action(restart);
290 cx.add_async_action(Workspace::save_all);
291 cx.add_action(Workspace::add_folder_to_project);
292 cx.add_action(
293 |workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext<Workspace>| {
294 let pane = workspace.active_pane().clone();
295 workspace.unfollow(&pane, cx);
296 },
297 );
298 cx.add_action(
299 |workspace: &mut Workspace, action: &Save, cx: &mut ViewContext<Workspace>| {
300 workspace
301 .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx)
302 .detach_and_log_err(cx);
303 },
304 );
305 cx.add_action(
306 |workspace: &mut Workspace, _: &SaveAs, cx: &mut ViewContext<Workspace>| {
307 workspace
308 .save_active_item(SaveIntent::SaveAs, cx)
309 .detach_and_log_err(cx);
310 },
311 );
312 cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| {
313 workspace.activate_previous_pane(cx)
314 });
315 cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| {
316 workspace.activate_next_pane(cx)
317 });
318
319 cx.add_action(
320 |workspace: &mut Workspace, action: &ActivatePaneInDirection, cx| {
321 workspace.activate_pane_in_direction(action.0, cx)
322 },
323 );
324
325 cx.add_action(
326 |workspace: &mut Workspace, action: &SwapPaneInDirection, cx| {
327 workspace.swap_pane_in_direction(action.0, cx)
328 },
329 );
330
331 cx.add_action(|workspace: &mut Workspace, _: &ToggleLeftDock, cx| {
332 workspace.toggle_dock(DockPosition::Left, cx);
333 });
334 cx.add_action(|workspace: &mut Workspace, _: &ToggleRightDock, cx| {
335 workspace.toggle_dock(DockPosition::Right, cx);
336 });
337 cx.add_action(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| {
338 workspace.toggle_dock(DockPosition::Bottom, cx);
339 });
340 cx.add_action(|workspace: &mut Workspace, _: &CloseAllDocks, cx| {
341 workspace.close_all_docks(cx);
342 });
343 cx.add_action(Workspace::activate_pane_at_index);
344 cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
345 workspace.reopen_closed_item(cx).detach();
346 });
347 cx.add_action(|workspace: &mut Workspace, _: &GoBack, cx| {
348 workspace
349 .go_back(workspace.active_pane().downgrade(), cx)
350 .detach();
351 });
352 cx.add_action(|workspace: &mut Workspace, _: &GoForward, cx| {
353 workspace
354 .go_forward(workspace.active_pane().downgrade(), cx)
355 .detach();
356 });
357
358 cx.add_action(|_: &mut Workspace, _: &install_cli::Install, cx| {
359 cx.spawn(|workspace, mut cx| async move {
360 let err = install_cli::install_cli(&cx)
361 .await
362 .context("Failed to create CLI symlink");
363
364 workspace.update(&mut cx, |workspace, cx| {
365 if matches!(err, Err(_)) {
366 err.notify_err(workspace, cx);
367 } else {
368 workspace.show_notification(1, cx, |cx| {
369 cx.add_view(|_| {
370 MessageNotification::new("Successfully installed the `zed` binary")
371 })
372 });
373 }
374 })
375 })
376 .detach();
377 });
378
379 let client = &app_state.client;
380 client.add_view_request_handler(Workspace::handle_follow);
381 client.add_view_message_handler(Workspace::handle_unfollow);
382 client.add_view_message_handler(Workspace::handle_update_followers);
383}
384
385type ProjectItemBuilders = HashMap<
386 TypeId,
387 fn(ModelHandle<Project>, AnyModelHandle, &mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
388>;
389pub fn register_project_item<I: ProjectItem>(cx: &mut AppContext) {
390 cx.update_default_global(|builders: &mut ProjectItemBuilders, _| {
391 builders.insert(TypeId::of::<I::Item>(), |project, model, cx| {
392 let item = model.downcast::<I::Item>().unwrap();
393 Box::new(cx.add_view(|cx| I::for_project_item(project, item, cx)))
394 });
395 });
396}
397
398type FollowableItemBuilder = fn(
399 ViewHandle<Pane>,
400 ViewHandle<Workspace>,
401 ViewId,
402 &mut Option<proto::view::Variant>,
403 &mut AppContext,
404) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>;
405type FollowableItemBuilders = HashMap<
406 TypeId,
407 (
408 FollowableItemBuilder,
409 fn(&AnyViewHandle) -> Box<dyn FollowableItemHandle>,
410 ),
411>;
412pub fn register_followable_item<I: FollowableItem>(cx: &mut AppContext) {
413 cx.update_default_global(|builders: &mut FollowableItemBuilders, _| {
414 builders.insert(
415 TypeId::of::<I>(),
416 (
417 |pane, workspace, id, state, cx| {
418 I::from_state_proto(pane, workspace, id, state, cx).map(|task| {
419 cx.foreground()
420 .spawn(async move { Ok(Box::new(task.await?) as Box<_>) })
421 })
422 },
423 |this| Box::new(this.clone().downcast::<I>().unwrap()),
424 ),
425 );
426 });
427}
428
429type ItemDeserializers = HashMap<
430 Arc<str>,
431 fn(
432 ModelHandle<Project>,
433 WeakViewHandle<Workspace>,
434 WorkspaceId,
435 ItemId,
436 &mut ViewContext<Pane>,
437 ) -> Task<Result<Box<dyn ItemHandle>>>,
438>;
439pub fn register_deserializable_item<I: Item>(cx: &mut AppContext) {
440 cx.update_default_global(|deserializers: &mut ItemDeserializers, _cx| {
441 if let Some(serialized_item_kind) = I::serialized_item_kind() {
442 deserializers.insert(
443 Arc::from(serialized_item_kind),
444 |project, workspace, workspace_id, item_id, cx| {
445 let task = I::deserialize(project, workspace, workspace_id, item_id, cx);
446 cx.foreground()
447 .spawn(async { Ok(Box::new(task.await?) as Box<_>) })
448 },
449 );
450 }
451 });
452}
453
454pub struct AppState {
455 pub languages: Arc<LanguageRegistry>,
456 pub client: Arc<Client>,
457 pub user_store: ModelHandle<UserStore>,
458 pub channel_store: ModelHandle<ChannelStore>,
459 pub fs: Arc<dyn fs::Fs>,
460 pub build_window_options:
461 fn(Option<WindowBounds>, Option<uuid::Uuid>, &dyn Platform) -> WindowOptions<'static>,
462 pub initialize_workspace:
463 fn(WeakViewHandle<Workspace>, bool, Arc<AppState>, AsyncAppContext) -> Task<Result<()>>,
464 pub background_actions: BackgroundActions,
465}
466
467impl AppState {
468 #[cfg(any(test, feature = "test-support"))]
469 pub fn test(cx: &mut AppContext) -> Arc<Self> {
470 use settings::SettingsStore;
471
472 if !cx.has_global::<SettingsStore>() {
473 cx.set_global(SettingsStore::test(cx));
474 }
475
476 let fs = fs::FakeFs::new(cx.background().clone());
477 let languages = Arc::new(LanguageRegistry::test());
478 let http_client = util::http::FakeHttpClient::with_404_response();
479 let client = Client::new(http_client.clone(), cx);
480 let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
481 let channel_store =
482 cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
483
484 theme::init((), cx);
485 client::init(&client, cx);
486 crate::init_settings(cx);
487
488 Arc::new(Self {
489 client,
490 fs,
491 languages,
492 user_store,
493 channel_store,
494 initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
495 build_window_options: |_, _, _| Default::default(),
496 background_actions: || &[],
497 })
498 }
499}
500
501struct DelayedDebouncedEditAction {
502 task: Option<Task<()>>,
503 cancel_channel: Option<oneshot::Sender<()>>,
504}
505
506impl DelayedDebouncedEditAction {
507 fn new() -> DelayedDebouncedEditAction {
508 DelayedDebouncedEditAction {
509 task: None,
510 cancel_channel: None,
511 }
512 }
513
514 fn fire_new<F>(&mut self, delay: Duration, cx: &mut ViewContext<Workspace>, func: F)
515 where
516 F: 'static + FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> Task<Result<()>>,
517 {
518 if let Some(channel) = self.cancel_channel.take() {
519 _ = channel.send(());
520 }
521
522 let (sender, mut receiver) = oneshot::channel::<()>();
523 self.cancel_channel = Some(sender);
524
525 let previous_task = self.task.take();
526 self.task = Some(cx.spawn(|workspace, mut cx| async move {
527 let mut timer = cx.background().timer(delay).fuse();
528 if let Some(previous_task) = previous_task {
529 previous_task.await;
530 }
531
532 futures::select_biased! {
533 _ = receiver => return,
534 _ = timer => {}
535 }
536
537 if let Some(result) = workspace
538 .update(&mut cx, |workspace, cx| (func)(workspace, cx))
539 .log_err()
540 {
541 result.await.log_err();
542 }
543 }));
544 }
545}
546
547pub enum Event {
548 PaneAdded(ViewHandle<Pane>),
549 ContactRequestedJoin(u64),
550}
551
552pub struct Workspace {
553 weak_self: WeakViewHandle<Self>,
554 remote_entity_subscription: Option<client::Subscription>,
555 modal: Option<ActiveModal>,
556 zoomed: Option<AnyWeakViewHandle>,
557 zoomed_position: Option<DockPosition>,
558 center: PaneGroup,
559 left_dock: ViewHandle<Dock>,
560 bottom_dock: ViewHandle<Dock>,
561 right_dock: ViewHandle<Dock>,
562 panes: Vec<ViewHandle<Pane>>,
563 panes_by_item: HashMap<usize, WeakViewHandle<Pane>>,
564 active_pane: ViewHandle<Pane>,
565 last_active_center_pane: Option<WeakViewHandle<Pane>>,
566 status_bar: ViewHandle<StatusBar>,
567 titlebar_item: Option<AnyViewHandle>,
568 notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
569 project: ModelHandle<Project>,
570 leader_state: LeaderState,
571 follower_states_by_leader: FollowerStatesByLeader,
572 last_leaders_by_pane: HashMap<WeakViewHandle<Pane>, PeerId>,
573 window_edited: bool,
574 active_call: Option<(ModelHandle<ActiveCall>, Vec<Subscription>)>,
575 leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
576 database_id: WorkspaceId,
577 app_state: Arc<AppState>,
578 subscriptions: Vec<Subscription>,
579 _apply_leader_updates: Task<Result<()>>,
580 _observe_current_user: Task<Result<()>>,
581 _schedule_serialize: Option<Task<()>>,
582 pane_history_timestamp: Arc<AtomicUsize>,
583}
584
585struct ActiveModal {
586 view: Box<dyn ModalHandle>,
587 previously_focused_view_id: Option<usize>,
588}
589
590#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
591pub struct ViewId {
592 pub creator: PeerId,
593 pub id: u64,
594}
595
596#[derive(Default)]
597struct LeaderState {
598 followers: HashSet<PeerId>,
599}
600
601type FollowerStatesByLeader = HashMap<PeerId, HashMap<ViewHandle<Pane>, FollowerState>>;
602
603#[derive(Default)]
604struct FollowerState {
605 active_view_id: Option<ViewId>,
606 items_by_leader_view_id: HashMap<ViewId, Box<dyn FollowableItemHandle>>,
607}
608
609enum WorkspaceBounds {}
610
611impl Workspace {
612 pub fn new(
613 workspace_id: WorkspaceId,
614 project: ModelHandle<Project>,
615 app_state: Arc<AppState>,
616 cx: &mut ViewContext<Self>,
617 ) -> Self {
618 cx.observe(&project, |_, _, cx| cx.notify()).detach();
619 cx.subscribe(&project, move |this, _, event, cx| {
620 match event {
621 project::Event::RemoteIdChanged(remote_id) => {
622 this.update_window_title(cx);
623 this.project_remote_id_changed(*remote_id, cx);
624 }
625
626 project::Event::CollaboratorLeft(peer_id) => {
627 this.collaborator_left(*peer_id, cx);
628 }
629
630 project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded => {
631 this.update_window_title(cx);
632 this.serialize_workspace(cx);
633 }
634
635 project::Event::DisconnectedFromHost => {
636 this.update_window_edited(cx);
637 cx.blur();
638 }
639
640 project::Event::Closed => {
641 cx.remove_window();
642 }
643
644 project::Event::DeletedEntry(entry_id) => {
645 for pane in this.panes.iter() {
646 pane.update(cx, |pane, cx| {
647 pane.handle_deleted_project_item(*entry_id, cx)
648 });
649 }
650 }
651
652 project::Event::Notification(message) => this.show_notification(0, cx, |cx| {
653 cx.add_view(|_| MessageNotification::new(message.clone()))
654 }),
655
656 _ => {}
657 }
658 cx.notify()
659 })
660 .detach();
661
662 let weak_handle = cx.weak_handle();
663 let pane_history_timestamp = Arc::new(AtomicUsize::new(0));
664
665 let center_pane = cx.add_view(|cx| {
666 Pane::new(
667 weak_handle.clone(),
668 project.clone(),
669 app_state.background_actions,
670 pane_history_timestamp.clone(),
671 cx,
672 )
673 });
674 cx.subscribe(¢er_pane, Self::handle_pane_event).detach();
675 cx.focus(¢er_pane);
676 cx.emit(Event::PaneAdded(center_pane.clone()));
677
678 let mut current_user = app_state.user_store.read(cx).watch_current_user();
679 let mut connection_status = app_state.client.status();
680 let _observe_current_user = cx.spawn(|this, mut cx| async move {
681 current_user.recv().await;
682 connection_status.recv().await;
683 let mut stream =
684 Stream::map(current_user, drop).merge(Stream::map(connection_status, drop));
685
686 while stream.recv().await.is_some() {
687 this.update(&mut cx, |_, cx| cx.notify())?;
688 }
689 anyhow::Ok(())
690 });
691
692 // All leader updates are enqueued and then processed in a single task, so
693 // that each asynchronous operation can be run in order.
694 let (leader_updates_tx, mut leader_updates_rx) =
695 mpsc::unbounded::<(PeerId, proto::UpdateFollowers)>();
696 let _apply_leader_updates = cx.spawn(|this, mut cx| async move {
697 while let Some((leader_id, update)) = leader_updates_rx.next().await {
698 Self::process_leader_update(&this, leader_id, update, &mut cx)
699 .await
700 .log_err();
701 }
702
703 Ok(())
704 });
705
706 cx.emit_global(WorkspaceCreated(weak_handle.clone()));
707
708 let left_dock = cx.add_view(|_| Dock::new(DockPosition::Left));
709 let bottom_dock = cx.add_view(|_| Dock::new(DockPosition::Bottom));
710 let right_dock = cx.add_view(|_| Dock::new(DockPosition::Right));
711 let left_dock_buttons =
712 cx.add_view(|cx| PanelButtons::new(left_dock.clone(), weak_handle.clone(), cx));
713 let bottom_dock_buttons =
714 cx.add_view(|cx| PanelButtons::new(bottom_dock.clone(), weak_handle.clone(), cx));
715 let right_dock_buttons =
716 cx.add_view(|cx| PanelButtons::new(right_dock.clone(), weak_handle.clone(), cx));
717 let status_bar = cx.add_view(|cx| {
718 let mut status_bar = StatusBar::new(¢er_pane.clone(), cx);
719 status_bar.add_left_item(left_dock_buttons, cx);
720 status_bar.add_right_item(right_dock_buttons, cx);
721 status_bar.add_right_item(bottom_dock_buttons, cx);
722 status_bar
723 });
724
725 cx.update_default_global::<DragAndDrop<Workspace>, _, _>(|drag_and_drop, _| {
726 drag_and_drop.register_container(weak_handle.clone());
727 });
728
729 let mut active_call = None;
730 if cx.has_global::<ModelHandle<ActiveCall>>() {
731 let call = cx.global::<ModelHandle<ActiveCall>>().clone();
732 let mut subscriptions = Vec::new();
733 subscriptions.push(cx.subscribe(&call, Self::on_active_call_event));
734 active_call = Some((call, subscriptions));
735 }
736
737 let subscriptions = vec![
738 cx.observe_fullscreen(|_, _, cx| cx.notify()),
739 cx.observe_window_activation(Self::on_window_activation_changed),
740 cx.observe_window_bounds(move |_, mut bounds, display, cx| {
741 // Transform fixed bounds to be stored in terms of the containing display
742 if let WindowBounds::Fixed(mut window_bounds) = bounds {
743 if let Some(screen) = cx.platform().screen_by_id(display) {
744 let screen_bounds = screen.bounds();
745 window_bounds
746 .set_origin_x(window_bounds.origin_x() - screen_bounds.origin_x());
747 window_bounds
748 .set_origin_y(window_bounds.origin_y() - screen_bounds.origin_y());
749 bounds = WindowBounds::Fixed(window_bounds);
750 }
751 }
752
753 cx.background()
754 .spawn(DB.set_window_bounds(workspace_id, bounds, display))
755 .detach_and_log_err(cx);
756 }),
757 cx.observe(&left_dock, |this, _, cx| {
758 this.serialize_workspace(cx);
759 cx.notify();
760 }),
761 cx.observe(&bottom_dock, |this, _, cx| {
762 this.serialize_workspace(cx);
763 cx.notify();
764 }),
765 cx.observe(&right_dock, |this, _, cx| {
766 this.serialize_workspace(cx);
767 cx.notify();
768 }),
769 ];
770
771 let mut this = Workspace {
772 weak_self: weak_handle.clone(),
773 modal: None,
774 zoomed: None,
775 zoomed_position: None,
776 center: PaneGroup::new(center_pane.clone()),
777 panes: vec![center_pane.clone()],
778 panes_by_item: Default::default(),
779 active_pane: center_pane.clone(),
780 last_active_center_pane: Some(center_pane.downgrade()),
781 status_bar,
782 titlebar_item: None,
783 notifications: Default::default(),
784 remote_entity_subscription: None,
785 left_dock,
786 bottom_dock,
787 right_dock,
788 project: project.clone(),
789 leader_state: Default::default(),
790 follower_states_by_leader: Default::default(),
791 last_leaders_by_pane: Default::default(),
792 window_edited: false,
793 active_call,
794 database_id: workspace_id,
795 app_state,
796 _observe_current_user,
797 _apply_leader_updates,
798 _schedule_serialize: None,
799 leader_updates_tx,
800 subscriptions,
801 pane_history_timestamp,
802 };
803 this.project_remote_id_changed(project.read(cx).remote_id(), cx);
804 cx.defer(|this, cx| this.update_window_title(cx));
805 this
806 }
807
808 fn new_local(
809 abs_paths: Vec<PathBuf>,
810 app_state: Arc<AppState>,
811 requesting_window: Option<WindowHandle<Workspace>>,
812 cx: &mut AppContext,
813 ) -> Task<(
814 WeakViewHandle<Workspace>,
815 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
816 )> {
817 let project_handle = Project::local(
818 app_state.client.clone(),
819 app_state.user_store.clone(),
820 app_state.languages.clone(),
821 app_state.fs.clone(),
822 cx,
823 );
824
825 cx.spawn(|mut cx| async move {
826 let serialized_workspace = persistence::DB.workspace_for_roots(&abs_paths.as_slice());
827
828 let paths_to_open = Arc::new(abs_paths);
829
830 // Get project paths for all of the abs_paths
831 let mut worktree_roots: HashSet<Arc<Path>> = Default::default();
832 let mut project_paths: Vec<(PathBuf, Option<ProjectPath>)> =
833 Vec::with_capacity(paths_to_open.len());
834 for path in paths_to_open.iter().cloned() {
835 if let Some((worktree, project_entry)) = cx
836 .update(|cx| {
837 Workspace::project_path_for_path(project_handle.clone(), &path, true, cx)
838 })
839 .await
840 .log_err()
841 {
842 worktree_roots.insert(worktree.read_with(&mut cx, |tree, _| tree.abs_path()));
843 project_paths.push((path, Some(project_entry)));
844 } else {
845 project_paths.push((path, None));
846 }
847 }
848
849 let workspace_id = if let Some(serialized_workspace) = serialized_workspace.as_ref() {
850 serialized_workspace.id
851 } else {
852 DB.next_id().await.unwrap_or(0)
853 };
854
855 let window = if let Some(window) = requesting_window {
856 window.replace_root(&mut cx, |cx| {
857 Workspace::new(workspace_id, project_handle.clone(), app_state.clone(), cx)
858 });
859 window
860 } else {
861 {
862 let window_bounds_override = window_bounds_env_override(&cx);
863 let (bounds, display) = if let Some(bounds) = window_bounds_override {
864 (Some(bounds), None)
865 } else {
866 serialized_workspace
867 .as_ref()
868 .and_then(|serialized_workspace| {
869 let display = serialized_workspace.display?;
870 let mut bounds = serialized_workspace.bounds?;
871
872 // Stored bounds are relative to the containing display.
873 // So convert back to global coordinates if that screen still exists
874 if let WindowBounds::Fixed(mut window_bounds) = bounds {
875 if let Some(screen) = cx.platform().screen_by_id(display) {
876 let screen_bounds = screen.bounds();
877 window_bounds.set_origin_x(
878 window_bounds.origin_x() + screen_bounds.origin_x(),
879 );
880 window_bounds.set_origin_y(
881 window_bounds.origin_y() + screen_bounds.origin_y(),
882 );
883 bounds = WindowBounds::Fixed(window_bounds);
884 } else {
885 // Screen no longer exists. Return none here.
886 return None;
887 }
888 }
889
890 Some((bounds, display))
891 })
892 .unzip()
893 };
894
895 // Use the serialized workspace to construct the new window
896 cx.add_window(
897 (app_state.build_window_options)(bounds, display, cx.platform().as_ref()),
898 |cx| {
899 Workspace::new(
900 workspace_id,
901 project_handle.clone(),
902 app_state.clone(),
903 cx,
904 )
905 },
906 )
907 }
908 };
909
910 // We haven't yielded the main thread since obtaining the window handle,
911 // so the window exists.
912 let workspace = window.root(&cx).unwrap();
913
914 (app_state.initialize_workspace)(
915 workspace.downgrade(),
916 serialized_workspace.is_some(),
917 app_state.clone(),
918 cx.clone(),
919 )
920 .await
921 .log_err();
922
923 window.update(&mut cx, |cx| cx.activate_window());
924
925 let workspace = workspace.downgrade();
926 notify_if_database_failed(&workspace, &mut cx);
927 let opened_items = open_items(
928 serialized_workspace,
929 &workspace,
930 project_paths,
931 app_state,
932 cx,
933 )
934 .await;
935
936 (workspace, opened_items)
937 })
938 }
939
940 pub fn weak_handle(&self) -> WeakViewHandle<Self> {
941 self.weak_self.clone()
942 }
943
944 pub fn left_dock(&self) -> &ViewHandle<Dock> {
945 &self.left_dock
946 }
947
948 pub fn bottom_dock(&self) -> &ViewHandle<Dock> {
949 &self.bottom_dock
950 }
951
952 pub fn right_dock(&self) -> &ViewHandle<Dock> {
953 &self.right_dock
954 }
955
956 pub fn add_panel<T: Panel>(&mut self, panel: ViewHandle<T>, cx: &mut ViewContext<Self>)
957 where
958 T::Event: std::fmt::Debug,
959 {
960 self.add_panel_with_extra_event_handler(panel, cx, |_, _, _, _| {})
961 }
962
963 pub fn add_panel_with_extra_event_handler<T: Panel, F>(
964 &mut self,
965 panel: ViewHandle<T>,
966 cx: &mut ViewContext<Self>,
967 handler: F,
968 ) where
969 T::Event: std::fmt::Debug,
970 F: Fn(&mut Self, &ViewHandle<T>, &T::Event, &mut ViewContext<Self>) + 'static,
971 {
972 let dock = match panel.position(cx) {
973 DockPosition::Left => &self.left_dock,
974 DockPosition::Bottom => &self.bottom_dock,
975 DockPosition::Right => &self.right_dock,
976 };
977
978 self.subscriptions.push(cx.subscribe(&panel, {
979 let mut dock = dock.clone();
980 let mut prev_position = panel.position(cx);
981 move |this, panel, event, cx| {
982 if T::should_change_position_on_event(event) {
983 let new_position = panel.read(cx).position(cx);
984 let mut was_visible = false;
985 dock.update(cx, |dock, cx| {
986 prev_position = new_position;
987
988 was_visible = dock.is_open()
989 && dock
990 .visible_panel()
991 .map_or(false, |active_panel| active_panel.id() == panel.id());
992 dock.remove_panel(&panel, cx);
993 });
994
995 if panel.is_zoomed(cx) {
996 this.zoomed_position = Some(new_position);
997 }
998
999 dock = match panel.read(cx).position(cx) {
1000 DockPosition::Left => &this.left_dock,
1001 DockPosition::Bottom => &this.bottom_dock,
1002 DockPosition::Right => &this.right_dock,
1003 }
1004 .clone();
1005 dock.update(cx, |dock, cx| {
1006 dock.add_panel(panel.clone(), cx);
1007 if was_visible {
1008 dock.set_open(true, cx);
1009 dock.activate_panel(dock.panels_len() - 1, cx);
1010 }
1011 });
1012 } else if T::should_zoom_in_on_event(event) {
1013 dock.update(cx, |dock, cx| dock.set_panel_zoomed(&panel, true, cx));
1014 if !panel.has_focus(cx) {
1015 cx.focus(&panel);
1016 }
1017 this.zoomed = Some(panel.downgrade().into_any());
1018 this.zoomed_position = Some(panel.read(cx).position(cx));
1019 } else if T::should_zoom_out_on_event(event) {
1020 dock.update(cx, |dock, cx| dock.set_panel_zoomed(&panel, false, cx));
1021 if this.zoomed_position == Some(prev_position) {
1022 this.zoomed = None;
1023 this.zoomed_position = None;
1024 }
1025 cx.notify();
1026 } else if T::is_focus_event(event) {
1027 let position = panel.read(cx).position(cx);
1028 this.dismiss_zoomed_items_to_reveal(Some(position), cx);
1029 if panel.is_zoomed(cx) {
1030 this.zoomed = Some(panel.downgrade().into_any());
1031 this.zoomed_position = Some(position);
1032 } else {
1033 this.zoomed = None;
1034 this.zoomed_position = None;
1035 }
1036 this.update_active_view_for_followers(cx);
1037 cx.notify();
1038 } else {
1039 handler(this, &panel, event, cx)
1040 }
1041 }
1042 }));
1043
1044 dock.update(cx, |dock, cx| dock.add_panel(panel, cx));
1045 }
1046
1047 pub fn status_bar(&self) -> &ViewHandle<StatusBar> {
1048 &self.status_bar
1049 }
1050
1051 pub fn app_state(&self) -> &Arc<AppState> {
1052 &self.app_state
1053 }
1054
1055 pub fn user_store(&self) -> &ModelHandle<UserStore> {
1056 &self.app_state.user_store
1057 }
1058
1059 pub fn project(&self) -> &ModelHandle<Project> {
1060 &self.project
1061 }
1062
1063 pub fn recent_navigation_history(
1064 &self,
1065 limit: Option<usize>,
1066 cx: &AppContext,
1067 ) -> Vec<(ProjectPath, Option<PathBuf>)> {
1068 let mut abs_paths_opened: HashMap<PathBuf, HashSet<ProjectPath>> = HashMap::default();
1069 let mut history: HashMap<ProjectPath, (Option<PathBuf>, usize)> = HashMap::default();
1070 for pane in &self.panes {
1071 let pane = pane.read(cx);
1072 pane.nav_history()
1073 .for_each_entry(cx, |entry, (project_path, fs_path)| {
1074 if let Some(fs_path) = &fs_path {
1075 abs_paths_opened
1076 .entry(fs_path.clone())
1077 .or_default()
1078 .insert(project_path.clone());
1079 }
1080 let timestamp = entry.timestamp;
1081 match history.entry(project_path) {
1082 hash_map::Entry::Occupied(mut entry) => {
1083 let (_, old_timestamp) = entry.get();
1084 if ×tamp > old_timestamp {
1085 entry.insert((fs_path, timestamp));
1086 }
1087 }
1088 hash_map::Entry::Vacant(entry) => {
1089 entry.insert((fs_path, timestamp));
1090 }
1091 }
1092 });
1093 }
1094
1095 history
1096 .into_iter()
1097 .sorted_by_key(|(_, (_, timestamp))| *timestamp)
1098 .map(|(project_path, (fs_path, _))| (project_path, fs_path))
1099 .rev()
1100 .filter(|(history_path, abs_path)| {
1101 let latest_project_path_opened = abs_path
1102 .as_ref()
1103 .and_then(|abs_path| abs_paths_opened.get(abs_path))
1104 .and_then(|project_paths| {
1105 project_paths
1106 .iter()
1107 .max_by(|b1, b2| b1.worktree_id.cmp(&b2.worktree_id))
1108 });
1109
1110 match latest_project_path_opened {
1111 Some(latest_project_path_opened) => latest_project_path_opened == history_path,
1112 None => true,
1113 }
1114 })
1115 .take(limit.unwrap_or(usize::MAX))
1116 .collect()
1117 }
1118
1119 fn navigate_history(
1120 &mut self,
1121 pane: WeakViewHandle<Pane>,
1122 mode: NavigationMode,
1123 cx: &mut ViewContext<Workspace>,
1124 ) -> Task<Result<()>> {
1125 let to_load = if let Some(pane) = pane.upgrade(cx) {
1126 cx.focus(&pane);
1127
1128 pane.update(cx, |pane, cx| {
1129 loop {
1130 // Retrieve the weak item handle from the history.
1131 let entry = pane.nav_history_mut().pop(mode, cx)?;
1132
1133 // If the item is still present in this pane, then activate it.
1134 if let Some(index) = entry
1135 .item
1136 .upgrade(cx)
1137 .and_then(|v| pane.index_for_item(v.as_ref()))
1138 {
1139 let prev_active_item_index = pane.active_item_index();
1140 pane.nav_history_mut().set_mode(mode);
1141 pane.activate_item(index, true, true, cx);
1142 pane.nav_history_mut().set_mode(NavigationMode::Normal);
1143
1144 let mut navigated = prev_active_item_index != pane.active_item_index();
1145 if let Some(data) = entry.data {
1146 navigated |= pane.active_item()?.navigate(data, cx);
1147 }
1148
1149 if navigated {
1150 break None;
1151 }
1152 }
1153 // If the item is no longer present in this pane, then retrieve its
1154 // project path in order to reopen it.
1155 else {
1156 break pane
1157 .nav_history()
1158 .path_for_item(entry.item.id())
1159 .map(|(project_path, _)| (project_path, entry));
1160 }
1161 }
1162 })
1163 } else {
1164 None
1165 };
1166
1167 if let Some((project_path, entry)) = to_load {
1168 // If the item was no longer present, then load it again from its previous path.
1169 let task = self.load_path(project_path, cx);
1170 cx.spawn(|workspace, mut cx| async move {
1171 let task = task.await;
1172 let mut navigated = false;
1173 if let Some((project_entry_id, build_item)) = task.log_err() {
1174 let prev_active_item_id = pane.update(&mut cx, |pane, _| {
1175 pane.nav_history_mut().set_mode(mode);
1176 pane.active_item().map(|p| p.id())
1177 })?;
1178
1179 pane.update(&mut cx, |pane, cx| {
1180 let item = pane.open_item(project_entry_id, true, cx, build_item);
1181 navigated |= Some(item.id()) != prev_active_item_id;
1182 pane.nav_history_mut().set_mode(NavigationMode::Normal);
1183 if let Some(data) = entry.data {
1184 navigated |= item.navigate(data, cx);
1185 }
1186 })?;
1187 }
1188
1189 if !navigated {
1190 workspace
1191 .update(&mut cx, |workspace, cx| {
1192 Self::navigate_history(workspace, pane, mode, cx)
1193 })?
1194 .await?;
1195 }
1196
1197 Ok(())
1198 })
1199 } else {
1200 Task::ready(Ok(()))
1201 }
1202 }
1203
1204 pub fn go_back(
1205 &mut self,
1206 pane: WeakViewHandle<Pane>,
1207 cx: &mut ViewContext<Workspace>,
1208 ) -> Task<Result<()>> {
1209 self.navigate_history(pane, NavigationMode::GoingBack, cx)
1210 }
1211
1212 pub fn go_forward(
1213 &mut self,
1214 pane: WeakViewHandle<Pane>,
1215 cx: &mut ViewContext<Workspace>,
1216 ) -> Task<Result<()>> {
1217 self.navigate_history(pane, NavigationMode::GoingForward, cx)
1218 }
1219
1220 pub fn reopen_closed_item(&mut self, cx: &mut ViewContext<Workspace>) -> Task<Result<()>> {
1221 self.navigate_history(
1222 self.active_pane().downgrade(),
1223 NavigationMode::ReopeningClosedItem,
1224 cx,
1225 )
1226 }
1227
1228 pub fn client(&self) -> &Client {
1229 &self.app_state.client
1230 }
1231
1232 pub fn set_titlebar_item(&mut self, item: AnyViewHandle, cx: &mut ViewContext<Self>) {
1233 self.titlebar_item = Some(item);
1234 cx.notify();
1235 }
1236
1237 pub fn titlebar_item(&self) -> Option<AnyViewHandle> {
1238 self.titlebar_item.clone()
1239 }
1240
1241 /// Call the given callback with a workspace whose project is local.
1242 ///
1243 /// If the given workspace has a local project, then it will be passed
1244 /// to the callback. Otherwise, a new empty window will be created.
1245 pub fn with_local_workspace<T, F>(
1246 &mut self,
1247 cx: &mut ViewContext<Self>,
1248 callback: F,
1249 ) -> Task<Result<T>>
1250 where
1251 T: 'static,
1252 F: 'static + FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
1253 {
1254 if self.project.read(cx).is_local() {
1255 Task::Ready(Some(Ok(callback(self, cx))))
1256 } else {
1257 let task = Self::new_local(Vec::new(), self.app_state.clone(), None, cx);
1258 cx.spawn(|_vh, mut cx| async move {
1259 let (workspace, _) = task.await;
1260 workspace.update(&mut cx, callback)
1261 })
1262 }
1263 }
1264
1265 pub fn worktrees<'a>(
1266 &self,
1267 cx: &'a AppContext,
1268 ) -> impl 'a + Iterator<Item = ModelHandle<Worktree>> {
1269 self.project.read(cx).worktrees(cx)
1270 }
1271
1272 pub fn visible_worktrees<'a>(
1273 &self,
1274 cx: &'a AppContext,
1275 ) -> impl 'a + Iterator<Item = ModelHandle<Worktree>> {
1276 self.project.read(cx).visible_worktrees(cx)
1277 }
1278
1279 pub fn worktree_scans_complete(&self, cx: &AppContext) -> impl Future<Output = ()> + 'static {
1280 let futures = self
1281 .worktrees(cx)
1282 .filter_map(|worktree| worktree.read(cx).as_local())
1283 .map(|worktree| worktree.scan_complete())
1284 .collect::<Vec<_>>();
1285 async move {
1286 for future in futures {
1287 future.await;
1288 }
1289 }
1290 }
1291
1292 pub fn close_global(_: &CloseWindow, cx: &mut AppContext) {
1293 cx.spawn(|mut cx| async move {
1294 let window = cx
1295 .windows()
1296 .into_iter()
1297 .find(|window| window.is_active(&cx).unwrap_or(false));
1298 if let Some(window) = window {
1299 //This can only get called when the window's project connection has been lost
1300 //so we don't need to prompt the user for anything and instead just close the window
1301 window.remove(&mut cx);
1302 }
1303 })
1304 .detach();
1305 }
1306
1307 pub fn close(
1308 &mut self,
1309 _: &CloseWindow,
1310 cx: &mut ViewContext<Self>,
1311 ) -> Option<Task<Result<()>>> {
1312 let window = cx.window();
1313 let prepare = self.prepare_to_close(false, cx);
1314 Some(cx.spawn(|_, mut cx| async move {
1315 if prepare.await? {
1316 window.remove(&mut cx);
1317 }
1318 Ok(())
1319 }))
1320 }
1321
1322 pub fn prepare_to_close(
1323 &mut self,
1324 quitting: bool,
1325 cx: &mut ViewContext<Self>,
1326 ) -> Task<Result<bool>> {
1327 let active_call = self.active_call().cloned();
1328 let window = cx.window();
1329
1330 cx.spawn(|this, mut cx| async move {
1331 let workspace_count = cx
1332 .windows()
1333 .into_iter()
1334 .filter(|window| window.root_is::<Workspace>())
1335 .count();
1336
1337 if let Some(active_call) = active_call {
1338 if !quitting
1339 && workspace_count == 1
1340 && active_call.read_with(&cx, |call, _| call.room().is_some())
1341 {
1342 let answer = window.prompt(
1343 PromptLevel::Warning,
1344 "Do you want to leave the current call?",
1345 &["Close window and hang up", "Cancel"],
1346 &mut cx,
1347 );
1348
1349 if let Some(mut answer) = answer {
1350 if answer.next().await == Some(1) {
1351 return anyhow::Ok(false);
1352 } else {
1353 active_call
1354 .update(&mut cx, |call, cx| call.hang_up(cx))
1355 .await
1356 .log_err();
1357 }
1358 }
1359 }
1360 }
1361
1362 Ok(this
1363 .update(&mut cx, |this, cx| {
1364 this.save_all_internal(SaveIntent::Close, cx)
1365 })?
1366 .await?)
1367 })
1368 }
1369
1370 fn save_all(
1371 &mut self,
1372 action: &SaveAll,
1373 cx: &mut ViewContext<Self>,
1374 ) -> Option<Task<Result<()>>> {
1375 let save_all =
1376 self.save_all_internal(action.save_intent.unwrap_or(SaveIntent::SaveAll), cx);
1377 Some(cx.foreground().spawn(async move {
1378 save_all.await?;
1379 Ok(())
1380 }))
1381 }
1382
1383 fn save_all_internal(
1384 &mut self,
1385 mut save_intent: SaveIntent,
1386 cx: &mut ViewContext<Self>,
1387 ) -> Task<Result<bool>> {
1388 if self.project.read(cx).is_read_only() {
1389 return Task::ready(Ok(true));
1390 }
1391 let dirty_items = self
1392 .panes
1393 .iter()
1394 .flat_map(|pane| {
1395 pane.read(cx).items().filter_map(|item| {
1396 if item.is_dirty(cx) {
1397 Some((pane.downgrade(), item.boxed_clone()))
1398 } else {
1399 None
1400 }
1401 })
1402 })
1403 .collect::<Vec<_>>();
1404
1405 let project = self.project.clone();
1406 cx.spawn(|workspace, mut cx| async move {
1407 // Override save mode and display "Save all files" prompt
1408 if save_intent == SaveIntent::Close && dirty_items.len() > 1 {
1409 let mut answer = workspace.update(&mut cx, |_, cx| {
1410 let prompt = Pane::file_names_for_prompt(
1411 &mut dirty_items.iter().map(|(_, handle)| handle),
1412 dirty_items.len(),
1413 cx,
1414 );
1415 cx.prompt(
1416 PromptLevel::Warning,
1417 &prompt,
1418 &["Save all", "Discard all", "Cancel"],
1419 )
1420 })?;
1421 match answer.next().await {
1422 Some(0) => save_intent = SaveIntent::SaveAll,
1423 Some(1) => save_intent = SaveIntent::Skip,
1424 _ => {}
1425 }
1426 }
1427 for (pane, item) in dirty_items {
1428 let (singleton, project_entry_ids) =
1429 cx.read(|cx| (item.is_singleton(cx), item.project_entry_ids(cx)));
1430 if singleton || !project_entry_ids.is_empty() {
1431 if let Some(ix) =
1432 pane.read_with(&cx, |pane, _| pane.index_for_item(item.as_ref()))?
1433 {
1434 if !Pane::save_item(
1435 project.clone(),
1436 &pane,
1437 ix,
1438 &*item,
1439 save_intent,
1440 &mut cx,
1441 )
1442 .await?
1443 {
1444 return Ok(false);
1445 }
1446 }
1447 }
1448 }
1449 Ok(true)
1450 })
1451 }
1452
1453 pub fn open(&mut self, _: &Open, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
1454 let mut paths = cx.prompt_for_paths(PathPromptOptions {
1455 files: true,
1456 directories: true,
1457 multiple: true,
1458 });
1459
1460 Some(cx.spawn(|this, mut cx| async move {
1461 if let Some(paths) = paths.recv().await.flatten() {
1462 if let Some(task) = this
1463 .update(&mut cx, |this, cx| this.open_workspace_for_paths(paths, cx))
1464 .log_err()
1465 {
1466 task.await?
1467 }
1468 }
1469 Ok(())
1470 }))
1471 }
1472
1473 pub fn open_workspace_for_paths(
1474 &mut self,
1475 paths: Vec<PathBuf>,
1476 cx: &mut ViewContext<Self>,
1477 ) -> Task<Result<()>> {
1478 let window = cx.window().downcast::<Self>();
1479 let is_remote = self.project.read(cx).is_remote();
1480 let has_worktree = self.project.read(cx).worktrees(cx).next().is_some();
1481 let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx));
1482 let close_task = if is_remote || has_worktree || has_dirty_items {
1483 None
1484 } else {
1485 Some(self.prepare_to_close(false, cx))
1486 };
1487 let app_state = self.app_state.clone();
1488
1489 cx.spawn(|_, mut cx| async move {
1490 let window_to_replace = if let Some(close_task) = close_task {
1491 if !close_task.await? {
1492 return Ok(());
1493 }
1494 window
1495 } else {
1496 None
1497 };
1498 cx.update(|cx| open_paths(&paths, &app_state, window_to_replace, cx))
1499 .await?;
1500 Ok(())
1501 })
1502 }
1503
1504 #[allow(clippy::type_complexity)]
1505 pub fn open_paths(
1506 &mut self,
1507 mut abs_paths: Vec<PathBuf>,
1508 visible: bool,
1509 cx: &mut ViewContext<Self>,
1510 ) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
1511 log::info!("open paths {:?}", abs_paths);
1512
1513 let fs = self.app_state.fs.clone();
1514
1515 // Sort the paths to ensure we add worktrees for parents before their children.
1516 abs_paths.sort_unstable();
1517 cx.spawn(|this, mut cx| async move {
1518 let mut tasks = Vec::with_capacity(abs_paths.len());
1519 for abs_path in &abs_paths {
1520 let project_path = match this
1521 .update(&mut cx, |this, cx| {
1522 Workspace::project_path_for_path(
1523 this.project.clone(),
1524 abs_path,
1525 visible,
1526 cx,
1527 )
1528 })
1529 .log_err()
1530 {
1531 Some(project_path) => project_path.await.log_err(),
1532 None => None,
1533 };
1534
1535 let this = this.clone();
1536 let task = cx.spawn(|mut cx| {
1537 let fs = fs.clone();
1538 let abs_path = abs_path.clone();
1539 async move {
1540 let (worktree, project_path) = project_path?;
1541 if fs.is_file(&abs_path).await {
1542 Some(
1543 this.update(&mut cx, |this, cx| {
1544 this.open_path(project_path, None, true, cx)
1545 })
1546 .log_err()?
1547 .await,
1548 )
1549 } else {
1550 this.update(&mut cx, |workspace, cx| {
1551 let worktree = worktree.read(cx);
1552 let worktree_abs_path = worktree.abs_path();
1553 let entry_id = if abs_path == worktree_abs_path.as_ref() {
1554 worktree.root_entry()
1555 } else {
1556 abs_path
1557 .strip_prefix(worktree_abs_path.as_ref())
1558 .ok()
1559 .and_then(|relative_path| {
1560 worktree.entry_for_path(relative_path)
1561 })
1562 }
1563 .map(|entry| entry.id);
1564 if let Some(entry_id) = entry_id {
1565 workspace.project().update(cx, |_, cx| {
1566 cx.emit(project::Event::ActiveEntryChanged(Some(entry_id)));
1567 })
1568 }
1569 })
1570 .log_err()?;
1571 None
1572 }
1573 }
1574 });
1575 tasks.push(task);
1576 }
1577
1578 futures::future::join_all(tasks).await
1579 })
1580 }
1581
1582 fn add_folder_to_project(&mut self, _: &AddFolderToProject, cx: &mut ViewContext<Self>) {
1583 let mut paths = cx.prompt_for_paths(PathPromptOptions {
1584 files: false,
1585 directories: true,
1586 multiple: true,
1587 });
1588 cx.spawn(|this, mut cx| async move {
1589 if let Some(paths) = paths.recv().await.flatten() {
1590 let results = this
1591 .update(&mut cx, |this, cx| this.open_paths(paths, true, cx))?
1592 .await;
1593 for result in results.into_iter().flatten() {
1594 result.log_err();
1595 }
1596 }
1597 anyhow::Ok(())
1598 })
1599 .detach_and_log_err(cx);
1600 }
1601
1602 fn project_path_for_path(
1603 project: ModelHandle<Project>,
1604 abs_path: &Path,
1605 visible: bool,
1606 cx: &mut AppContext,
1607 ) -> Task<Result<(ModelHandle<Worktree>, ProjectPath)>> {
1608 let entry = project.update(cx, |project, cx| {
1609 project.find_or_create_local_worktree(abs_path, visible, cx)
1610 });
1611 cx.spawn(|cx| async move {
1612 let (worktree, path) = entry.await?;
1613 let worktree_id = worktree.read_with(&cx, |t, _| t.id());
1614 Ok((
1615 worktree,
1616 ProjectPath {
1617 worktree_id,
1618 path: path.into(),
1619 },
1620 ))
1621 })
1622 }
1623
1624 /// Returns the modal that was toggled closed if it was open.
1625 pub fn toggle_modal<V, F>(
1626 &mut self,
1627 cx: &mut ViewContext<Self>,
1628 add_view: F,
1629 ) -> Option<ViewHandle<V>>
1630 where
1631 V: 'static + Modal,
1632 F: FnOnce(&mut Self, &mut ViewContext<Self>) -> ViewHandle<V>,
1633 {
1634 cx.notify();
1635 // Whatever modal was visible is getting clobbered. If its the same type as V, then return
1636 // it. Otherwise, create a new modal and set it as active.
1637 if let Some(already_open_modal) = self
1638 .dismiss_modal(cx)
1639 .and_then(|modal| modal.downcast::<V>())
1640 {
1641 cx.focus_self();
1642 Some(already_open_modal)
1643 } else {
1644 let modal = add_view(self, cx);
1645 cx.subscribe(&modal, |this, _, event, cx| {
1646 if V::dismiss_on_event(event) {
1647 this.dismiss_modal(cx);
1648 }
1649 })
1650 .detach();
1651 let previously_focused_view_id = cx.focused_view_id();
1652 cx.focus(&modal);
1653 self.modal = Some(ActiveModal {
1654 view: Box::new(modal),
1655 previously_focused_view_id,
1656 });
1657 None
1658 }
1659 }
1660
1661 pub fn modal<V: 'static + View>(&self) -> Option<ViewHandle<V>> {
1662 self.modal
1663 .as_ref()
1664 .and_then(|modal| modal.view.as_any().clone().downcast::<V>())
1665 }
1666
1667 pub fn dismiss_modal(&mut self, cx: &mut ViewContext<Self>) -> Option<AnyViewHandle> {
1668 if let Some(modal) = self.modal.take() {
1669 if let Some(previously_focused_view_id) = modal.previously_focused_view_id {
1670 if modal.view.has_focus(cx) {
1671 cx.window_context().focus(Some(previously_focused_view_id));
1672 }
1673 }
1674 cx.notify();
1675 Some(modal.view.as_any().clone())
1676 } else {
1677 None
1678 }
1679 }
1680
1681 pub fn items<'a>(
1682 &'a self,
1683 cx: &'a AppContext,
1684 ) -> impl 'a + Iterator<Item = &Box<dyn ItemHandle>> {
1685 self.panes.iter().flat_map(|pane| pane.read(cx).items())
1686 }
1687
1688 pub fn item_of_type<T: Item>(&self, cx: &AppContext) -> Option<ViewHandle<T>> {
1689 self.items_of_type(cx).max_by_key(|item| item.id())
1690 }
1691
1692 pub fn items_of_type<'a, T: Item>(
1693 &'a self,
1694 cx: &'a AppContext,
1695 ) -> impl 'a + Iterator<Item = ViewHandle<T>> {
1696 self.panes
1697 .iter()
1698 .flat_map(|pane| pane.read(cx).items_of_type())
1699 }
1700
1701 pub fn active_item(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
1702 self.active_pane().read(cx).active_item()
1703 }
1704
1705 fn active_project_path(&self, cx: &ViewContext<Self>) -> Option<ProjectPath> {
1706 self.active_item(cx).and_then(|item| item.project_path(cx))
1707 }
1708
1709 pub fn save_active_item(
1710 &mut self,
1711 save_intent: SaveIntent,
1712 cx: &mut ViewContext<Self>,
1713 ) -> Task<Result<()>> {
1714 let project = self.project.clone();
1715 let pane = self.active_pane();
1716 let item_ix = pane.read(cx).active_item_index();
1717 let item = pane.read(cx).active_item();
1718 let pane = pane.downgrade();
1719
1720 cx.spawn(|_, mut cx| async move {
1721 if let Some(item) = item {
1722 Pane::save_item(project, &pane, item_ix, item.as_ref(), save_intent, &mut cx)
1723 .await
1724 .map(|_| ())
1725 } else {
1726 Ok(())
1727 }
1728 })
1729 }
1730
1731 pub fn close_inactive_items_and_panes(
1732 &mut self,
1733 _: &CloseInactiveTabsAndPanes,
1734 cx: &mut ViewContext<Self>,
1735 ) -> Option<Task<Result<()>>> {
1736 self.close_all_internal(true, SaveIntent::Close, cx)
1737 }
1738
1739 pub fn close_all_items_and_panes(
1740 &mut self,
1741 action: &CloseAllItemsAndPanes,
1742 cx: &mut ViewContext<Self>,
1743 ) -> Option<Task<Result<()>>> {
1744 self.close_all_internal(false, action.save_intent.unwrap_or(SaveIntent::Close), cx)
1745 }
1746
1747 fn close_all_internal(
1748 &mut self,
1749 retain_active_pane: bool,
1750 save_intent: SaveIntent,
1751 cx: &mut ViewContext<Self>,
1752 ) -> Option<Task<Result<()>>> {
1753 let current_pane = self.active_pane();
1754
1755 let mut tasks = Vec::new();
1756
1757 if retain_active_pane {
1758 if let Some(current_pane_close) = current_pane.update(cx, |pane, cx| {
1759 pane.close_inactive_items(&CloseInactiveItems, cx)
1760 }) {
1761 tasks.push(current_pane_close);
1762 };
1763 }
1764
1765 for pane in self.panes() {
1766 if retain_active_pane && pane.id() == current_pane.id() {
1767 continue;
1768 }
1769
1770 if let Some(close_pane_items) = pane.update(cx, |pane: &mut Pane, cx| {
1771 pane.close_all_items(
1772 &CloseAllItems {
1773 save_intent: Some(save_intent),
1774 },
1775 cx,
1776 )
1777 }) {
1778 tasks.push(close_pane_items)
1779 }
1780 }
1781
1782 if tasks.is_empty() {
1783 None
1784 } else {
1785 Some(cx.spawn(|_, _| async move {
1786 for task in tasks {
1787 task.await?
1788 }
1789 Ok(())
1790 }))
1791 }
1792 }
1793
1794 pub fn toggle_dock(&mut self, dock_side: DockPosition, cx: &mut ViewContext<Self>) {
1795 let dock = match dock_side {
1796 DockPosition::Left => &self.left_dock,
1797 DockPosition::Bottom => &self.bottom_dock,
1798 DockPosition::Right => &self.right_dock,
1799 };
1800 let mut focus_center = false;
1801 let mut reveal_dock = false;
1802 dock.update(cx, |dock, cx| {
1803 let other_is_zoomed = self.zoomed.is_some() && self.zoomed_position != Some(dock_side);
1804 let was_visible = dock.is_open() && !other_is_zoomed;
1805 dock.set_open(!was_visible, cx);
1806
1807 if let Some(active_panel) = dock.active_panel() {
1808 if was_visible {
1809 if active_panel.has_focus(cx) {
1810 focus_center = true;
1811 }
1812 } else {
1813 cx.focus(active_panel.as_any());
1814 reveal_dock = true;
1815 }
1816 }
1817 });
1818
1819 if reveal_dock {
1820 self.dismiss_zoomed_items_to_reveal(Some(dock_side), cx);
1821 }
1822
1823 if focus_center {
1824 cx.focus_self();
1825 }
1826
1827 cx.notify();
1828 self.serialize_workspace(cx);
1829 }
1830
1831 pub fn close_all_docks(&mut self, cx: &mut ViewContext<Self>) {
1832 let docks = [&self.left_dock, &self.bottom_dock, &self.right_dock];
1833
1834 for dock in docks {
1835 dock.update(cx, |dock, cx| {
1836 dock.set_open(false, cx);
1837 });
1838 }
1839
1840 cx.focus_self();
1841 cx.notify();
1842 self.serialize_workspace(cx);
1843 }
1844
1845 /// Transfer focus to the panel of the given type.
1846 pub fn focus_panel<T: Panel>(&mut self, cx: &mut ViewContext<Self>) -> Option<ViewHandle<T>> {
1847 self.focus_or_unfocus_panel::<T>(cx, |_, _| true)?
1848 .as_any()
1849 .clone()
1850 .downcast()
1851 }
1852
1853 /// Focus the panel of the given type if it isn't already focused. If it is
1854 /// already focused, then transfer focus back to the workspace center.
1855 pub fn toggle_panel_focus<T: Panel>(&mut self, cx: &mut ViewContext<Self>) {
1856 self.focus_or_unfocus_panel::<T>(cx, |panel, cx| !panel.has_focus(cx));
1857 }
1858
1859 /// Focus or unfocus the given panel type, depending on the given callback.
1860 fn focus_or_unfocus_panel<T: Panel>(
1861 &mut self,
1862 cx: &mut ViewContext<Self>,
1863 should_focus: impl Fn(&dyn PanelHandle, &mut ViewContext<Dock>) -> bool,
1864 ) -> Option<Rc<dyn PanelHandle>> {
1865 for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
1866 if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
1867 let mut focus_center = false;
1868 let mut reveal_dock = false;
1869 let panel = dock.update(cx, |dock, cx| {
1870 dock.activate_panel(panel_index, cx);
1871
1872 let panel = dock.active_panel().cloned();
1873 if let Some(panel) = panel.as_ref() {
1874 if should_focus(&**panel, cx) {
1875 dock.set_open(true, cx);
1876 cx.focus(panel.as_any());
1877 reveal_dock = true;
1878 } else {
1879 // if panel.is_zoomed(cx) {
1880 // dock.set_open(false, cx);
1881 // }
1882 focus_center = true;
1883 }
1884 }
1885 panel
1886 });
1887
1888 if focus_center {
1889 cx.focus_self();
1890 }
1891
1892 self.serialize_workspace(cx);
1893 cx.notify();
1894 return panel;
1895 }
1896 }
1897 None
1898 }
1899
1900 pub fn panel<T: Panel>(&self, cx: &WindowContext) -> Option<ViewHandle<T>> {
1901 for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
1902 let dock = dock.read(cx);
1903 if let Some(panel) = dock.panel::<T>() {
1904 return Some(panel);
1905 }
1906 }
1907 None
1908 }
1909
1910 fn zoom_out(&mut self, cx: &mut ViewContext<Self>) {
1911 for pane in &self.panes {
1912 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
1913 }
1914
1915 self.left_dock.update(cx, |dock, cx| dock.zoom_out(cx));
1916 self.bottom_dock.update(cx, |dock, cx| dock.zoom_out(cx));
1917 self.right_dock.update(cx, |dock, cx| dock.zoom_out(cx));
1918 self.zoomed = None;
1919 self.zoomed_position = None;
1920
1921 cx.notify();
1922 }
1923
1924 #[cfg(any(test, feature = "test-support"))]
1925 pub fn zoomed_view(&self, cx: &AppContext) -> Option<AnyViewHandle> {
1926 self.zoomed.and_then(|view| view.upgrade(cx))
1927 }
1928
1929 fn dismiss_zoomed_items_to_reveal(
1930 &mut self,
1931 dock_to_reveal: Option<DockPosition>,
1932 cx: &mut ViewContext<Self>,
1933 ) {
1934 // If a center pane is zoomed, unzoom it.
1935 for pane in &self.panes {
1936 if pane != &self.active_pane || dock_to_reveal.is_some() {
1937 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
1938 }
1939 }
1940
1941 // If another dock is zoomed, hide it.
1942 let mut focus_center = false;
1943 for dock in [&self.left_dock, &self.right_dock, &self.bottom_dock] {
1944 dock.update(cx, |dock, cx| {
1945 if Some(dock.position()) != dock_to_reveal {
1946 if let Some(panel) = dock.active_panel() {
1947 if panel.is_zoomed(cx) {
1948 focus_center |= panel.has_focus(cx);
1949 dock.set_open(false, cx);
1950 }
1951 }
1952 }
1953 });
1954 }
1955
1956 if focus_center {
1957 cx.focus_self();
1958 }
1959
1960 if self.zoomed_position != dock_to_reveal {
1961 self.zoomed = None;
1962 self.zoomed_position = None;
1963 }
1964
1965 cx.notify();
1966 }
1967
1968 fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
1969 let pane = cx.add_view(|cx| {
1970 Pane::new(
1971 self.weak_handle(),
1972 self.project.clone(),
1973 self.app_state.background_actions,
1974 self.pane_history_timestamp.clone(),
1975 cx,
1976 )
1977 });
1978 cx.subscribe(&pane, Self::handle_pane_event).detach();
1979 self.panes.push(pane.clone());
1980 cx.focus(&pane);
1981 cx.emit(Event::PaneAdded(pane.clone()));
1982 pane
1983 }
1984
1985 pub fn add_item_to_center(
1986 &mut self,
1987 item: Box<dyn ItemHandle>,
1988 cx: &mut ViewContext<Self>,
1989 ) -> bool {
1990 if let Some(center_pane) = self.last_active_center_pane.clone() {
1991 if let Some(center_pane) = center_pane.upgrade(cx) {
1992 center_pane.update(cx, |pane, cx| pane.add_item(item, true, true, None, cx));
1993 true
1994 } else {
1995 false
1996 }
1997 } else {
1998 false
1999 }
2000 }
2001
2002 pub fn add_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
2003 self.active_pane
2004 .update(cx, |pane, cx| pane.add_item(item, true, true, None, cx));
2005 }
2006
2007 pub fn split_item(
2008 &mut self,
2009 split_direction: SplitDirection,
2010 item: Box<dyn ItemHandle>,
2011 cx: &mut ViewContext<Self>,
2012 ) {
2013 let new_pane = self.split_pane(self.active_pane.clone(), split_direction, cx);
2014 new_pane.update(cx, move |new_pane, cx| {
2015 new_pane.add_item(item, true, true, None, cx)
2016 })
2017 }
2018
2019 pub fn open_abs_path(
2020 &mut self,
2021 abs_path: PathBuf,
2022 visible: bool,
2023 cx: &mut ViewContext<Self>,
2024 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
2025 cx.spawn(|workspace, mut cx| async move {
2026 let open_paths_task_result = workspace
2027 .update(&mut cx, |workspace, cx| {
2028 workspace.open_paths(vec![abs_path.clone()], visible, cx)
2029 })
2030 .with_context(|| format!("open abs path {abs_path:?} task spawn"))?
2031 .await;
2032 anyhow::ensure!(
2033 open_paths_task_result.len() == 1,
2034 "open abs path {abs_path:?} task returned incorrect number of results"
2035 );
2036 match open_paths_task_result
2037 .into_iter()
2038 .next()
2039 .expect("ensured single task result")
2040 {
2041 Some(open_result) => {
2042 open_result.with_context(|| format!("open abs path {abs_path:?} task join"))
2043 }
2044 None => anyhow::bail!("open abs path {abs_path:?} task returned None"),
2045 }
2046 })
2047 }
2048
2049 pub fn split_abs_path(
2050 &mut self,
2051 abs_path: PathBuf,
2052 visible: bool,
2053 cx: &mut ViewContext<Self>,
2054 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
2055 let project_path_task =
2056 Workspace::project_path_for_path(self.project.clone(), &abs_path, visible, cx);
2057 cx.spawn(|this, mut cx| async move {
2058 let (_, path) = project_path_task.await?;
2059 this.update(&mut cx, |this, cx| this.split_path(path, cx))?
2060 .await
2061 })
2062 }
2063
2064 pub fn open_path(
2065 &mut self,
2066 path: impl Into<ProjectPath>,
2067 pane: Option<WeakViewHandle<Pane>>,
2068 focus_item: bool,
2069 cx: &mut ViewContext<Self>,
2070 ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
2071 let pane = pane.unwrap_or_else(|| {
2072 self.last_active_center_pane.clone().unwrap_or_else(|| {
2073 self.panes
2074 .first()
2075 .expect("There must be an active pane")
2076 .downgrade()
2077 })
2078 });
2079
2080 let task = self.load_path(path.into(), cx);
2081 cx.spawn(|_, mut cx| async move {
2082 let (project_entry_id, build_item) = task.await?;
2083 pane.update(&mut cx, |pane, cx| {
2084 pane.open_item(project_entry_id, focus_item, cx, build_item)
2085 })
2086 })
2087 }
2088
2089 pub fn split_path(
2090 &mut self,
2091 path: impl Into<ProjectPath>,
2092 cx: &mut ViewContext<Self>,
2093 ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
2094 let pane = self.last_active_center_pane.clone().unwrap_or_else(|| {
2095 self.panes
2096 .first()
2097 .expect("There must be an active pane")
2098 .downgrade()
2099 });
2100
2101 if let Member::Pane(center_pane) = &self.center.root {
2102 if center_pane.read(cx).items_len() == 0 {
2103 return self.open_path(path, Some(pane), true, cx);
2104 }
2105 }
2106
2107 let task = self.load_path(path.into(), cx);
2108 cx.spawn(|this, mut cx| async move {
2109 let (project_entry_id, build_item) = task.await?;
2110 this.update(&mut cx, move |this, cx| -> Option<_> {
2111 let pane = pane.upgrade(cx)?;
2112 let new_pane = this.split_pane(pane, SplitDirection::Right, cx);
2113 new_pane.update(cx, |new_pane, cx| {
2114 Some(new_pane.open_item(project_entry_id, true, cx, build_item))
2115 })
2116 })
2117 .map(|option| option.ok_or_else(|| anyhow!("pane was dropped")))?
2118 })
2119 }
2120
2121 pub(crate) fn load_path(
2122 &mut self,
2123 path: ProjectPath,
2124 cx: &mut ViewContext<Self>,
2125 ) -> Task<
2126 Result<(
2127 ProjectEntryId,
2128 impl 'static + FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
2129 )>,
2130 > {
2131 let project = self.project().clone();
2132 let project_item = project.update(cx, |project, cx| project.open_path(path, cx));
2133 cx.spawn(|_, mut cx| async move {
2134 let (project_entry_id, project_item) = project_item.await?;
2135 let build_item = cx.update(|cx| {
2136 cx.default_global::<ProjectItemBuilders>()
2137 .get(&project_item.model_type())
2138 .ok_or_else(|| anyhow!("no item builder for project item"))
2139 .cloned()
2140 })?;
2141 let build_item =
2142 move |cx: &mut ViewContext<Pane>| build_item(project, project_item, cx);
2143 Ok((project_entry_id, build_item))
2144 })
2145 }
2146
2147 pub fn open_project_item<T>(
2148 &mut self,
2149 project_item: ModelHandle<T::Item>,
2150 cx: &mut ViewContext<Self>,
2151 ) -> ViewHandle<T>
2152 where
2153 T: ProjectItem,
2154 {
2155 use project::Item as _;
2156
2157 let entry_id = project_item.read(cx).entry_id(cx);
2158 if let Some(item) = entry_id
2159 .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id, cx))
2160 .and_then(|item| item.downcast())
2161 {
2162 self.activate_item(&item, cx);
2163 return item;
2164 }
2165
2166 let item = cx.add_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
2167 self.add_item(Box::new(item.clone()), cx);
2168 item
2169 }
2170
2171 pub fn split_project_item<T>(
2172 &mut self,
2173 project_item: ModelHandle<T::Item>,
2174 cx: &mut ViewContext<Self>,
2175 ) -> ViewHandle<T>
2176 where
2177 T: ProjectItem,
2178 {
2179 use project::Item as _;
2180
2181 let entry_id = project_item.read(cx).entry_id(cx);
2182 if let Some(item) = entry_id
2183 .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id, cx))
2184 .and_then(|item| item.downcast())
2185 {
2186 self.activate_item(&item, cx);
2187 return item;
2188 }
2189
2190 let item = cx.add_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
2191 self.split_item(SplitDirection::Right, Box::new(item.clone()), cx);
2192 item
2193 }
2194
2195 pub fn open_shared_screen(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
2196 if let Some(shared_screen) = self.shared_screen_for_peer(peer_id, &self.active_pane, cx) {
2197 self.active_pane.update(cx, |pane, cx| {
2198 pane.add_item(Box::new(shared_screen), false, true, None, cx)
2199 });
2200 }
2201 }
2202
2203 pub fn activate_item(&mut self, item: &dyn ItemHandle, cx: &mut ViewContext<Self>) -> bool {
2204 let result = self.panes.iter().find_map(|pane| {
2205 pane.read(cx)
2206 .index_for_item(item)
2207 .map(|ix| (pane.clone(), ix))
2208 });
2209 if let Some((pane, ix)) = result {
2210 pane.update(cx, |pane, cx| pane.activate_item(ix, true, true, cx));
2211 true
2212 } else {
2213 false
2214 }
2215 }
2216
2217 fn activate_pane_at_index(&mut self, action: &ActivatePane, cx: &mut ViewContext<Self>) {
2218 let panes = self.center.panes();
2219 if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
2220 cx.focus(&pane);
2221 } else {
2222 self.split_and_clone(self.active_pane.clone(), SplitDirection::Right, cx);
2223 }
2224 }
2225
2226 pub fn activate_next_pane(&mut self, cx: &mut ViewContext<Self>) {
2227 let panes = self.center.panes();
2228 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
2229 let next_ix = (ix + 1) % panes.len();
2230 let next_pane = panes[next_ix].clone();
2231 cx.focus(&next_pane);
2232 }
2233 }
2234
2235 pub fn activate_previous_pane(&mut self, cx: &mut ViewContext<Self>) {
2236 let panes = self.center.panes();
2237 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
2238 let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
2239 let prev_pane = panes[prev_ix].clone();
2240 cx.focus(&prev_pane);
2241 }
2242 }
2243
2244 pub fn activate_pane_in_direction(
2245 &mut self,
2246 direction: SplitDirection,
2247 cx: &mut ViewContext<Self>,
2248 ) {
2249 if let Some(pane) = self.find_pane_in_direction(direction, cx) {
2250 cx.focus(pane);
2251 }
2252 }
2253
2254 pub fn swap_pane_in_direction(
2255 &mut self,
2256 direction: SplitDirection,
2257 cx: &mut ViewContext<Self>,
2258 ) {
2259 if let Some(to) = self
2260 .find_pane_in_direction(direction, cx)
2261 .map(|pane| pane.clone())
2262 {
2263 self.center.swap(&self.active_pane.clone(), &to);
2264 cx.notify();
2265 }
2266 }
2267
2268 fn find_pane_in_direction(
2269 &mut self,
2270 direction: SplitDirection,
2271 cx: &mut ViewContext<Self>,
2272 ) -> Option<&ViewHandle<Pane>> {
2273 let Some(bounding_box) = self.center.bounding_box_for_pane(&self.active_pane) else {
2274 return None;
2275 };
2276 let cursor = self.active_pane.read(cx).pixel_position_of_cursor(cx);
2277 let center = match cursor {
2278 Some(cursor) if bounding_box.contains_point(cursor) => cursor,
2279 _ => bounding_box.center(),
2280 };
2281
2282 let distance_to_next = theme::current(cx).workspace.pane_divider.width + 1.;
2283
2284 let target = match direction {
2285 SplitDirection::Left => vec2f(bounding_box.origin_x() - distance_to_next, center.y()),
2286 SplitDirection::Right => vec2f(bounding_box.max_x() + distance_to_next, center.y()),
2287 SplitDirection::Up => vec2f(center.x(), bounding_box.origin_y() - distance_to_next),
2288 SplitDirection::Down => vec2f(center.x(), bounding_box.max_y() + distance_to_next),
2289 };
2290 self.center.pane_at_pixel_position(target)
2291 }
2292
2293 fn handle_pane_focused(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
2294 if self.active_pane != pane {
2295 self.active_pane = pane.clone();
2296 self.status_bar.update(cx, |status_bar, cx| {
2297 status_bar.set_active_pane(&self.active_pane, cx);
2298 });
2299 self.active_item_path_changed(cx);
2300 self.last_active_center_pane = Some(pane.downgrade());
2301 }
2302
2303 self.dismiss_zoomed_items_to_reveal(None, cx);
2304 if pane.read(cx).is_zoomed() {
2305 self.zoomed = Some(pane.downgrade().into_any());
2306 } else {
2307 self.zoomed = None;
2308 }
2309 self.zoomed_position = None;
2310 self.update_active_view_for_followers(cx);
2311
2312 cx.notify();
2313 }
2314
2315 fn handle_pane_event(
2316 &mut self,
2317 pane: ViewHandle<Pane>,
2318 event: &pane::Event,
2319 cx: &mut ViewContext<Self>,
2320 ) {
2321 match event {
2322 pane::Event::AddItem { item } => item.added_to_pane(self, pane, cx),
2323 pane::Event::Split(direction) => {
2324 self.split_and_clone(pane, *direction, cx);
2325 }
2326 pane::Event::Remove => self.remove_pane(pane, cx),
2327 pane::Event::ActivateItem { local } => {
2328 if *local {
2329 self.unfollow(&pane, cx);
2330 }
2331 if &pane == self.active_pane() {
2332 self.active_item_path_changed(cx);
2333 }
2334 }
2335 pane::Event::ChangeItemTitle => {
2336 if pane == self.active_pane {
2337 self.active_item_path_changed(cx);
2338 }
2339 self.update_window_edited(cx);
2340 }
2341 pane::Event::RemoveItem { item_id } => {
2342 self.update_window_edited(cx);
2343 if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) {
2344 if entry.get().id() == pane.id() {
2345 entry.remove();
2346 }
2347 }
2348 }
2349 pane::Event::Focus => {
2350 self.handle_pane_focused(pane.clone(), cx);
2351 }
2352 pane::Event::ZoomIn => {
2353 if pane == self.active_pane {
2354 pane.update(cx, |pane, cx| pane.set_zoomed(true, cx));
2355 if pane.read(cx).has_focus() {
2356 self.zoomed = Some(pane.downgrade().into_any());
2357 self.zoomed_position = None;
2358 }
2359 cx.notify();
2360 }
2361 }
2362 pane::Event::ZoomOut => {
2363 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
2364 if self.zoomed_position.is_none() {
2365 self.zoomed = None;
2366 }
2367 cx.notify();
2368 }
2369 }
2370
2371 self.serialize_workspace(cx);
2372 }
2373
2374 pub fn split_pane(
2375 &mut self,
2376 pane_to_split: ViewHandle<Pane>,
2377 split_direction: SplitDirection,
2378 cx: &mut ViewContext<Self>,
2379 ) -> ViewHandle<Pane> {
2380 let new_pane = self.add_pane(cx);
2381 self.center
2382 .split(&pane_to_split, &new_pane, split_direction)
2383 .unwrap();
2384 cx.notify();
2385 new_pane
2386 }
2387
2388 pub fn split_and_clone(
2389 &mut self,
2390 pane: ViewHandle<Pane>,
2391 direction: SplitDirection,
2392 cx: &mut ViewContext<Self>,
2393 ) -> Option<ViewHandle<Pane>> {
2394 let item = pane.read(cx).active_item()?;
2395 let maybe_pane_handle = if let Some(clone) = item.clone_on_split(self.database_id(), cx) {
2396 let new_pane = self.add_pane(cx);
2397 new_pane.update(cx, |pane, cx| pane.add_item(clone, true, true, None, cx));
2398 self.center.split(&pane, &new_pane, direction).unwrap();
2399 Some(new_pane)
2400 } else {
2401 None
2402 };
2403 cx.notify();
2404 maybe_pane_handle
2405 }
2406
2407 pub fn split_pane_with_item(
2408 &mut self,
2409 pane_to_split: WeakViewHandle<Pane>,
2410 split_direction: SplitDirection,
2411 from: WeakViewHandle<Pane>,
2412 item_id_to_move: usize,
2413 cx: &mut ViewContext<Self>,
2414 ) {
2415 let Some(pane_to_split) = pane_to_split.upgrade(cx) else {
2416 return;
2417 };
2418 let Some(from) = from.upgrade(cx) else {
2419 return;
2420 };
2421
2422 let new_pane = self.add_pane(cx);
2423 self.move_item(from.clone(), new_pane.clone(), item_id_to_move, 0, cx);
2424 self.center
2425 .split(&pane_to_split, &new_pane, split_direction)
2426 .unwrap();
2427 cx.notify();
2428 }
2429
2430 pub fn split_pane_with_project_entry(
2431 &mut self,
2432 pane_to_split: WeakViewHandle<Pane>,
2433 split_direction: SplitDirection,
2434 project_entry: ProjectEntryId,
2435 cx: &mut ViewContext<Self>,
2436 ) -> Option<Task<Result<()>>> {
2437 let pane_to_split = pane_to_split.upgrade(cx)?;
2438 let new_pane = self.add_pane(cx);
2439 self.center
2440 .split(&pane_to_split, &new_pane, split_direction)
2441 .unwrap();
2442
2443 let path = self.project.read(cx).path_for_entry(project_entry, cx)?;
2444 let task = self.open_path(path, Some(new_pane.downgrade()), true, cx);
2445 Some(cx.foreground().spawn(async move {
2446 task.await?;
2447 Ok(())
2448 }))
2449 }
2450
2451 pub fn move_item(
2452 &mut self,
2453 source: ViewHandle<Pane>,
2454 destination: ViewHandle<Pane>,
2455 item_id_to_move: usize,
2456 destination_index: usize,
2457 cx: &mut ViewContext<Self>,
2458 ) {
2459 let item_to_move = source
2460 .read(cx)
2461 .items()
2462 .enumerate()
2463 .find(|(_, item_handle)| item_handle.id() == item_id_to_move);
2464
2465 if item_to_move.is_none() {
2466 log::warn!("Tried to move item handle which was not in `from` pane. Maybe tab was closed during drop");
2467 return;
2468 }
2469 let (item_ix, item_handle) = item_to_move.unwrap();
2470 let item_handle = item_handle.clone();
2471
2472 if source != destination {
2473 // Close item from previous pane
2474 source.update(cx, |source, cx| {
2475 source.remove_item(item_ix, false, cx);
2476 });
2477 }
2478
2479 // This automatically removes duplicate items in the pane
2480 destination.update(cx, |destination, cx| {
2481 destination.add_item(item_handle, true, true, Some(destination_index), cx);
2482 cx.focus_self();
2483 });
2484 }
2485
2486 fn remove_pane(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
2487 if self.center.remove(&pane).unwrap() {
2488 self.force_remove_pane(&pane, cx);
2489 self.unfollow(&pane, cx);
2490 self.last_leaders_by_pane.remove(&pane.downgrade());
2491 for removed_item in pane.read(cx).items() {
2492 self.panes_by_item.remove(&removed_item.id());
2493 }
2494
2495 cx.notify();
2496 } else {
2497 self.active_item_path_changed(cx);
2498 }
2499 }
2500
2501 pub fn panes(&self) -> &[ViewHandle<Pane>] {
2502 &self.panes
2503 }
2504
2505 pub fn active_pane(&self) -> &ViewHandle<Pane> {
2506 &self.active_pane
2507 }
2508
2509 fn project_remote_id_changed(&mut self, remote_id: Option<u64>, cx: &mut ViewContext<Self>) {
2510 if let Some(remote_id) = remote_id {
2511 self.remote_entity_subscription = Some(
2512 self.app_state
2513 .client
2514 .add_view_for_remote_entity(remote_id, cx),
2515 );
2516 } else {
2517 self.remote_entity_subscription.take();
2518 }
2519 }
2520
2521 fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
2522 self.leader_state.followers.remove(&peer_id);
2523 if let Some(states_by_pane) = self.follower_states_by_leader.remove(&peer_id) {
2524 for state in states_by_pane.into_values() {
2525 for item in state.items_by_leader_view_id.into_values() {
2526 item.set_leader_replica_id(None, cx);
2527 }
2528 }
2529 }
2530 cx.notify();
2531 }
2532
2533 pub fn toggle_follow(
2534 &mut self,
2535 leader_id: PeerId,
2536 cx: &mut ViewContext<Self>,
2537 ) -> Option<Task<Result<()>>> {
2538 let pane = self.active_pane().clone();
2539
2540 if let Some(prev_leader_id) = self.unfollow(&pane, cx) {
2541 if leader_id == prev_leader_id {
2542 return None;
2543 }
2544 }
2545
2546 self.last_leaders_by_pane
2547 .insert(pane.downgrade(), leader_id);
2548 self.follower_states_by_leader
2549 .entry(leader_id)
2550 .or_default()
2551 .insert(pane.clone(), Default::default());
2552 cx.notify();
2553
2554 let project_id = self.project.read(cx).remote_id()?;
2555 let request = self.app_state.client.request(proto::Follow {
2556 project_id,
2557 leader_id: Some(leader_id),
2558 });
2559
2560 Some(cx.spawn(|this, mut cx| async move {
2561 let response = request.await?;
2562 this.update(&mut cx, |this, _| {
2563 let state = this
2564 .follower_states_by_leader
2565 .get_mut(&leader_id)
2566 .and_then(|states_by_pane| states_by_pane.get_mut(&pane))
2567 .ok_or_else(|| anyhow!("following interrupted"))?;
2568 state.active_view_id = if let Some(active_view_id) = response.active_view_id {
2569 Some(ViewId::from_proto(active_view_id)?)
2570 } else {
2571 None
2572 };
2573 Ok::<_, anyhow::Error>(())
2574 })??;
2575 Self::add_views_from_leader(
2576 this.clone(),
2577 leader_id,
2578 vec![pane],
2579 response.views,
2580 &mut cx,
2581 )
2582 .await?;
2583 this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx))?;
2584 Ok(())
2585 }))
2586 }
2587
2588 pub fn follow_next_collaborator(
2589 &mut self,
2590 _: &FollowNextCollaborator,
2591 cx: &mut ViewContext<Self>,
2592 ) -> Option<Task<Result<()>>> {
2593 let collaborators = self.project.read(cx).collaborators();
2594 let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
2595 let mut collaborators = collaborators.keys().copied();
2596 for peer_id in collaborators.by_ref() {
2597 if peer_id == leader_id {
2598 break;
2599 }
2600 }
2601 collaborators.next()
2602 } else if let Some(last_leader_id) =
2603 self.last_leaders_by_pane.get(&self.active_pane.downgrade())
2604 {
2605 if collaborators.contains_key(last_leader_id) {
2606 Some(*last_leader_id)
2607 } else {
2608 None
2609 }
2610 } else {
2611 None
2612 };
2613
2614 next_leader_id
2615 .or_else(|| collaborators.keys().copied().next())
2616 .and_then(|leader_id| self.toggle_follow(leader_id, cx))
2617 }
2618
2619 pub fn unfollow(
2620 &mut self,
2621 pane: &ViewHandle<Pane>,
2622 cx: &mut ViewContext<Self>,
2623 ) -> Option<PeerId> {
2624 for (leader_id, states_by_pane) in &mut self.follower_states_by_leader {
2625 let leader_id = *leader_id;
2626 if let Some(state) = states_by_pane.remove(pane) {
2627 for (_, item) in state.items_by_leader_view_id {
2628 item.set_leader_replica_id(None, cx);
2629 }
2630
2631 if states_by_pane.is_empty() {
2632 self.follower_states_by_leader.remove(&leader_id);
2633 if let Some(project_id) = self.project.read(cx).remote_id() {
2634 self.app_state
2635 .client
2636 .send(proto::Unfollow {
2637 project_id,
2638 leader_id: Some(leader_id),
2639 })
2640 .log_err();
2641 }
2642 }
2643
2644 cx.notify();
2645 return Some(leader_id);
2646 }
2647 }
2648 None
2649 }
2650
2651 pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
2652 self.follower_states_by_leader.contains_key(&peer_id)
2653 }
2654
2655 pub fn is_followed_by(&self, peer_id: PeerId) -> bool {
2656 self.leader_state.followers.contains(&peer_id)
2657 }
2658
2659 fn render_titlebar(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
2660 // TODO: There should be a better system in place for this
2661 // (https://github.com/zed-industries/zed/issues/1290)
2662 let is_fullscreen = cx.window_is_fullscreen();
2663 let container_theme = if is_fullscreen {
2664 let mut container_theme = theme.titlebar.container;
2665 container_theme.padding.left = container_theme.padding.right;
2666 container_theme
2667 } else {
2668 theme.titlebar.container
2669 };
2670
2671 enum TitleBar {}
2672 MouseEventHandler::new::<TitleBar, _>(0, cx, |_, cx| {
2673 Stack::new()
2674 .with_children(
2675 self.titlebar_item
2676 .as_ref()
2677 .map(|item| ChildView::new(item, cx)),
2678 )
2679 .contained()
2680 .with_style(container_theme)
2681 })
2682 .on_click(MouseButton::Left, |event, _, cx| {
2683 if event.click_count == 2 {
2684 cx.zoom_window();
2685 }
2686 })
2687 .constrained()
2688 .with_height(theme.titlebar.height)
2689 .into_any_named("titlebar")
2690 }
2691
2692 fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
2693 let active_entry = self.active_project_path(cx);
2694 self.project
2695 .update(cx, |project, cx| project.set_active_path(active_entry, cx));
2696 self.update_window_title(cx);
2697 }
2698
2699 fn update_window_title(&mut self, cx: &mut ViewContext<Self>) {
2700 let project = self.project().read(cx);
2701 let mut title = String::new();
2702
2703 if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
2704 let filename = path
2705 .path
2706 .file_name()
2707 .map(|s| s.to_string_lossy())
2708 .or_else(|| {
2709 Some(Cow::Borrowed(
2710 project
2711 .worktree_for_id(path.worktree_id, cx)?
2712 .read(cx)
2713 .root_name(),
2714 ))
2715 });
2716
2717 if let Some(filename) = filename {
2718 title.push_str(filename.as_ref());
2719 title.push_str(" β ");
2720 }
2721 }
2722
2723 for (i, name) in project.worktree_root_names(cx).enumerate() {
2724 if i > 0 {
2725 title.push_str(", ");
2726 }
2727 title.push_str(name);
2728 }
2729
2730 if title.is_empty() {
2731 title = "empty project".to_string();
2732 }
2733
2734 if project.is_remote() {
2735 title.push_str(" β");
2736 } else if project.is_shared() {
2737 title.push_str(" β");
2738 }
2739
2740 cx.set_window_title(&title);
2741 }
2742
2743 fn update_window_edited(&mut self, cx: &mut ViewContext<Self>) {
2744 let is_edited = !self.project.read(cx).is_read_only()
2745 && self
2746 .items(cx)
2747 .any(|item| item.has_conflict(cx) || item.is_dirty(cx));
2748 if is_edited != self.window_edited {
2749 self.window_edited = is_edited;
2750 cx.set_window_edited(self.window_edited)
2751 }
2752 }
2753
2754 fn render_disconnected_overlay(
2755 &self,
2756 cx: &mut ViewContext<Workspace>,
2757 ) -> Option<AnyElement<Workspace>> {
2758 if self.project.read(cx).is_read_only() {
2759 enum DisconnectedOverlay {}
2760 Some(
2761 MouseEventHandler::new::<DisconnectedOverlay, _>(0, cx, |_, cx| {
2762 let theme = &theme::current(cx);
2763 Label::new(
2764 "Your connection to the remote project has been lost.",
2765 theme.workspace.disconnected_overlay.text.clone(),
2766 )
2767 .aligned()
2768 .contained()
2769 .with_style(theme.workspace.disconnected_overlay.container)
2770 })
2771 .with_cursor_style(CursorStyle::Arrow)
2772 .capture_all()
2773 .into_any_named("disconnected overlay"),
2774 )
2775 } else {
2776 None
2777 }
2778 }
2779
2780 fn render_notifications(
2781 &self,
2782 theme: &theme::Workspace,
2783 cx: &AppContext,
2784 ) -> Option<AnyElement<Workspace>> {
2785 if self.notifications.is_empty() {
2786 None
2787 } else {
2788 Some(
2789 Flex::column()
2790 .with_children(self.notifications.iter().map(|(_, _, notification)| {
2791 ChildView::new(notification.as_any(), cx)
2792 .contained()
2793 .with_style(theme.notification)
2794 }))
2795 .constrained()
2796 .with_width(theme.notifications.width)
2797 .contained()
2798 .with_style(theme.notifications.container)
2799 .aligned()
2800 .bottom()
2801 .right()
2802 .into_any(),
2803 )
2804 }
2805 }
2806
2807 // RPC handlers
2808
2809 async fn handle_follow(
2810 this: WeakViewHandle<Self>,
2811 envelope: TypedEnvelope<proto::Follow>,
2812 _: Arc<Client>,
2813 mut cx: AsyncAppContext,
2814 ) -> Result<proto::FollowResponse> {
2815 this.update(&mut cx, |this, cx| {
2816 let client = &this.app_state.client;
2817 this.leader_state
2818 .followers
2819 .insert(envelope.original_sender_id()?);
2820
2821 let active_view_id = this.active_item(cx).and_then(|i| {
2822 Some(
2823 i.to_followable_item_handle(cx)?
2824 .remote_id(client, cx)?
2825 .to_proto(),
2826 )
2827 });
2828
2829 cx.notify();
2830
2831 Ok(proto::FollowResponse {
2832 active_view_id,
2833 views: this
2834 .panes()
2835 .iter()
2836 .flat_map(|pane| {
2837 let leader_id = this.leader_for_pane(pane);
2838 pane.read(cx).items().filter_map({
2839 let cx = &cx;
2840 move |item| {
2841 let item = item.to_followable_item_handle(cx)?;
2842 let id = item.remote_id(client, cx)?.to_proto();
2843 let variant = item.to_state_proto(cx)?;
2844 Some(proto::View {
2845 id: Some(id),
2846 leader_id,
2847 variant: Some(variant),
2848 })
2849 }
2850 })
2851 })
2852 .collect(),
2853 })
2854 })?
2855 }
2856
2857 async fn handle_unfollow(
2858 this: WeakViewHandle<Self>,
2859 envelope: TypedEnvelope<proto::Unfollow>,
2860 _: Arc<Client>,
2861 mut cx: AsyncAppContext,
2862 ) -> Result<()> {
2863 this.update(&mut cx, |this, cx| {
2864 this.leader_state
2865 .followers
2866 .remove(&envelope.original_sender_id()?);
2867 cx.notify();
2868 Ok(())
2869 })?
2870 }
2871
2872 async fn handle_update_followers(
2873 this: WeakViewHandle<Self>,
2874 envelope: TypedEnvelope<proto::UpdateFollowers>,
2875 _: Arc<Client>,
2876 cx: AsyncAppContext,
2877 ) -> Result<()> {
2878 let leader_id = envelope.original_sender_id()?;
2879 this.read_with(&cx, |this, _| {
2880 this.leader_updates_tx
2881 .unbounded_send((leader_id, envelope.payload))
2882 })??;
2883 Ok(())
2884 }
2885
2886 async fn process_leader_update(
2887 this: &WeakViewHandle<Self>,
2888 leader_id: PeerId,
2889 update: proto::UpdateFollowers,
2890 cx: &mut AsyncAppContext,
2891 ) -> Result<()> {
2892 match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
2893 proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
2894 this.update(cx, |this, _| {
2895 if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
2896 for state in state.values_mut() {
2897 state.active_view_id =
2898 if let Some(active_view_id) = update_active_view.id.clone() {
2899 Some(ViewId::from_proto(active_view_id)?)
2900 } else {
2901 None
2902 };
2903 }
2904 }
2905 anyhow::Ok(())
2906 })??;
2907 }
2908 proto::update_followers::Variant::UpdateView(update_view) => {
2909 let variant = update_view
2910 .variant
2911 .ok_or_else(|| anyhow!("missing update view variant"))?;
2912 let id = update_view
2913 .id
2914 .ok_or_else(|| anyhow!("missing update view id"))?;
2915 let mut tasks = Vec::new();
2916 this.update(cx, |this, cx| {
2917 let project = this.project.clone();
2918 if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
2919 for state in state.values_mut() {
2920 let view_id = ViewId::from_proto(id.clone())?;
2921 if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
2922 tasks.push(item.apply_update_proto(&project, variant.clone(), cx));
2923 }
2924 }
2925 }
2926 anyhow::Ok(())
2927 })??;
2928 try_join_all(tasks).await.log_err();
2929 }
2930 proto::update_followers::Variant::CreateView(view) => {
2931 let panes = this.read_with(cx, |this, _| {
2932 this.follower_states_by_leader
2933 .get(&leader_id)
2934 .into_iter()
2935 .flat_map(|states_by_pane| states_by_pane.keys())
2936 .cloned()
2937 .collect()
2938 })?;
2939 Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], cx).await?;
2940 }
2941 }
2942 this.update(cx, |this, cx| this.leader_updated(leader_id, cx))?;
2943 Ok(())
2944 }
2945
2946 async fn add_views_from_leader(
2947 this: WeakViewHandle<Self>,
2948 leader_id: PeerId,
2949 panes: Vec<ViewHandle<Pane>>,
2950 views: Vec<proto::View>,
2951 cx: &mut AsyncAppContext,
2952 ) -> Result<()> {
2953 let this = this
2954 .upgrade(cx)
2955 .ok_or_else(|| anyhow!("workspace dropped"))?;
2956 let project = this
2957 .read_with(cx, |this, _| this.project.clone())
2958 .ok_or_else(|| anyhow!("window dropped"))?;
2959
2960 let replica_id = project
2961 .read_with(cx, |project, _| {
2962 project
2963 .collaborators()
2964 .get(&leader_id)
2965 .map(|c| c.replica_id)
2966 })
2967 .ok_or_else(|| anyhow!("no such collaborator {}", leader_id))?;
2968
2969 let item_builders = cx.update(|cx| {
2970 cx.default_global::<FollowableItemBuilders>()
2971 .values()
2972 .map(|b| b.0)
2973 .collect::<Vec<_>>()
2974 });
2975
2976 let mut item_tasks_by_pane = HashMap::default();
2977 for pane in panes {
2978 let mut item_tasks = Vec::new();
2979 let mut leader_view_ids = Vec::new();
2980 for view in &views {
2981 let Some(id) = &view.id else { continue };
2982 let id = ViewId::from_proto(id.clone())?;
2983 let mut variant = view.variant.clone();
2984 if variant.is_none() {
2985 Err(anyhow!("missing view variant"))?;
2986 }
2987 for build_item in &item_builders {
2988 let task = cx
2989 .update(|cx| build_item(pane.clone(), this.clone(), id, &mut variant, cx));
2990 if let Some(task) = task {
2991 item_tasks.push(task);
2992 leader_view_ids.push(id);
2993 break;
2994 } else {
2995 assert!(variant.is_some());
2996 }
2997 }
2998 }
2999
3000 item_tasks_by_pane.insert(pane, (item_tasks, leader_view_ids));
3001 }
3002
3003 for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane {
3004 let items = futures::future::try_join_all(item_tasks).await?;
3005 this.update(cx, |this, cx| {
3006 let state = this
3007 .follower_states_by_leader
3008 .get_mut(&leader_id)?
3009 .get_mut(&pane)?;
3010
3011 for (id, item) in leader_view_ids.into_iter().zip(items) {
3012 item.set_leader_replica_id(Some(replica_id), cx);
3013 state.items_by_leader_view_id.insert(id, item);
3014 }
3015
3016 Some(())
3017 });
3018 }
3019 Ok(())
3020 }
3021
3022 fn update_active_view_for_followers(&self, cx: &AppContext) {
3023 if self.active_pane.read(cx).has_focus() {
3024 self.update_followers(
3025 proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView {
3026 id: self.active_item(cx).and_then(|item| {
3027 item.to_followable_item_handle(cx)?
3028 .remote_id(&self.app_state.client, cx)
3029 .map(|id| id.to_proto())
3030 }),
3031 leader_id: self.leader_for_pane(&self.active_pane),
3032 }),
3033 cx,
3034 );
3035 } else {
3036 self.update_followers(
3037 proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView {
3038 id: None,
3039 leader_id: None,
3040 }),
3041 cx,
3042 );
3043 }
3044 }
3045
3046 fn update_followers(
3047 &self,
3048 update: proto::update_followers::Variant,
3049 cx: &AppContext,
3050 ) -> Option<()> {
3051 let project_id = self.project.read(cx).remote_id()?;
3052 if !self.leader_state.followers.is_empty() {
3053 self.app_state
3054 .client
3055 .send(proto::UpdateFollowers {
3056 project_id,
3057 follower_ids: self.leader_state.followers.iter().copied().collect(),
3058 variant: Some(update),
3059 })
3060 .log_err();
3061 }
3062 None
3063 }
3064
3065 pub fn leader_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<PeerId> {
3066 self.follower_states_by_leader
3067 .iter()
3068 .find_map(|(leader_id, state)| {
3069 if state.contains_key(pane) {
3070 Some(*leader_id)
3071 } else {
3072 None
3073 }
3074 })
3075 }
3076
3077 fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
3078 cx.notify();
3079
3080 let call = self.active_call()?;
3081 let room = call.read(cx).room()?.read(cx);
3082 let participant = room.remote_participant_for_peer_id(leader_id)?;
3083 let mut items_to_activate = Vec::new();
3084 match participant.location {
3085 call::ParticipantLocation::SharedProject { project_id } => {
3086 if Some(project_id) == self.project.read(cx).remote_id() {
3087 for (pane, state) in self.follower_states_by_leader.get(&leader_id)? {
3088 if let Some(item) = state
3089 .active_view_id
3090 .and_then(|id| state.items_by_leader_view_id.get(&id))
3091 {
3092 items_to_activate.push((pane.clone(), item.boxed_clone()));
3093 } else if let Some(shared_screen) =
3094 self.shared_screen_for_peer(leader_id, pane, cx)
3095 {
3096 items_to_activate.push((pane.clone(), Box::new(shared_screen)));
3097 }
3098 }
3099 }
3100 }
3101 call::ParticipantLocation::UnsharedProject => {}
3102 call::ParticipantLocation::External => {
3103 for (pane, _) in self.follower_states_by_leader.get(&leader_id)? {
3104 if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
3105 items_to_activate.push((pane.clone(), Box::new(shared_screen)));
3106 }
3107 }
3108 }
3109 }
3110
3111 for (pane, item) in items_to_activate {
3112 let pane_was_focused = pane.read(cx).has_focus();
3113 if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) {
3114 pane.update(cx, |pane, cx| pane.activate_item(index, false, false, cx));
3115 } else {
3116 pane.update(cx, |pane, cx| {
3117 pane.add_item(item.boxed_clone(), false, false, None, cx)
3118 });
3119 }
3120
3121 if pane_was_focused {
3122 pane.update(cx, |pane, cx| pane.focus_active_item(cx));
3123 }
3124 }
3125
3126 None
3127 }
3128
3129 fn shared_screen_for_peer(
3130 &self,
3131 peer_id: PeerId,
3132 pane: &ViewHandle<Pane>,
3133 cx: &mut ViewContext<Self>,
3134 ) -> Option<ViewHandle<SharedScreen>> {
3135 let call = self.active_call()?;
3136 let room = call.read(cx).room()?.read(cx);
3137 let participant = room.remote_participant_for_peer_id(peer_id)?;
3138 let track = participant.video_tracks.values().next()?.clone();
3139 let user = participant.user.clone();
3140
3141 for item in pane.read(cx).items_of_type::<SharedScreen>() {
3142 if item.read(cx).peer_id == peer_id {
3143 return Some(item);
3144 }
3145 }
3146
3147 Some(cx.add_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx)))
3148 }
3149
3150 pub fn on_window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
3151 if active {
3152 cx.background()
3153 .spawn(persistence::DB.update_timestamp(self.database_id()))
3154 .detach();
3155 } else {
3156 for pane in &self.panes {
3157 pane.update(cx, |pane, cx| {
3158 if let Some(item) = pane.active_item() {
3159 item.workspace_deactivated(cx);
3160 }
3161 if matches!(
3162 settings::get::<WorkspaceSettings>(cx).autosave,
3163 AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
3164 ) {
3165 for item in pane.items() {
3166 Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
3167 .detach_and_log_err(cx);
3168 }
3169 }
3170 });
3171 }
3172 }
3173 }
3174
3175 fn active_call(&self) -> Option<&ModelHandle<ActiveCall>> {
3176 self.active_call.as_ref().map(|(call, _)| call)
3177 }
3178
3179 fn on_active_call_event(
3180 &mut self,
3181 _: ModelHandle<ActiveCall>,
3182 event: &call::room::Event,
3183 cx: &mut ViewContext<Self>,
3184 ) {
3185 match event {
3186 call::room::Event::ParticipantLocationChanged { participant_id }
3187 | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
3188 self.leader_updated(*participant_id, cx);
3189 }
3190 _ => {}
3191 }
3192 }
3193
3194 pub fn database_id(&self) -> WorkspaceId {
3195 self.database_id
3196 }
3197
3198 fn location(&self, cx: &AppContext) -> Option<WorkspaceLocation> {
3199 let project = self.project().read(cx);
3200
3201 if project.is_local() {
3202 Some(
3203 project
3204 .visible_worktrees(cx)
3205 .map(|worktree| worktree.read(cx).abs_path())
3206 .collect::<Vec<_>>()
3207 .into(),
3208 )
3209 } else {
3210 None
3211 }
3212 }
3213
3214 fn remove_panes(&mut self, member: Member, cx: &mut ViewContext<Workspace>) {
3215 match member {
3216 Member::Axis(PaneAxis { members, .. }) => {
3217 for child in members.iter() {
3218 self.remove_panes(child.clone(), cx)
3219 }
3220 }
3221 Member::Pane(pane) => {
3222 self.force_remove_pane(&pane, cx);
3223 }
3224 }
3225 }
3226
3227 fn force_remove_pane(&mut self, pane: &ViewHandle<Pane>, cx: &mut ViewContext<Workspace>) {
3228 self.panes.retain(|p| p != pane);
3229 cx.focus(self.panes.last().unwrap());
3230 if self.last_active_center_pane == Some(pane.downgrade()) {
3231 self.last_active_center_pane = None;
3232 }
3233 cx.notify();
3234 }
3235
3236 fn schedule_serialize(&mut self, cx: &mut ViewContext<Self>) {
3237 self._schedule_serialize = Some(cx.spawn(|this, cx| async move {
3238 cx.background().timer(Duration::from_millis(100)).await;
3239 this.read_with(&cx, |this, cx| this.serialize_workspace(cx))
3240 .ok();
3241 }));
3242 }
3243
3244 fn serialize_workspace(&self, cx: &ViewContext<Self>) {
3245 fn serialize_pane_handle(
3246 pane_handle: &ViewHandle<Pane>,
3247 cx: &AppContext,
3248 ) -> SerializedPane {
3249 let (items, active) = {
3250 let pane = pane_handle.read(cx);
3251 let active_item_id = pane.active_item().map(|item| item.id());
3252 (
3253 pane.items()
3254 .filter_map(|item_handle| {
3255 Some(SerializedItem {
3256 kind: Arc::from(item_handle.serialized_item_kind()?),
3257 item_id: item_handle.id(),
3258 active: Some(item_handle.id()) == active_item_id,
3259 })
3260 })
3261 .collect::<Vec<_>>(),
3262 pane.has_focus(),
3263 )
3264 };
3265
3266 SerializedPane::new(items, active)
3267 }
3268
3269 fn build_serialized_pane_group(
3270 pane_group: &Member,
3271 cx: &AppContext,
3272 ) -> SerializedPaneGroup {
3273 match pane_group {
3274 Member::Axis(PaneAxis {
3275 axis,
3276 members,
3277 flexes,
3278 bounding_boxes: _,
3279 }) => SerializedPaneGroup::Group {
3280 axis: *axis,
3281 children: members
3282 .iter()
3283 .map(|member| build_serialized_pane_group(member, cx))
3284 .collect::<Vec<_>>(),
3285 flexes: Some(flexes.borrow().clone()),
3286 },
3287 Member::Pane(pane_handle) => {
3288 SerializedPaneGroup::Pane(serialize_pane_handle(&pane_handle, cx))
3289 }
3290 }
3291 }
3292
3293 fn build_serialized_docks(this: &Workspace, cx: &ViewContext<Workspace>) -> DockStructure {
3294 let left_dock = this.left_dock.read(cx);
3295 let left_visible = left_dock.is_open();
3296 let left_active_panel = left_dock.visible_panel().and_then(|panel| {
3297 Some(
3298 cx.view_ui_name(panel.as_any().window(), panel.id())?
3299 .to_string(),
3300 )
3301 });
3302 let left_dock_zoom = left_dock
3303 .visible_panel()
3304 .map(|panel| panel.is_zoomed(cx))
3305 .unwrap_or(false);
3306
3307 let right_dock = this.right_dock.read(cx);
3308 let right_visible = right_dock.is_open();
3309 let right_active_panel = right_dock.visible_panel().and_then(|panel| {
3310 Some(
3311 cx.view_ui_name(panel.as_any().window(), panel.id())?
3312 .to_string(),
3313 )
3314 });
3315 let right_dock_zoom = right_dock
3316 .visible_panel()
3317 .map(|panel| panel.is_zoomed(cx))
3318 .unwrap_or(false);
3319
3320 let bottom_dock = this.bottom_dock.read(cx);
3321 let bottom_visible = bottom_dock.is_open();
3322 let bottom_active_panel = bottom_dock.visible_panel().and_then(|panel| {
3323 Some(
3324 cx.view_ui_name(panel.as_any().window(), panel.id())?
3325 .to_string(),
3326 )
3327 });
3328 let bottom_dock_zoom = bottom_dock
3329 .visible_panel()
3330 .map(|panel| panel.is_zoomed(cx))
3331 .unwrap_or(false);
3332
3333 DockStructure {
3334 left: DockData {
3335 visible: left_visible,
3336 active_panel: left_active_panel,
3337 zoom: left_dock_zoom,
3338 },
3339 right: DockData {
3340 visible: right_visible,
3341 active_panel: right_active_panel,
3342 zoom: right_dock_zoom,
3343 },
3344 bottom: DockData {
3345 visible: bottom_visible,
3346 active_panel: bottom_active_panel,
3347 zoom: bottom_dock_zoom,
3348 },
3349 }
3350 }
3351
3352 if let Some(location) = self.location(cx) {
3353 // Load bearing special case:
3354 // - with_local_workspace() relies on this to not have other stuff open
3355 // when you open your log
3356 if !location.paths().is_empty() {
3357 let center_group = build_serialized_pane_group(&self.center.root, cx);
3358 let docks = build_serialized_docks(self, cx);
3359
3360 let serialized_workspace = SerializedWorkspace {
3361 id: self.database_id,
3362 location,
3363 center_group,
3364 bounds: Default::default(),
3365 display: Default::default(),
3366 docks,
3367 };
3368
3369 cx.background()
3370 .spawn(persistence::DB.save_workspace(serialized_workspace))
3371 .detach();
3372 }
3373 }
3374 }
3375
3376 pub(crate) fn load_workspace(
3377 workspace: WeakViewHandle<Workspace>,
3378 serialized_workspace: SerializedWorkspace,
3379 paths_to_open: Vec<Option<ProjectPath>>,
3380 cx: &mut AppContext,
3381 ) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
3382 cx.spawn(|mut cx| async move {
3383 let result = async_iife! {{
3384 let (project, old_center_pane) =
3385 workspace.read_with(&cx, |workspace, _| {
3386 (
3387 workspace.project().clone(),
3388 workspace.last_active_center_pane.clone(),
3389 )
3390 })?;
3391
3392 let mut center_items = None;
3393 let mut center_group = None;
3394 // Traverse the splits tree and add to things
3395 if let Some((group, active_pane, items)) = serialized_workspace
3396 .center_group
3397 .deserialize(&project, serialized_workspace.id, &workspace, &mut cx)
3398 .await {
3399 center_items = Some(items);
3400 center_group = Some((group, active_pane))
3401 }
3402
3403 let resulting_list = cx.read(|cx| {
3404 let mut opened_items = center_items
3405 .unwrap_or_default()
3406 .into_iter()
3407 .filter_map(|item| {
3408 let item = item?;
3409 let project_path = item.project_path(cx)?;
3410 Some((project_path, item))
3411 })
3412 .collect::<HashMap<_, _>>();
3413
3414 paths_to_open
3415 .into_iter()
3416 .map(|path_to_open| {
3417 path_to_open.map(|path_to_open| {
3418 Ok(opened_items.remove(&path_to_open))
3419 })
3420 .transpose()
3421 .map(|item| item.flatten())
3422 .transpose()
3423 })
3424 .collect::<Vec<_>>()
3425 });
3426
3427 // Remove old panes from workspace panes list
3428 workspace.update(&mut cx, |workspace, cx| {
3429 if let Some((center_group, active_pane)) = center_group {
3430 workspace.remove_panes(workspace.center.root.clone(), cx);
3431
3432 // Swap workspace center group
3433 workspace.center = PaneGroup::with_root(center_group);
3434
3435 // Change the focus to the workspace first so that we retrigger focus in on the pane.
3436 cx.focus_self();
3437
3438 if let Some(active_pane) = active_pane {
3439 cx.focus(&active_pane);
3440 } else {
3441 cx.focus(workspace.panes.last().unwrap());
3442 }
3443 } else {
3444 let old_center_handle = old_center_pane.and_then(|weak| weak.upgrade(cx));
3445 if let Some(old_center_handle) = old_center_handle {
3446 cx.focus(&old_center_handle)
3447 } else {
3448 cx.focus_self()
3449 }
3450 }
3451
3452 let docks = serialized_workspace.docks;
3453 workspace.left_dock.update(cx, |dock, cx| {
3454 dock.set_open(docks.left.visible, cx);
3455 if let Some(active_panel) = docks.left.active_panel {
3456 if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
3457 dock.activate_panel(ix, cx);
3458 }
3459 }
3460 dock.active_panel()
3461 .map(|panel| {
3462 panel.set_zoomed(docks.left.zoom, cx)
3463 });
3464 if docks.left.visible && docks.left.zoom {
3465 cx.focus_self()
3466 }
3467 });
3468 // TODO: I think the bug is that setting zoom or active undoes the bottom zoom or something
3469 workspace.right_dock.update(cx, |dock, cx| {
3470 dock.set_open(docks.right.visible, cx);
3471 if let Some(active_panel) = docks.right.active_panel {
3472 if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
3473 dock.activate_panel(ix, cx);
3474
3475 }
3476 }
3477 dock.active_panel()
3478 .map(|panel| {
3479 panel.set_zoomed(docks.right.zoom, cx)
3480 });
3481
3482 if docks.right.visible && docks.right.zoom {
3483 cx.focus_self()
3484 }
3485 });
3486 workspace.bottom_dock.update(cx, |dock, cx| {
3487 dock.set_open(docks.bottom.visible, cx);
3488 if let Some(active_panel) = docks.bottom.active_panel {
3489 if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
3490 dock.activate_panel(ix, cx);
3491 }
3492 }
3493
3494 dock.active_panel()
3495 .map(|panel| {
3496 panel.set_zoomed(docks.bottom.zoom, cx)
3497 });
3498
3499 if docks.bottom.visible && docks.bottom.zoom {
3500 cx.focus_self()
3501 }
3502 });
3503
3504
3505 cx.notify();
3506 })?;
3507
3508 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
3509 workspace.read_with(&cx, |workspace, cx| workspace.serialize_workspace(cx))?;
3510
3511 Ok::<_, anyhow::Error>(resulting_list)
3512 }};
3513
3514 result.await.unwrap_or_default()
3515 })
3516 }
3517
3518 #[cfg(any(test, feature = "test-support"))]
3519 pub fn test_new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
3520 let client = project.read(cx).client();
3521 let user_store = project.read(cx).user_store();
3522
3523 let channel_store =
3524 cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
3525 let app_state = Arc::new(AppState {
3526 languages: project.read(cx).languages().clone(),
3527 client,
3528 user_store,
3529 channel_store,
3530 fs: project.read(cx).fs().clone(),
3531 build_window_options: |_, _, _| Default::default(),
3532 initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
3533 background_actions: || &[],
3534 });
3535 Self::new(0, project, app_state, cx)
3536 }
3537
3538 fn render_dock(&self, position: DockPosition, cx: &WindowContext) -> Option<AnyElement<Self>> {
3539 let dock = match position {
3540 DockPosition::Left => &self.left_dock,
3541 DockPosition::Right => &self.right_dock,
3542 DockPosition::Bottom => &self.bottom_dock,
3543 };
3544 let active_panel = dock.read(cx).visible_panel()?;
3545 let element = if Some(active_panel.id()) == self.zoomed.as_ref().map(|zoomed| zoomed.id()) {
3546 dock.read(cx).render_placeholder(cx)
3547 } else {
3548 ChildView::new(dock, cx).into_any()
3549 };
3550
3551 Some(
3552 element
3553 .constrained()
3554 .dynamically(move |constraint, _, cx| match position {
3555 DockPosition::Left | DockPosition::Right => SizeConstraint::new(
3556 Vector2F::new(20., constraint.min.y()),
3557 Vector2F::new(cx.window_size().x() * 0.8, constraint.max.y()),
3558 ),
3559 DockPosition::Bottom => SizeConstraint::new(
3560 Vector2F::new(constraint.min.x(), 20.),
3561 Vector2F::new(constraint.max.x(), cx.window_size().y() * 0.8),
3562 ),
3563 })
3564 .into_any(),
3565 )
3566 }
3567}
3568
3569fn window_bounds_env_override(cx: &AsyncAppContext) -> Option<WindowBounds> {
3570 ZED_WINDOW_POSITION
3571 .zip(*ZED_WINDOW_SIZE)
3572 .map(|(position, size)| {
3573 WindowBounds::Fixed(RectF::new(
3574 cx.platform().screens()[0].bounds().origin() + position,
3575 size,
3576 ))
3577 })
3578}
3579
3580async fn open_items(
3581 serialized_workspace: Option<SerializedWorkspace>,
3582 workspace: &WeakViewHandle<Workspace>,
3583 mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
3584 app_state: Arc<AppState>,
3585 mut cx: AsyncAppContext,
3586) -> Vec<Option<anyhow::Result<Box<dyn ItemHandle>>>> {
3587 let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
3588
3589 if let Some(serialized_workspace) = serialized_workspace {
3590 let workspace = workspace.clone();
3591 let restored_items = cx
3592 .update(|cx| {
3593 Workspace::load_workspace(
3594 workspace,
3595 serialized_workspace,
3596 project_paths_to_open
3597 .iter()
3598 .map(|(_, project_path)| project_path)
3599 .cloned()
3600 .collect(),
3601 cx,
3602 )
3603 })
3604 .await;
3605
3606 let restored_project_paths = cx.read(|cx| {
3607 restored_items
3608 .iter()
3609 .filter_map(|item| item.as_ref()?.as_ref().ok()?.project_path(cx))
3610 .collect::<HashSet<_>>()
3611 });
3612
3613 opened_items = restored_items;
3614 project_paths_to_open
3615 .iter_mut()
3616 .for_each(|(_, project_path)| {
3617 if let Some(project_path_to_open) = project_path {
3618 if restored_project_paths.contains(project_path_to_open) {
3619 *project_path = None;
3620 }
3621 }
3622 });
3623 } else {
3624 for _ in 0..project_paths_to_open.len() {
3625 opened_items.push(None);
3626 }
3627 }
3628 assert!(opened_items.len() == project_paths_to_open.len());
3629
3630 let tasks =
3631 project_paths_to_open
3632 .into_iter()
3633 .enumerate()
3634 .map(|(i, (abs_path, project_path))| {
3635 let workspace = workspace.clone();
3636 cx.spawn(|mut cx| {
3637 let fs = app_state.fs.clone();
3638 async move {
3639 let file_project_path = project_path?;
3640 if fs.is_file(&abs_path).await {
3641 Some((
3642 i,
3643 workspace
3644 .update(&mut cx, |workspace, cx| {
3645 workspace.open_path(file_project_path, None, true, cx)
3646 })
3647 .log_err()?
3648 .await,
3649 ))
3650 } else {
3651 None
3652 }
3653 }
3654 })
3655 });
3656
3657 for maybe_opened_path in futures::future::join_all(tasks.into_iter())
3658 .await
3659 .into_iter()
3660 {
3661 if let Some((i, path_open_result)) = maybe_opened_path {
3662 opened_items[i] = Some(path_open_result);
3663 }
3664 }
3665
3666 opened_items
3667}
3668
3669fn notify_of_new_dock(workspace: &WeakViewHandle<Workspace>, cx: &mut AsyncAppContext) {
3670 const NEW_PANEL_BLOG_POST: &str = "https://zed.dev/blog/new-panel-system";
3671 const NEW_DOCK_HINT_KEY: &str = "show_new_dock_key";
3672 const MESSAGE_ID: usize = 2;
3673
3674 if workspace
3675 .read_with(cx, |workspace, cx| {
3676 workspace.has_shown_notification_once::<MessageNotification>(MESSAGE_ID, cx)
3677 })
3678 .unwrap_or(false)
3679 {
3680 return;
3681 }
3682
3683 if db::kvp::KEY_VALUE_STORE
3684 .read_kvp(NEW_DOCK_HINT_KEY)
3685 .ok()
3686 .flatten()
3687 .is_some()
3688 {
3689 if !workspace
3690 .read_with(cx, |workspace, cx| {
3691 workspace.has_shown_notification_once::<MessageNotification>(MESSAGE_ID, cx)
3692 })
3693 .unwrap_or(false)
3694 {
3695 cx.update(|cx| {
3696 cx.update_global::<NotificationTracker, _, _>(|tracker, _| {
3697 let entry = tracker
3698 .entry(TypeId::of::<MessageNotification>())
3699 .or_default();
3700 if !entry.contains(&MESSAGE_ID) {
3701 entry.push(MESSAGE_ID);
3702 }
3703 });
3704 });
3705 }
3706
3707 return;
3708 }
3709
3710 cx.spawn(|_| async move {
3711 db::kvp::KEY_VALUE_STORE
3712 .write_kvp(NEW_DOCK_HINT_KEY.to_string(), "seen".to_string())
3713 .await
3714 .ok();
3715 })
3716 .detach();
3717
3718 workspace
3719 .update(cx, |workspace, cx| {
3720 workspace.show_notification_once(2, cx, |cx| {
3721 cx.add_view(|_| {
3722 MessageNotification::new_element(|text, _| {
3723 Text::new(
3724 "Looking for the dock? Try ctrl-`!\nshift-escape now zooms your pane.",
3725 text,
3726 )
3727 .with_custom_runs(vec![26..32, 34..46], |_, bounds, cx| {
3728 let code_span_background_color = settings::get::<ThemeSettings>(cx)
3729 .theme
3730 .editor
3731 .document_highlight_read_background;
3732
3733 cx.scene().push_quad(gpui::Quad {
3734 bounds,
3735 background: Some(code_span_background_color),
3736 border: Default::default(),
3737 corner_radii: (2.0).into(),
3738 })
3739 })
3740 .into_any()
3741 })
3742 .with_click_message("Read more about the new panel system")
3743 .on_click(|cx| cx.platform().open_url(NEW_PANEL_BLOG_POST))
3744 })
3745 })
3746 })
3747 .ok();
3748}
3749
3750fn notify_if_database_failed(workspace: &WeakViewHandle<Workspace>, cx: &mut AsyncAppContext) {
3751 const REPORT_ISSUE_URL: &str ="https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";
3752
3753 workspace
3754 .update(cx, |workspace, cx| {
3755 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
3756 workspace.show_notification_once(0, cx, |cx| {
3757 cx.add_view(|_| {
3758 MessageNotification::new("Failed to load the database file.")
3759 .with_click_message("Click to let us know about this error")
3760 .on_click(|cx| cx.platform().open_url(REPORT_ISSUE_URL))
3761 })
3762 });
3763 }
3764 })
3765 .log_err();
3766}
3767
3768impl Entity for Workspace {
3769 type Event = Event;
3770}
3771
3772impl View for Workspace {
3773 fn ui_name() -> &'static str {
3774 "Workspace"
3775 }
3776
3777 fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
3778 let theme = theme::current(cx).clone();
3779 Stack::new()
3780 .with_child(
3781 Flex::column()
3782 .with_child(self.render_titlebar(&theme, cx))
3783 .with_child(
3784 Stack::new()
3785 .with_child({
3786 let project = self.project.clone();
3787 Flex::row()
3788 .with_children(self.render_dock(DockPosition::Left, cx))
3789 .with_child(
3790 Flex::column()
3791 .with_child(
3792 FlexItem::new(
3793 self.center.render(
3794 &project,
3795 &theme,
3796 &self.follower_states_by_leader,
3797 self.active_call(),
3798 self.active_pane(),
3799 self.zoomed
3800 .as_ref()
3801 .and_then(|zoomed| zoomed.upgrade(cx))
3802 .as_ref(),
3803 &self.app_state,
3804 cx,
3805 ),
3806 )
3807 .flex(1., true),
3808 )
3809 .with_children(
3810 self.render_dock(DockPosition::Bottom, cx),
3811 )
3812 .flex(1., true),
3813 )
3814 .with_children(self.render_dock(DockPosition::Right, cx))
3815 })
3816 .with_child(Overlay::new(
3817 Stack::new()
3818 .with_children(self.zoomed.as_ref().and_then(|zoomed| {
3819 enum ZoomBackground {}
3820 let zoomed = zoomed.upgrade(cx)?;
3821
3822 let mut foreground_style =
3823 theme.workspace.zoomed_pane_foreground;
3824 if let Some(zoomed_dock_position) = self.zoomed_position {
3825 foreground_style =
3826 theme.workspace.zoomed_panel_foreground;
3827 let margin = foreground_style.margin.top;
3828 let border = foreground_style.border.top;
3829
3830 // Only include a margin and border on the opposite side.
3831 foreground_style.margin.top = 0.;
3832 foreground_style.margin.left = 0.;
3833 foreground_style.margin.bottom = 0.;
3834 foreground_style.margin.right = 0.;
3835 foreground_style.border.top = false;
3836 foreground_style.border.left = false;
3837 foreground_style.border.bottom = false;
3838 foreground_style.border.right = false;
3839 match zoomed_dock_position {
3840 DockPosition::Left => {
3841 foreground_style.margin.right = margin;
3842 foreground_style.border.right = border;
3843 }
3844 DockPosition::Right => {
3845 foreground_style.margin.left = margin;
3846 foreground_style.border.left = border;
3847 }
3848 DockPosition::Bottom => {
3849 foreground_style.margin.top = margin;
3850 foreground_style.border.top = border;
3851 }
3852 }
3853 }
3854
3855 Some(
3856 ChildView::new(&zoomed, cx)
3857 .contained()
3858 .with_style(foreground_style)
3859 .aligned()
3860 .contained()
3861 .with_style(theme.workspace.zoomed_background)
3862 .mouse::<ZoomBackground>(0)
3863 .capture_all()
3864 .on_down(
3865 MouseButton::Left,
3866 |_, this: &mut Self, cx| {
3867 this.zoom_out(cx);
3868 },
3869 ),
3870 )
3871 }))
3872 .with_children(self.modal.as_ref().map(|modal| {
3873 // Prevent clicks within the modal from falling
3874 // through to the rest of the workspace.
3875 enum ModalBackground {}
3876 MouseEventHandler::new::<ModalBackground, _>(
3877 0,
3878 cx,
3879 |_, cx| ChildView::new(modal.view.as_any(), cx),
3880 )
3881 .on_click(MouseButton::Left, |_, _, _| {})
3882 .contained()
3883 .with_style(theme.workspace.modal)
3884 .aligned()
3885 .top()
3886 }))
3887 .with_children(self.render_notifications(&theme.workspace, cx)),
3888 ))
3889 .provide_resize_bounds::<WorkspaceBounds>()
3890 .flex(1.0, true),
3891 )
3892 .with_child(ChildView::new(&self.status_bar, cx))
3893 .contained()
3894 .with_background_color(theme.workspace.background),
3895 )
3896 .with_children(DragAndDrop::render(cx))
3897 .with_children(self.render_disconnected_overlay(cx))
3898 .into_any_named("workspace")
3899 }
3900
3901 fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
3902 if cx.is_self_focused() {
3903 cx.focus(&self.active_pane);
3904 }
3905 }
3906
3907 fn modifiers_changed(&mut self, e: &ModifiersChangedEvent, cx: &mut ViewContext<Self>) -> bool {
3908 DragAndDrop::<Workspace>::update_modifiers(e.modifiers, cx)
3909 }
3910}
3911
3912impl ViewId {
3913 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
3914 Ok(Self {
3915 creator: message
3916 .creator
3917 .ok_or_else(|| anyhow!("creator is missing"))?,
3918 id: message.id,
3919 })
3920 }
3921
3922 pub(crate) fn to_proto(&self) -> proto::ViewId {
3923 proto::ViewId {
3924 creator: Some(self.creator),
3925 id: self.id,
3926 }
3927 }
3928}
3929
3930pub trait WorkspaceHandle {
3931 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
3932}
3933
3934impl WorkspaceHandle for ViewHandle<Workspace> {
3935 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
3936 self.read(cx)
3937 .worktrees(cx)
3938 .flat_map(|worktree| {
3939 let worktree_id = worktree.read(cx).id();
3940 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
3941 worktree_id,
3942 path: f.path.clone(),
3943 })
3944 })
3945 .collect::<Vec<_>>()
3946 }
3947}
3948
3949impl std::fmt::Debug for OpenPaths {
3950 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3951 f.debug_struct("OpenPaths")
3952 .field("paths", &self.paths)
3953 .finish()
3954 }
3955}
3956
3957pub struct WorkspaceCreated(pub WeakViewHandle<Workspace>);
3958
3959pub fn activate_workspace_for_project(
3960 cx: &mut AsyncAppContext,
3961 predicate: impl Fn(&mut Project, &mut ModelContext<Project>) -> bool,
3962) -> Option<WeakViewHandle<Workspace>> {
3963 for window in cx.windows() {
3964 let handle = window
3965 .update(cx, |cx| {
3966 if let Some(workspace_handle) = cx.root_view().clone().downcast::<Workspace>() {
3967 let project = workspace_handle.read(cx).project.clone();
3968 if project.update(cx, &predicate) {
3969 cx.activate_window();
3970 return Some(workspace_handle.clone());
3971 }
3972 }
3973 None
3974 })
3975 .flatten();
3976
3977 if let Some(handle) = handle {
3978 return Some(handle.downgrade());
3979 }
3980 }
3981 None
3982}
3983
3984pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
3985 DB.last_workspace().await.log_err().flatten()
3986}
3987
3988#[allow(clippy::type_complexity)]
3989pub fn open_paths(
3990 abs_paths: &[PathBuf],
3991 app_state: &Arc<AppState>,
3992 requesting_window: Option<WindowHandle<Workspace>>,
3993 cx: &mut AppContext,
3994) -> Task<
3995 Result<(
3996 WeakViewHandle<Workspace>,
3997 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
3998 )>,
3999> {
4000 let app_state = app_state.clone();
4001 let abs_paths = abs_paths.to_vec();
4002 cx.spawn(|mut cx| async move {
4003 // Open paths in existing workspace if possible
4004 let existing = activate_workspace_for_project(&mut cx, |project, cx| {
4005 project.contains_paths(&abs_paths, cx)
4006 });
4007
4008 if let Some(existing) = existing {
4009 Ok((
4010 existing.clone(),
4011 existing
4012 .update(&mut cx, |workspace, cx| {
4013 workspace.open_paths(abs_paths, true, cx)
4014 })?
4015 .await,
4016 ))
4017 } else {
4018 Ok(cx
4019 .update(|cx| {
4020 Workspace::new_local(abs_paths, app_state.clone(), requesting_window, cx)
4021 })
4022 .await)
4023 }
4024 })
4025}
4026
4027pub fn open_new(
4028 app_state: &Arc<AppState>,
4029 cx: &mut AppContext,
4030 init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static,
4031) -> Task<()> {
4032 let task = Workspace::new_local(Vec::new(), app_state.clone(), None, cx);
4033 cx.spawn(|mut cx| async move {
4034 let (workspace, opened_paths) = task.await;
4035
4036 workspace
4037 .update(&mut cx, |workspace, cx| {
4038 if opened_paths.is_empty() {
4039 init(workspace, cx)
4040 }
4041 })
4042 .log_err();
4043 })
4044}
4045
4046pub fn create_and_open_local_file(
4047 path: &'static Path,
4048 cx: &mut ViewContext<Workspace>,
4049 default_content: impl 'static + Send + FnOnce() -> Rope,
4050) -> Task<Result<Box<dyn ItemHandle>>> {
4051 cx.spawn(|workspace, mut cx| async move {
4052 let fs = workspace.read_with(&cx, |workspace, _| workspace.app_state().fs.clone())?;
4053 if !fs.is_file(path).await {
4054 fs.create_file(path, Default::default()).await?;
4055 fs.save(path, &default_content(), Default::default())
4056 .await?;
4057 }
4058
4059 let mut items = workspace
4060 .update(&mut cx, |workspace, cx| {
4061 workspace.with_local_workspace(cx, |workspace, cx| {
4062 workspace.open_paths(vec![path.to_path_buf()], false, cx)
4063 })
4064 })?
4065 .await?
4066 .await;
4067
4068 let item = items.pop().flatten();
4069 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
4070 })
4071}
4072
4073pub fn join_remote_project(
4074 project_id: u64,
4075 follow_user_id: u64,
4076 app_state: Arc<AppState>,
4077 cx: &mut AppContext,
4078) -> Task<Result<()>> {
4079 cx.spawn(|mut cx| async move {
4080 let existing_workspace = cx
4081 .windows()
4082 .into_iter()
4083 .find_map(|window| {
4084 window.downcast::<Workspace>().and_then(|window| {
4085 window.read_root_with(&cx, |workspace, cx| {
4086 if workspace.project().read(cx).remote_id() == Some(project_id) {
4087 Some(cx.handle().downgrade())
4088 } else {
4089 None
4090 }
4091 })
4092 })
4093 })
4094 .flatten();
4095
4096 let workspace = if let Some(existing_workspace) = existing_workspace {
4097 existing_workspace
4098 } else {
4099 let active_call = cx.read(ActiveCall::global);
4100 let room = active_call
4101 .read_with(&cx, |call, _| call.room().cloned())
4102 .ok_or_else(|| anyhow!("not in a call"))?;
4103 let project = room
4104 .update(&mut cx, |room, cx| {
4105 room.join_project(
4106 project_id,
4107 app_state.languages.clone(),
4108 app_state.fs.clone(),
4109 cx,
4110 )
4111 })
4112 .await?;
4113
4114 let window_bounds_override = window_bounds_env_override(&cx);
4115 let window = cx.add_window(
4116 (app_state.build_window_options)(
4117 window_bounds_override,
4118 None,
4119 cx.platform().as_ref(),
4120 ),
4121 |cx| Workspace::new(0, project, app_state.clone(), cx),
4122 );
4123 let workspace = window.root(&cx).unwrap();
4124 (app_state.initialize_workspace)(
4125 workspace.downgrade(),
4126 false,
4127 app_state.clone(),
4128 cx.clone(),
4129 )
4130 .await
4131 .log_err();
4132
4133 workspace.downgrade()
4134 };
4135
4136 workspace.window().activate(&mut cx);
4137 cx.platform().activate(true);
4138
4139 workspace.update(&mut cx, |workspace, cx| {
4140 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
4141 let follow_peer_id = room
4142 .read(cx)
4143 .remote_participants()
4144 .iter()
4145 .find(|(_, participant)| participant.user.id == follow_user_id)
4146 .map(|(_, p)| p.peer_id)
4147 .or_else(|| {
4148 // If we couldn't follow the given user, follow the host instead.
4149 let collaborator = workspace
4150 .project()
4151 .read(cx)
4152 .collaborators()
4153 .values()
4154 .find(|collaborator| collaborator.replica_id == 0)?;
4155 Some(collaborator.peer_id)
4156 });
4157
4158 if let Some(follow_peer_id) = follow_peer_id {
4159 if !workspace.is_being_followed(follow_peer_id) {
4160 workspace
4161 .toggle_follow(follow_peer_id, cx)
4162 .map(|follow| follow.detach_and_log_err(cx));
4163 }
4164 }
4165 }
4166 })?;
4167
4168 anyhow::Ok(())
4169 })
4170}
4171
4172pub fn restart(_: &Restart, cx: &mut AppContext) {
4173 let should_confirm = settings::get::<WorkspaceSettings>(cx).confirm_quit;
4174 cx.spawn(|mut cx| async move {
4175 let mut workspace_windows = cx
4176 .windows()
4177 .into_iter()
4178 .filter_map(|window| window.downcast::<Workspace>())
4179 .collect::<Vec<_>>();
4180
4181 // If multiple windows have unsaved changes, and need a save prompt,
4182 // prompt in the active window before switching to a different window.
4183 workspace_windows.sort_by_key(|window| window.is_active(&cx) == Some(false));
4184
4185 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
4186 let answer = window.prompt(
4187 PromptLevel::Info,
4188 "Are you sure you want to restart?",
4189 &["Restart", "Cancel"],
4190 &mut cx,
4191 );
4192
4193 if let Some(mut answer) = answer {
4194 let answer = answer.next().await;
4195 if answer != Some(0) {
4196 return Ok(());
4197 }
4198 }
4199 }
4200
4201 // If the user cancels any save prompt, then keep the app open.
4202 for window in workspace_windows {
4203 if let Some(should_close) = window.update_root(&mut cx, |workspace, cx| {
4204 workspace.prepare_to_close(true, cx)
4205 }) {
4206 if !should_close.await? {
4207 return Ok(());
4208 }
4209 }
4210 }
4211 cx.platform().restart();
4212 anyhow::Ok(())
4213 })
4214 .detach_and_log_err(cx);
4215}
4216
4217fn parse_pixel_position_env_var(value: &str) -> Option<Vector2F> {
4218 let mut parts = value.split(',');
4219 let width: usize = parts.next()?.parse().ok()?;
4220 let height: usize = parts.next()?.parse().ok()?;
4221 Some(vec2f(width as f32, height as f32))
4222}
4223
4224#[cfg(test)]
4225mod tests {
4226 use super::*;
4227 use crate::{
4228 dock::test::{TestPanel, TestPanelEvent},
4229 item::test::{TestItem, TestItemEvent, TestProjectItem},
4230 };
4231 use fs::FakeFs;
4232 use gpui::{executor::Deterministic, test::EmptyView, TestAppContext};
4233 use project::{Project, ProjectEntryId};
4234 use serde_json::json;
4235 use settings::SettingsStore;
4236 use std::{cell::RefCell, rc::Rc};
4237
4238 #[gpui::test]
4239 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
4240 init_test(cx);
4241
4242 let fs = FakeFs::new(cx.background());
4243 let project = Project::test(fs, [], cx).await;
4244 let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
4245 let workspace = window.root(cx);
4246
4247 // Adding an item with no ambiguity renders the tab without detail.
4248 let item1 = window.add_view(cx, |_| {
4249 let mut item = TestItem::new();
4250 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
4251 item
4252 });
4253 workspace.update(cx, |workspace, cx| {
4254 workspace.add_item(Box::new(item1.clone()), cx);
4255 });
4256 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), None));
4257
4258 // Adding an item that creates ambiguity increases the level of detail on
4259 // both tabs.
4260 let item2 = window.add_view(cx, |_| {
4261 let mut item = TestItem::new();
4262 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
4263 item
4264 });
4265 workspace.update(cx, |workspace, cx| {
4266 workspace.add_item(Box::new(item2.clone()), cx);
4267 });
4268 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4269 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4270
4271 // Adding an item that creates ambiguity increases the level of detail only
4272 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
4273 // we stop at the highest detail available.
4274 let item3 = window.add_view(cx, |_| {
4275 let mut item = TestItem::new();
4276 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
4277 item
4278 });
4279 workspace.update(cx, |workspace, cx| {
4280 workspace.add_item(Box::new(item3.clone()), cx);
4281 });
4282 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4283 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
4284 item3.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
4285 }
4286
4287 #[gpui::test]
4288 async fn test_tracking_active_path(cx: &mut TestAppContext) {
4289 init_test(cx);
4290
4291 let fs = FakeFs::new(cx.background());
4292 fs.insert_tree(
4293 "/root1",
4294 json!({
4295 "one.txt": "",
4296 "two.txt": "",
4297 }),
4298 )
4299 .await;
4300 fs.insert_tree(
4301 "/root2",
4302 json!({
4303 "three.txt": "",
4304 }),
4305 )
4306 .await;
4307
4308 let project = Project::test(fs, ["root1".as_ref()], cx).await;
4309 let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
4310 let workspace = window.root(cx);
4311 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4312 let worktree_id = project.read_with(cx, |project, cx| {
4313 project.worktrees(cx).next().unwrap().read(cx).id()
4314 });
4315
4316 let item1 = window.add_view(cx, |cx| {
4317 TestItem::new().with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
4318 });
4319 let item2 = window.add_view(cx, |cx| {
4320 TestItem::new().with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
4321 });
4322
4323 // Add an item to an empty pane
4324 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx));
4325 project.read_with(cx, |project, cx| {
4326 assert_eq!(
4327 project.active_entry(),
4328 project
4329 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
4330 .map(|e| e.id)
4331 );
4332 });
4333 assert_eq!(window.current_title(cx).as_deref(), Some("one.txt β root1"));
4334
4335 // Add a second item to a non-empty pane
4336 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
4337 assert_eq!(window.current_title(cx).as_deref(), Some("two.txt β root1"));
4338 project.read_with(cx, |project, cx| {
4339 assert_eq!(
4340 project.active_entry(),
4341 project
4342 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
4343 .map(|e| e.id)
4344 );
4345 });
4346
4347 // Close the active item
4348 pane.update(cx, |pane, cx| {
4349 pane.close_active_item(&Default::default(), cx).unwrap()
4350 })
4351 .await
4352 .unwrap();
4353 assert_eq!(window.current_title(cx).as_deref(), Some("one.txt β root1"));
4354 project.read_with(cx, |project, cx| {
4355 assert_eq!(
4356 project.active_entry(),
4357 project
4358 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
4359 .map(|e| e.id)
4360 );
4361 });
4362
4363 // Add a project folder
4364 project
4365 .update(cx, |project, cx| {
4366 project.find_or_create_local_worktree("/root2", true, cx)
4367 })
4368 .await
4369 .unwrap();
4370 assert_eq!(
4371 window.current_title(cx).as_deref(),
4372 Some("one.txt β root1, root2")
4373 );
4374
4375 // Remove a project folder
4376 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
4377 assert_eq!(window.current_title(cx).as_deref(), Some("one.txt β root2"));
4378 }
4379
4380 #[gpui::test]
4381 async fn test_close_window(cx: &mut TestAppContext) {
4382 init_test(cx);
4383
4384 let fs = FakeFs::new(cx.background());
4385 fs.insert_tree("/root", json!({ "one": "" })).await;
4386
4387 let project = Project::test(fs, ["root".as_ref()], cx).await;
4388 let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
4389 let workspace = window.root(cx);
4390
4391 // When there are no dirty items, there's nothing to do.
4392 let item1 = window.add_view(cx, |_| TestItem::new());
4393 workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
4394 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
4395 assert!(task.await.unwrap());
4396
4397 // When there are dirty untitled items, prompt to save each one. If the user
4398 // cancels any prompt, then abort.
4399 let item2 = window.add_view(cx, |_| TestItem::new().with_dirty(true));
4400 let item3 = window.add_view(cx, |cx| {
4401 TestItem::new()
4402 .with_dirty(true)
4403 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4404 });
4405 workspace.update(cx, |w, cx| {
4406 w.add_item(Box::new(item2.clone()), cx);
4407 w.add_item(Box::new(item3.clone()), cx);
4408 });
4409 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
4410 cx.foreground().run_until_parked();
4411 window.simulate_prompt_answer(2, cx); // cancel save all
4412 cx.foreground().run_until_parked();
4413 window.simulate_prompt_answer(2, cx); // cancel save all
4414 cx.foreground().run_until_parked();
4415 assert!(!window.has_pending_prompt(cx));
4416 assert!(!task.await.unwrap());
4417 }
4418
4419 #[gpui::test]
4420 async fn test_close_pane_items(cx: &mut TestAppContext) {
4421 init_test(cx);
4422
4423 let fs = FakeFs::new(cx.background());
4424
4425 let project = Project::test(fs, None, cx).await;
4426 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
4427 let workspace = window.root(cx);
4428
4429 let item1 = window.add_view(cx, |cx| {
4430 TestItem::new()
4431 .with_dirty(true)
4432 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4433 });
4434 let item2 = window.add_view(cx, |cx| {
4435 TestItem::new()
4436 .with_dirty(true)
4437 .with_conflict(true)
4438 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
4439 });
4440 let item3 = window.add_view(cx, |cx| {
4441 TestItem::new()
4442 .with_dirty(true)
4443 .with_conflict(true)
4444 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
4445 });
4446 let item4 = window.add_view(cx, |cx| {
4447 TestItem::new()
4448 .with_dirty(true)
4449 .with_project_items(&[TestProjectItem::new_untitled(cx)])
4450 });
4451 let pane = workspace.update(cx, |workspace, cx| {
4452 workspace.add_item(Box::new(item1.clone()), cx);
4453 workspace.add_item(Box::new(item2.clone()), cx);
4454 workspace.add_item(Box::new(item3.clone()), cx);
4455 workspace.add_item(Box::new(item4.clone()), cx);
4456 workspace.active_pane().clone()
4457 });
4458
4459 let close_items = pane.update(cx, |pane, cx| {
4460 pane.activate_item(1, true, true, cx);
4461 assert_eq!(pane.active_item().unwrap().id(), item2.id());
4462 let item1_id = item1.id();
4463 let item3_id = item3.id();
4464 let item4_id = item4.id();
4465 pane.close_items(cx, SaveIntent::Close, move |id| {
4466 [item1_id, item3_id, item4_id].contains(&id)
4467 })
4468 });
4469 cx.foreground().run_until_parked();
4470
4471 assert!(window.has_pending_prompt(cx));
4472 // Ignore "Save all" prompt
4473 window.simulate_prompt_answer(2, cx);
4474 cx.foreground().run_until_parked();
4475 // There's a prompt to save item 1.
4476 pane.read_with(cx, |pane, _| {
4477 assert_eq!(pane.items_len(), 4);
4478 assert_eq!(pane.active_item().unwrap().id(), item1.id());
4479 });
4480 // Confirm saving item 1.
4481 window.simulate_prompt_answer(0, cx);
4482 cx.foreground().run_until_parked();
4483
4484 // Item 1 is saved. There's a prompt to save item 3.
4485 pane.read_with(cx, |pane, cx| {
4486 assert_eq!(item1.read(cx).save_count, 1);
4487 assert_eq!(item1.read(cx).save_as_count, 0);
4488 assert_eq!(item1.read(cx).reload_count, 0);
4489 assert_eq!(pane.items_len(), 3);
4490 assert_eq!(pane.active_item().unwrap().id(), item3.id());
4491 });
4492 assert!(window.has_pending_prompt(cx));
4493
4494 // Cancel saving item 3.
4495 window.simulate_prompt_answer(1, cx);
4496 cx.foreground().run_until_parked();
4497
4498 // Item 3 is reloaded. There's a prompt to save item 4.
4499 pane.read_with(cx, |pane, cx| {
4500 assert_eq!(item3.read(cx).save_count, 0);
4501 assert_eq!(item3.read(cx).save_as_count, 0);
4502 assert_eq!(item3.read(cx).reload_count, 1);
4503 assert_eq!(pane.items_len(), 2);
4504 assert_eq!(pane.active_item().unwrap().id(), item4.id());
4505 });
4506 assert!(window.has_pending_prompt(cx));
4507
4508 // Confirm saving item 4.
4509 window.simulate_prompt_answer(0, cx);
4510 cx.foreground().run_until_parked();
4511
4512 // There's a prompt for a path for item 4.
4513 cx.simulate_new_path_selection(|_| Some(Default::default()));
4514 close_items.await.unwrap();
4515
4516 // The requested items are closed.
4517 pane.read_with(cx, |pane, cx| {
4518 assert_eq!(item4.read(cx).save_count, 0);
4519 assert_eq!(item4.read(cx).save_as_count, 1);
4520 assert_eq!(item4.read(cx).reload_count, 0);
4521 assert_eq!(pane.items_len(), 1);
4522 assert_eq!(pane.active_item().unwrap().id(), item2.id());
4523 });
4524 }
4525
4526 #[gpui::test]
4527 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
4528 init_test(cx);
4529
4530 let fs = FakeFs::new(cx.background());
4531
4532 let project = Project::test(fs, [], cx).await;
4533 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
4534 let workspace = window.root(cx);
4535
4536 // Create several workspace items with single project entries, and two
4537 // workspace items with multiple project entries.
4538 let single_entry_items = (0..=4)
4539 .map(|project_entry_id| {
4540 window.add_view(cx, |cx| {
4541 TestItem::new()
4542 .with_dirty(true)
4543 .with_project_items(&[TestProjectItem::new(
4544 project_entry_id,
4545 &format!("{project_entry_id}.txt"),
4546 cx,
4547 )])
4548 })
4549 })
4550 .collect::<Vec<_>>();
4551 let item_2_3 = window.add_view(cx, |cx| {
4552 TestItem::new()
4553 .with_dirty(true)
4554 .with_singleton(false)
4555 .with_project_items(&[
4556 single_entry_items[2].read(cx).project_items[0].clone(),
4557 single_entry_items[3].read(cx).project_items[0].clone(),
4558 ])
4559 });
4560 let item_3_4 = window.add_view(cx, |cx| {
4561 TestItem::new()
4562 .with_dirty(true)
4563 .with_singleton(false)
4564 .with_project_items(&[
4565 single_entry_items[3].read(cx).project_items[0].clone(),
4566 single_entry_items[4].read(cx).project_items[0].clone(),
4567 ])
4568 });
4569
4570 // Create two panes that contain the following project entries:
4571 // left pane:
4572 // multi-entry items: (2, 3)
4573 // single-entry items: 0, 1, 2, 3, 4
4574 // right pane:
4575 // single-entry items: 1
4576 // multi-entry items: (3, 4)
4577 let left_pane = workspace.update(cx, |workspace, cx| {
4578 let left_pane = workspace.active_pane().clone();
4579 workspace.add_item(Box::new(item_2_3.clone()), cx);
4580 for item in single_entry_items {
4581 workspace.add_item(Box::new(item), cx);
4582 }
4583 left_pane.update(cx, |pane, cx| {
4584 pane.activate_item(2, true, true, cx);
4585 });
4586
4587 workspace
4588 .split_and_clone(left_pane.clone(), SplitDirection::Right, cx)
4589 .unwrap();
4590
4591 left_pane
4592 });
4593
4594 //Need to cause an effect flush in order to respect new focus
4595 workspace.update(cx, |workspace, cx| {
4596 workspace.add_item(Box::new(item_3_4.clone()), cx);
4597 cx.focus(&left_pane);
4598 });
4599
4600 // When closing all of the items in the left pane, we should be prompted twice:
4601 // once for project entry 0, and once for project entry 2. After those two
4602 // prompts, the task should complete.
4603
4604 let close = left_pane.update(cx, |pane, cx| {
4605 pane.close_items(cx, SaveIntent::Close, move |_| true)
4606 });
4607 cx.foreground().run_until_parked();
4608 // Discard "Save all" prompt
4609 window.simulate_prompt_answer(2, cx);
4610
4611 cx.foreground().run_until_parked();
4612 left_pane.read_with(cx, |pane, cx| {
4613 assert_eq!(
4614 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
4615 &[ProjectEntryId::from_proto(0)]
4616 );
4617 });
4618 window.simulate_prompt_answer(0, cx);
4619
4620 cx.foreground().run_until_parked();
4621 left_pane.read_with(cx, |pane, cx| {
4622 assert_eq!(
4623 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
4624 &[ProjectEntryId::from_proto(2)]
4625 );
4626 });
4627 window.simulate_prompt_answer(0, cx);
4628
4629 cx.foreground().run_until_parked();
4630 close.await.unwrap();
4631 left_pane.read_with(cx, |pane, _| {
4632 assert_eq!(pane.items_len(), 0);
4633 });
4634 }
4635
4636 #[gpui::test]
4637 async fn test_autosave(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
4638 init_test(cx);
4639
4640 let fs = FakeFs::new(cx.background());
4641
4642 let project = Project::test(fs, [], cx).await;
4643 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
4644 let workspace = window.root(cx);
4645 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4646
4647 let item = window.add_view(cx, |cx| {
4648 TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4649 });
4650 let item_id = item.id();
4651 workspace.update(cx, |workspace, cx| {
4652 workspace.add_item(Box::new(item.clone()), cx);
4653 });
4654
4655 // Autosave on window change.
4656 item.update(cx, |item, cx| {
4657 cx.update_global(|settings: &mut SettingsStore, cx| {
4658 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4659 settings.autosave = Some(AutosaveSetting::OnWindowChange);
4660 })
4661 });
4662 item.is_dirty = true;
4663 });
4664
4665 // Deactivating the window saves the file.
4666 window.simulate_deactivation(cx);
4667 deterministic.run_until_parked();
4668 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
4669
4670 // Autosave on focus change.
4671 item.update(cx, |item, cx| {
4672 cx.focus_self();
4673 cx.update_global(|settings: &mut SettingsStore, cx| {
4674 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4675 settings.autosave = Some(AutosaveSetting::OnFocusChange);
4676 })
4677 });
4678 item.is_dirty = true;
4679 });
4680
4681 // Blurring the item saves the file.
4682 item.update(cx, |_, cx| cx.blur());
4683 deterministic.run_until_parked();
4684 item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
4685
4686 // Deactivating the window still saves the file.
4687 window.simulate_activation(cx);
4688 item.update(cx, |item, cx| {
4689 cx.focus_self();
4690 item.is_dirty = true;
4691 });
4692 window.simulate_deactivation(cx);
4693
4694 deterministic.run_until_parked();
4695 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
4696
4697 // Autosave after delay.
4698 item.update(cx, |item, cx| {
4699 cx.update_global(|settings: &mut SettingsStore, cx| {
4700 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4701 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
4702 })
4703 });
4704 item.is_dirty = true;
4705 cx.emit(TestItemEvent::Edit);
4706 });
4707
4708 // Delay hasn't fully expired, so the file is still dirty and unsaved.
4709 deterministic.advance_clock(Duration::from_millis(250));
4710 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
4711
4712 // After delay expires, the file is saved.
4713 deterministic.advance_clock(Duration::from_millis(250));
4714 item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
4715
4716 // Autosave on focus change, ensuring closing the tab counts as such.
4717 item.update(cx, |item, cx| {
4718 cx.update_global(|settings: &mut SettingsStore, cx| {
4719 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4720 settings.autosave = Some(AutosaveSetting::OnFocusChange);
4721 })
4722 });
4723 item.is_dirty = true;
4724 });
4725
4726 pane.update(cx, |pane, cx| {
4727 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
4728 })
4729 .await
4730 .unwrap();
4731 assert!(!window.has_pending_prompt(cx));
4732 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
4733
4734 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
4735 workspace.update(cx, |workspace, cx| {
4736 workspace.add_item(Box::new(item.clone()), cx);
4737 });
4738 item.update(cx, |item, cx| {
4739 item.project_items[0].update(cx, |item, _| {
4740 item.entry_id = None;
4741 });
4742 item.is_dirty = true;
4743 cx.blur();
4744 });
4745 deterministic.run_until_parked();
4746 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
4747
4748 // Ensure autosave is prevented for deleted files also when closing the buffer.
4749 let _close_items = pane.update(cx, |pane, cx| {
4750 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
4751 });
4752 deterministic.run_until_parked();
4753 assert!(window.has_pending_prompt(cx));
4754 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
4755 }
4756
4757 #[gpui::test]
4758 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
4759 init_test(cx);
4760
4761 let fs = FakeFs::new(cx.background());
4762
4763 let project = Project::test(fs, [], cx).await;
4764 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
4765 let workspace = window.root(cx);
4766
4767 let item = window.add_view(cx, |cx| {
4768 TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4769 });
4770 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4771 let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone());
4772 let toolbar_notify_count = Rc::new(RefCell::new(0));
4773
4774 workspace.update(cx, |workspace, cx| {
4775 workspace.add_item(Box::new(item.clone()), cx);
4776 let toolbar_notification_count = toolbar_notify_count.clone();
4777 cx.observe(&toolbar, move |_, _, _| {
4778 *toolbar_notification_count.borrow_mut() += 1
4779 })
4780 .detach();
4781 });
4782
4783 pane.read_with(cx, |pane, _| {
4784 assert!(!pane.can_navigate_backward());
4785 assert!(!pane.can_navigate_forward());
4786 });
4787
4788 item.update(cx, |item, cx| {
4789 item.set_state("one".to_string(), cx);
4790 });
4791
4792 // Toolbar must be notified to re-render the navigation buttons
4793 assert_eq!(*toolbar_notify_count.borrow(), 1);
4794
4795 pane.read_with(cx, |pane, _| {
4796 assert!(pane.can_navigate_backward());
4797 assert!(!pane.can_navigate_forward());
4798 });
4799
4800 workspace
4801 .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
4802 .await
4803 .unwrap();
4804
4805 assert_eq!(*toolbar_notify_count.borrow(), 3);
4806 pane.read_with(cx, |pane, _| {
4807 assert!(!pane.can_navigate_backward());
4808 assert!(pane.can_navigate_forward());
4809 });
4810 }
4811
4812 #[gpui::test]
4813 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
4814 init_test(cx);
4815 let fs = FakeFs::new(cx.background());
4816
4817 let project = Project::test(fs, [], cx).await;
4818 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
4819 let workspace = window.root(cx);
4820
4821 let panel = workspace.update(cx, |workspace, cx| {
4822 let panel = cx.add_view(|_| TestPanel::new(DockPosition::Right));
4823 workspace.add_panel(panel.clone(), cx);
4824
4825 workspace
4826 .right_dock()
4827 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
4828
4829 panel
4830 });
4831
4832 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4833 pane.update(cx, |pane, cx| {
4834 let item = cx.add_view(|_| TestItem::new());
4835 pane.add_item(Box::new(item), true, true, None, cx);
4836 });
4837
4838 // Transfer focus from center to panel
4839 workspace.update(cx, |workspace, cx| {
4840 workspace.toggle_panel_focus::<TestPanel>(cx);
4841 });
4842
4843 workspace.read_with(cx, |workspace, cx| {
4844 assert!(workspace.right_dock().read(cx).is_open());
4845 assert!(!panel.is_zoomed(cx));
4846 assert!(panel.has_focus(cx));
4847 });
4848
4849 // Transfer focus from panel to center
4850 workspace.update(cx, |workspace, cx| {
4851 workspace.toggle_panel_focus::<TestPanel>(cx);
4852 });
4853
4854 workspace.read_with(cx, |workspace, cx| {
4855 assert!(workspace.right_dock().read(cx).is_open());
4856 assert!(!panel.is_zoomed(cx));
4857 assert!(!panel.has_focus(cx));
4858 });
4859
4860 // Close the dock
4861 workspace.update(cx, |workspace, cx| {
4862 workspace.toggle_dock(DockPosition::Right, cx);
4863 });
4864
4865 workspace.read_with(cx, |workspace, cx| {
4866 assert!(!workspace.right_dock().read(cx).is_open());
4867 assert!(!panel.is_zoomed(cx));
4868 assert!(!panel.has_focus(cx));
4869 });
4870
4871 // Open the dock
4872 workspace.update(cx, |workspace, cx| {
4873 workspace.toggle_dock(DockPosition::Right, cx);
4874 });
4875
4876 workspace.read_with(cx, |workspace, cx| {
4877 assert!(workspace.right_dock().read(cx).is_open());
4878 assert!(!panel.is_zoomed(cx));
4879 assert!(panel.has_focus(cx));
4880 });
4881
4882 // Focus and zoom panel
4883 panel.update(cx, |panel, cx| {
4884 cx.focus_self();
4885 panel.set_zoomed(true, cx)
4886 });
4887
4888 workspace.read_with(cx, |workspace, cx| {
4889 assert!(workspace.right_dock().read(cx).is_open());
4890 assert!(panel.is_zoomed(cx));
4891 assert!(panel.has_focus(cx));
4892 });
4893
4894 // Transfer focus to the center closes the dock
4895 workspace.update(cx, |workspace, cx| {
4896 workspace.toggle_panel_focus::<TestPanel>(cx);
4897 });
4898
4899 workspace.read_with(cx, |workspace, cx| {
4900 assert!(!workspace.right_dock().read(cx).is_open());
4901 assert!(panel.is_zoomed(cx));
4902 assert!(!panel.has_focus(cx));
4903 });
4904
4905 // Transferring focus back to the panel keeps it zoomed
4906 workspace.update(cx, |workspace, cx| {
4907 workspace.toggle_panel_focus::<TestPanel>(cx);
4908 });
4909
4910 workspace.read_with(cx, |workspace, cx| {
4911 assert!(workspace.right_dock().read(cx).is_open());
4912 assert!(panel.is_zoomed(cx));
4913 assert!(panel.has_focus(cx));
4914 });
4915
4916 // Close the dock while it is zoomed
4917 workspace.update(cx, |workspace, cx| {
4918 workspace.toggle_dock(DockPosition::Right, cx)
4919 });
4920
4921 workspace.read_with(cx, |workspace, cx| {
4922 assert!(!workspace.right_dock().read(cx).is_open());
4923 assert!(panel.is_zoomed(cx));
4924 assert!(workspace.zoomed.is_none());
4925 assert!(!panel.has_focus(cx));
4926 });
4927
4928 // Opening the dock, when it's zoomed, retains focus
4929 workspace.update(cx, |workspace, cx| {
4930 workspace.toggle_dock(DockPosition::Right, cx)
4931 });
4932
4933 workspace.read_with(cx, |workspace, cx| {
4934 assert!(workspace.right_dock().read(cx).is_open());
4935 assert!(panel.is_zoomed(cx));
4936 assert!(workspace.zoomed.is_some());
4937 assert!(panel.has_focus(cx));
4938 });
4939
4940 // Unzoom and close the panel, zoom the active pane.
4941 panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
4942 workspace.update(cx, |workspace, cx| {
4943 workspace.toggle_dock(DockPosition::Right, cx)
4944 });
4945 pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
4946
4947 // Opening a dock unzooms the pane.
4948 workspace.update(cx, |workspace, cx| {
4949 workspace.toggle_dock(DockPosition::Right, cx)
4950 });
4951 workspace.read_with(cx, |workspace, cx| {
4952 let pane = pane.read(cx);
4953 assert!(!pane.is_zoomed());
4954 assert!(!pane.has_focus());
4955 assert!(workspace.right_dock().read(cx).is_open());
4956 assert!(workspace.zoomed.is_none());
4957 });
4958 }
4959
4960 #[gpui::test]
4961 async fn test_panels(cx: &mut gpui::TestAppContext) {
4962 init_test(cx);
4963 let fs = FakeFs::new(cx.background());
4964
4965 let project = Project::test(fs, [], cx).await;
4966 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
4967 let workspace = window.root(cx);
4968
4969 let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
4970 // Add panel_1 on the left, panel_2 on the right.
4971 let panel_1 = cx.add_view(|_| TestPanel::new(DockPosition::Left));
4972 workspace.add_panel(panel_1.clone(), cx);
4973 workspace
4974 .left_dock()
4975 .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
4976 let panel_2 = cx.add_view(|_| TestPanel::new(DockPosition::Right));
4977 workspace.add_panel(panel_2.clone(), cx);
4978 workspace
4979 .right_dock()
4980 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
4981
4982 let left_dock = workspace.left_dock();
4983 assert_eq!(
4984 left_dock.read(cx).visible_panel().unwrap().id(),
4985 panel_1.id()
4986 );
4987 assert_eq!(
4988 left_dock.read(cx).active_panel_size(cx).unwrap(),
4989 panel_1.size(cx)
4990 );
4991
4992 left_dock.update(cx, |left_dock, cx| {
4993 left_dock.resize_active_panel(Some(1337.), cx)
4994 });
4995 assert_eq!(
4996 workspace
4997 .right_dock()
4998 .read(cx)
4999 .visible_panel()
5000 .unwrap()
5001 .id(),
5002 panel_2.id()
5003 );
5004
5005 (panel_1, panel_2)
5006 });
5007
5008 // Move panel_1 to the right
5009 panel_1.update(cx, |panel_1, cx| {
5010 panel_1.set_position(DockPosition::Right, cx)
5011 });
5012
5013 workspace.update(cx, |workspace, cx| {
5014 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
5015 // Since it was the only panel on the left, the left dock should now be closed.
5016 assert!(!workspace.left_dock().read(cx).is_open());
5017 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
5018 let right_dock = workspace.right_dock();
5019 assert_eq!(
5020 right_dock.read(cx).visible_panel().unwrap().id(),
5021 panel_1.id()
5022 );
5023 assert_eq!(right_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
5024
5025 // Now we move panel_2Β to the left
5026 panel_2.set_position(DockPosition::Left, cx);
5027 });
5028
5029 workspace.update(cx, |workspace, cx| {
5030 // Since panel_2 was not visible on the right, we don't open the left dock.
5031 assert!(!workspace.left_dock().read(cx).is_open());
5032 // And the right dock is unaffected in it's displaying of panel_1
5033 assert!(workspace.right_dock().read(cx).is_open());
5034 assert_eq!(
5035 workspace
5036 .right_dock()
5037 .read(cx)
5038 .visible_panel()
5039 .unwrap()
5040 .id(),
5041 panel_1.id()
5042 );
5043 });
5044
5045 // Move panel_1 back to the left
5046 panel_1.update(cx, |panel_1, cx| {
5047 panel_1.set_position(DockPosition::Left, cx)
5048 });
5049
5050 workspace.update(cx, |workspace, cx| {
5051 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
5052 let left_dock = workspace.left_dock();
5053 assert!(left_dock.read(cx).is_open());
5054 assert_eq!(
5055 left_dock.read(cx).visible_panel().unwrap().id(),
5056 panel_1.id()
5057 );
5058 assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
5059 // And right the dock should be closed as it no longer has any panels.
5060 assert!(!workspace.right_dock().read(cx).is_open());
5061
5062 // Now we move panel_1 to the bottom
5063 panel_1.set_position(DockPosition::Bottom, cx);
5064 });
5065
5066 workspace.update(cx, |workspace, cx| {
5067 // Since panel_1 was visible on the left, we close the left dock.
5068 assert!(!workspace.left_dock().read(cx).is_open());
5069 // The bottom dock is sized based on the panel's default size,
5070 // since the panel orientation changed from vertical to horizontal.
5071 let bottom_dock = workspace.bottom_dock();
5072 assert_eq!(
5073 bottom_dock.read(cx).active_panel_size(cx).unwrap(),
5074 panel_1.size(cx),
5075 );
5076 // Close bottom dock and move panel_1 back to the left.
5077 bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
5078 panel_1.set_position(DockPosition::Left, cx);
5079 });
5080
5081 // Emit activated event on panel 1
5082 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Activated));
5083
5084 // Now the left dock is open and panel_1 is active and focused.
5085 workspace.read_with(cx, |workspace, cx| {
5086 let left_dock = workspace.left_dock();
5087 assert!(left_dock.read(cx).is_open());
5088 assert_eq!(
5089 left_dock.read(cx).visible_panel().unwrap().id(),
5090 panel_1.id()
5091 );
5092 assert!(panel_1.is_focused(cx));
5093 });
5094
5095 // Emit closed event on panel 2, which is not active
5096 panel_2.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed));
5097
5098 // Wo don't close the left dock, because panel_2 wasn't the active panel
5099 workspace.read_with(cx, |workspace, cx| {
5100 let left_dock = workspace.left_dock();
5101 assert!(left_dock.read(cx).is_open());
5102 assert_eq!(
5103 left_dock.read(cx).visible_panel().unwrap().id(),
5104 panel_1.id()
5105 );
5106 });
5107
5108 // Emitting a ZoomIn event shows the panel as zoomed.
5109 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomIn));
5110 workspace.read_with(cx, |workspace, _| {
5111 assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
5112 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
5113 });
5114
5115 // Move panel to another dock while it is zoomed
5116 panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
5117 workspace.read_with(cx, |workspace, _| {
5118 assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
5119 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5120 });
5121
5122 // If focus is transferred to another view that's not a panel or another pane, we still show
5123 // the panel as zoomed.
5124 let focus_receiver = window.add_view(cx, |_| EmptyView);
5125 focus_receiver.update(cx, |_, cx| cx.focus_self());
5126 workspace.read_with(cx, |workspace, _| {
5127 assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
5128 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5129 });
5130
5131 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
5132 workspace.update(cx, |_, cx| cx.focus_self());
5133 workspace.read_with(cx, |workspace, _| {
5134 assert_eq!(workspace.zoomed, None);
5135 assert_eq!(workspace.zoomed_position, None);
5136 });
5137
5138 // If focus is transferred again to another view that's not a panel or a pane, we won't
5139 // show the panel as zoomed because it wasn't zoomed before.
5140 focus_receiver.update(cx, |_, cx| cx.focus_self());
5141 workspace.read_with(cx, |workspace, _| {
5142 assert_eq!(workspace.zoomed, None);
5143 assert_eq!(workspace.zoomed_position, None);
5144 });
5145
5146 // When focus is transferred back to the panel, it is zoomed again.
5147 panel_1.update(cx, |_, cx| cx.focus_self());
5148 workspace.read_with(cx, |workspace, _| {
5149 assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
5150 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5151 });
5152
5153 // Emitting a ZoomOut event unzooms the panel.
5154 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomOut));
5155 workspace.read_with(cx, |workspace, _| {
5156 assert_eq!(workspace.zoomed, None);
5157 assert_eq!(workspace.zoomed_position, None);
5158 });
5159
5160 // Emit closed event on panel 1, which is active
5161 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed));
5162
5163 // Now the left dock is closed, because panel_1 was the active panel
5164 workspace.read_with(cx, |workspace, cx| {
5165 let right_dock = workspace.right_dock();
5166 assert!(!right_dock.read(cx).is_open());
5167 });
5168 }
5169
5170 pub fn init_test(cx: &mut TestAppContext) {
5171 cx.foreground().forbid_parking();
5172 cx.update(|cx| {
5173 cx.set_global(SettingsStore::test(cx));
5174 theme::init((), cx);
5175 language::init(cx);
5176 crate::init_settings(cx);
5177 Project::init_settings(cx);
5178 });
5179 }
5180}