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