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 let item = state
3046 .active_view_id
3047 .and_then(|id| state.items_by_leader_view_id.get(&id));
3048 let shared_screen = self.shared_screen_for_peer(leader_id, pane, cx);
3049
3050 if leader_in_this_app {
3051 if let Some(item) = item {
3052 if leader_in_this_project || !item.is_project_item(cx) {
3053 items_to_activate.push((pane.clone(), item.boxed_clone()));
3054 }
3055 } else if let Some(shared_screen) = shared_screen {
3056 items_to_activate.push((pane.clone(), Box::new(shared_screen)));
3057 }
3058 } else if let Some(shared_screen) = shared_screen {
3059 items_to_activate.push((pane.clone(), Box::new(shared_screen)));
3060 }
3061 }
3062
3063 for (pane, item) in items_to_activate {
3064 let pane_was_focused = pane.read(cx).has_focus();
3065 if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) {
3066 pane.update(cx, |pane, cx| pane.activate_item(index, false, false, cx));
3067 } else {
3068 pane.update(cx, |pane, cx| {
3069 pane.add_item(item.boxed_clone(), false, false, None, cx)
3070 });
3071 }
3072
3073 if pane_was_focused {
3074 pane.update(cx, |pane, cx| pane.focus_active_item(cx));
3075 }
3076 }
3077
3078 None
3079 }
3080
3081 fn shared_screen_for_peer(
3082 &self,
3083 peer_id: PeerId,
3084 pane: &ViewHandle<Pane>,
3085 cx: &mut ViewContext<Self>,
3086 ) -> Option<ViewHandle<SharedScreen>> {
3087 let call = self.active_call()?;
3088 let room = call.read(cx).room()?.read(cx);
3089 let participant = room.remote_participant_for_peer_id(peer_id)?;
3090 let track = participant.video_tracks.values().next()?.clone();
3091 let user = participant.user.clone();
3092
3093 for item in pane.read(cx).items_of_type::<SharedScreen>() {
3094 if item.read(cx).peer_id == peer_id {
3095 return Some(item);
3096 }
3097 }
3098
3099 Some(cx.add_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx)))
3100 }
3101
3102 pub fn on_window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
3103 if active {
3104 cx.background()
3105 .spawn(persistence::DB.update_timestamp(self.database_id()))
3106 .detach();
3107 } else {
3108 for pane in &self.panes {
3109 pane.update(cx, |pane, cx| {
3110 if let Some(item) = pane.active_item() {
3111 item.workspace_deactivated(cx);
3112 }
3113 if matches!(
3114 settings::get::<WorkspaceSettings>(cx).autosave,
3115 AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
3116 ) {
3117 for item in pane.items() {
3118 Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
3119 .detach_and_log_err(cx);
3120 }
3121 }
3122 });
3123 }
3124 }
3125 }
3126
3127 fn active_call(&self) -> Option<&ModelHandle<ActiveCall>> {
3128 self.active_call.as_ref().map(|(call, _)| call)
3129 }
3130
3131 fn on_active_call_event(
3132 &mut self,
3133 _: ModelHandle<ActiveCall>,
3134 event: &call::room::Event,
3135 cx: &mut ViewContext<Self>,
3136 ) {
3137 match event {
3138 call::room::Event::ParticipantLocationChanged { participant_id }
3139 | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
3140 self.leader_updated(*participant_id, cx);
3141 }
3142 _ => {}
3143 }
3144 }
3145
3146 pub fn database_id(&self) -> WorkspaceId {
3147 self.database_id
3148 }
3149
3150 fn location(&self, cx: &AppContext) -> Option<WorkspaceLocation> {
3151 let project = self.project().read(cx);
3152
3153 if project.is_local() {
3154 Some(
3155 project
3156 .visible_worktrees(cx)
3157 .map(|worktree| worktree.read(cx).abs_path())
3158 .collect::<Vec<_>>()
3159 .into(),
3160 )
3161 } else {
3162 None
3163 }
3164 }
3165
3166 fn remove_panes(&mut self, member: Member, cx: &mut ViewContext<Workspace>) {
3167 match member {
3168 Member::Axis(PaneAxis { members, .. }) => {
3169 for child in members.iter() {
3170 self.remove_panes(child.clone(), cx)
3171 }
3172 }
3173 Member::Pane(pane) => {
3174 self.force_remove_pane(&pane, cx);
3175 }
3176 }
3177 }
3178
3179 fn force_remove_pane(&mut self, pane: &ViewHandle<Pane>, cx: &mut ViewContext<Workspace>) {
3180 self.panes.retain(|p| p != pane);
3181 cx.focus(self.panes.last().unwrap());
3182 if self.last_active_center_pane == Some(pane.downgrade()) {
3183 self.last_active_center_pane = None;
3184 }
3185 cx.notify();
3186 }
3187
3188 fn schedule_serialize(&mut self, cx: &mut ViewContext<Self>) {
3189 self._schedule_serialize = Some(cx.spawn(|this, cx| async move {
3190 cx.background().timer(Duration::from_millis(100)).await;
3191 this.read_with(&cx, |this, cx| this.serialize_workspace(cx))
3192 .ok();
3193 }));
3194 }
3195
3196 fn serialize_workspace(&self, cx: &ViewContext<Self>) {
3197 fn serialize_pane_handle(
3198 pane_handle: &ViewHandle<Pane>,
3199 cx: &AppContext,
3200 ) -> SerializedPane {
3201 let (items, active) = {
3202 let pane = pane_handle.read(cx);
3203 let active_item_id = pane.active_item().map(|item| item.id());
3204 (
3205 pane.items()
3206 .filter_map(|item_handle| {
3207 Some(SerializedItem {
3208 kind: Arc::from(item_handle.serialized_item_kind()?),
3209 item_id: item_handle.id(),
3210 active: Some(item_handle.id()) == active_item_id,
3211 })
3212 })
3213 .collect::<Vec<_>>(),
3214 pane.has_focus(),
3215 )
3216 };
3217
3218 SerializedPane::new(items, active)
3219 }
3220
3221 fn build_serialized_pane_group(
3222 pane_group: &Member,
3223 cx: &AppContext,
3224 ) -> SerializedPaneGroup {
3225 match pane_group {
3226 Member::Axis(PaneAxis {
3227 axis,
3228 members,
3229 flexes,
3230 bounding_boxes: _,
3231 }) => SerializedPaneGroup::Group {
3232 axis: *axis,
3233 children: members
3234 .iter()
3235 .map(|member| build_serialized_pane_group(member, cx))
3236 .collect::<Vec<_>>(),
3237 flexes: Some(flexes.borrow().clone()),
3238 },
3239 Member::Pane(pane_handle) => {
3240 SerializedPaneGroup::Pane(serialize_pane_handle(&pane_handle, cx))
3241 }
3242 }
3243 }
3244
3245 fn build_serialized_docks(this: &Workspace, cx: &ViewContext<Workspace>) -> DockStructure {
3246 let left_dock = this.left_dock.read(cx);
3247 let left_visible = left_dock.is_open();
3248 let left_active_panel = left_dock.visible_panel().and_then(|panel| {
3249 Some(
3250 cx.view_ui_name(panel.as_any().window(), panel.id())?
3251 .to_string(),
3252 )
3253 });
3254 let left_dock_zoom = left_dock
3255 .visible_panel()
3256 .map(|panel| panel.is_zoomed(cx))
3257 .unwrap_or(false);
3258
3259 let right_dock = this.right_dock.read(cx);
3260 let right_visible = right_dock.is_open();
3261 let right_active_panel = right_dock.visible_panel().and_then(|panel| {
3262 Some(
3263 cx.view_ui_name(panel.as_any().window(), panel.id())?
3264 .to_string(),
3265 )
3266 });
3267 let right_dock_zoom = right_dock
3268 .visible_panel()
3269 .map(|panel| panel.is_zoomed(cx))
3270 .unwrap_or(false);
3271
3272 let bottom_dock = this.bottom_dock.read(cx);
3273 let bottom_visible = bottom_dock.is_open();
3274 let bottom_active_panel = bottom_dock.visible_panel().and_then(|panel| {
3275 Some(
3276 cx.view_ui_name(panel.as_any().window(), panel.id())?
3277 .to_string(),
3278 )
3279 });
3280 let bottom_dock_zoom = bottom_dock
3281 .visible_panel()
3282 .map(|panel| panel.is_zoomed(cx))
3283 .unwrap_or(false);
3284
3285 DockStructure {
3286 left: DockData {
3287 visible: left_visible,
3288 active_panel: left_active_panel,
3289 zoom: left_dock_zoom,
3290 },
3291 right: DockData {
3292 visible: right_visible,
3293 active_panel: right_active_panel,
3294 zoom: right_dock_zoom,
3295 },
3296 bottom: DockData {
3297 visible: bottom_visible,
3298 active_panel: bottom_active_panel,
3299 zoom: bottom_dock_zoom,
3300 },
3301 }
3302 }
3303
3304 if let Some(location) = self.location(cx) {
3305 // Load bearing special case:
3306 // - with_local_workspace() relies on this to not have other stuff open
3307 // when you open your log
3308 if !location.paths().is_empty() {
3309 let center_group = build_serialized_pane_group(&self.center.root, cx);
3310 let docks = build_serialized_docks(self, cx);
3311
3312 let serialized_workspace = SerializedWorkspace {
3313 id: self.database_id,
3314 location,
3315 center_group,
3316 bounds: Default::default(),
3317 display: Default::default(),
3318 docks,
3319 };
3320
3321 cx.background()
3322 .spawn(persistence::DB.save_workspace(serialized_workspace))
3323 .detach();
3324 }
3325 }
3326 }
3327
3328 pub(crate) fn load_workspace(
3329 workspace: WeakViewHandle<Workspace>,
3330 serialized_workspace: SerializedWorkspace,
3331 paths_to_open: Vec<Option<ProjectPath>>,
3332 cx: &mut AppContext,
3333 ) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
3334 cx.spawn(|mut cx| async move {
3335 let result = async_iife! {{
3336 let (project, old_center_pane) =
3337 workspace.read_with(&cx, |workspace, _| {
3338 (
3339 workspace.project().clone(),
3340 workspace.last_active_center_pane.clone(),
3341 )
3342 })?;
3343
3344 let mut center_items = None;
3345 let mut center_group = None;
3346 // Traverse the splits tree and add to things
3347 if let Some((group, active_pane, items)) = serialized_workspace
3348 .center_group
3349 .deserialize(&project, serialized_workspace.id, &workspace, &mut cx)
3350 .await {
3351 center_items = Some(items);
3352 center_group = Some((group, active_pane))
3353 }
3354
3355 let resulting_list = cx.read(|cx| {
3356 let mut opened_items = center_items
3357 .unwrap_or_default()
3358 .into_iter()
3359 .filter_map(|item| {
3360 let item = item?;
3361 let project_path = item.project_path(cx)?;
3362 Some((project_path, item))
3363 })
3364 .collect::<HashMap<_, _>>();
3365
3366 paths_to_open
3367 .into_iter()
3368 .map(|path_to_open| {
3369 path_to_open.map(|path_to_open| {
3370 Ok(opened_items.remove(&path_to_open))
3371 })
3372 .transpose()
3373 .map(|item| item.flatten())
3374 .transpose()
3375 })
3376 .collect::<Vec<_>>()
3377 });
3378
3379 // Remove old panes from workspace panes list
3380 workspace.update(&mut cx, |workspace, cx| {
3381 if let Some((center_group, active_pane)) = center_group {
3382 workspace.remove_panes(workspace.center.root.clone(), cx);
3383
3384 // Swap workspace center group
3385 workspace.center = PaneGroup::with_root(center_group);
3386
3387 // Change the focus to the workspace first so that we retrigger focus in on the pane.
3388 cx.focus_self();
3389
3390 if let Some(active_pane) = active_pane {
3391 cx.focus(&active_pane);
3392 } else {
3393 cx.focus(workspace.panes.last().unwrap());
3394 }
3395 } else {
3396 let old_center_handle = old_center_pane.and_then(|weak| weak.upgrade(cx));
3397 if let Some(old_center_handle) = old_center_handle {
3398 cx.focus(&old_center_handle)
3399 } else {
3400 cx.focus_self()
3401 }
3402 }
3403
3404 let docks = serialized_workspace.docks;
3405 workspace.left_dock.update(cx, |dock, cx| {
3406 dock.set_open(docks.left.visible, cx);
3407 if let Some(active_panel) = docks.left.active_panel {
3408 if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
3409 dock.activate_panel(ix, cx);
3410 }
3411 }
3412 dock.active_panel()
3413 .map(|panel| {
3414 panel.set_zoomed(docks.left.zoom, cx)
3415 });
3416 if docks.left.visible && docks.left.zoom {
3417 cx.focus_self()
3418 }
3419 });
3420 // TODO: I think the bug is that setting zoom or active undoes the bottom zoom or something
3421 workspace.right_dock.update(cx, |dock, cx| {
3422 dock.set_open(docks.right.visible, cx);
3423 if let Some(active_panel) = docks.right.active_panel {
3424 if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
3425 dock.activate_panel(ix, cx);
3426
3427 }
3428 }
3429 dock.active_panel()
3430 .map(|panel| {
3431 panel.set_zoomed(docks.right.zoom, cx)
3432 });
3433
3434 if docks.right.visible && docks.right.zoom {
3435 cx.focus_self()
3436 }
3437 });
3438 workspace.bottom_dock.update(cx, |dock, cx| {
3439 dock.set_open(docks.bottom.visible, cx);
3440 if let Some(active_panel) = docks.bottom.active_panel {
3441 if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
3442 dock.activate_panel(ix, cx);
3443 }
3444 }
3445
3446 dock.active_panel()
3447 .map(|panel| {
3448 panel.set_zoomed(docks.bottom.zoom, cx)
3449 });
3450
3451 if docks.bottom.visible && docks.bottom.zoom {
3452 cx.focus_self()
3453 }
3454 });
3455
3456
3457 cx.notify();
3458 })?;
3459
3460 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
3461 workspace.read_with(&cx, |workspace, cx| workspace.serialize_workspace(cx))?;
3462
3463 Ok::<_, anyhow::Error>(resulting_list)
3464 }};
3465
3466 result.await.unwrap_or_default()
3467 })
3468 }
3469
3470 #[cfg(any(test, feature = "test-support"))]
3471 pub fn test_new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
3472 let client = project.read(cx).client();
3473 let user_store = project.read(cx).user_store();
3474
3475 let channel_store =
3476 cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
3477 let app_state = Arc::new(AppState {
3478 languages: project.read(cx).languages().clone(),
3479 client,
3480 user_store,
3481 channel_store,
3482 fs: project.read(cx).fs().clone(),
3483 build_window_options: |_, _, _| Default::default(),
3484 initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
3485 background_actions: || &[],
3486 });
3487 Self::new(0, project, app_state, cx)
3488 }
3489
3490 fn render_dock(&self, position: DockPosition, cx: &WindowContext) -> Option<AnyElement<Self>> {
3491 let dock = match position {
3492 DockPosition::Left => &self.left_dock,
3493 DockPosition::Right => &self.right_dock,
3494 DockPosition::Bottom => &self.bottom_dock,
3495 };
3496 let active_panel = dock.read(cx).visible_panel()?;
3497 let element = if Some(active_panel.id()) == self.zoomed.as_ref().map(|zoomed| zoomed.id()) {
3498 dock.read(cx).render_placeholder(cx)
3499 } else {
3500 ChildView::new(dock, cx).into_any()
3501 };
3502
3503 Some(
3504 element
3505 .constrained()
3506 .dynamically(move |constraint, _, cx| match position {
3507 DockPosition::Left | DockPosition::Right => SizeConstraint::new(
3508 Vector2F::new(20., constraint.min.y()),
3509 Vector2F::new(cx.window_size().x() * 0.8, constraint.max.y()),
3510 ),
3511 DockPosition::Bottom => SizeConstraint::new(
3512 Vector2F::new(constraint.min.x(), 20.),
3513 Vector2F::new(constraint.max.x(), cx.window_size().y() * 0.8),
3514 ),
3515 })
3516 .into_any(),
3517 )
3518 }
3519}
3520
3521fn window_bounds_env_override(cx: &AsyncAppContext) -> Option<WindowBounds> {
3522 ZED_WINDOW_POSITION
3523 .zip(*ZED_WINDOW_SIZE)
3524 .map(|(position, size)| {
3525 WindowBounds::Fixed(RectF::new(
3526 cx.platform().screens()[0].bounds().origin() + position,
3527 size,
3528 ))
3529 })
3530}
3531
3532async fn open_items(
3533 serialized_workspace: Option<SerializedWorkspace>,
3534 workspace: &WeakViewHandle<Workspace>,
3535 mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
3536 app_state: Arc<AppState>,
3537 mut cx: AsyncAppContext,
3538) -> Vec<Option<anyhow::Result<Box<dyn ItemHandle>>>> {
3539 let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
3540
3541 if let Some(serialized_workspace) = serialized_workspace {
3542 let workspace = workspace.clone();
3543 let restored_items = cx
3544 .update(|cx| {
3545 Workspace::load_workspace(
3546 workspace,
3547 serialized_workspace,
3548 project_paths_to_open
3549 .iter()
3550 .map(|(_, project_path)| project_path)
3551 .cloned()
3552 .collect(),
3553 cx,
3554 )
3555 })
3556 .await;
3557
3558 let restored_project_paths = cx.read(|cx| {
3559 restored_items
3560 .iter()
3561 .filter_map(|item| item.as_ref()?.as_ref().ok()?.project_path(cx))
3562 .collect::<HashSet<_>>()
3563 });
3564
3565 opened_items = restored_items;
3566 project_paths_to_open
3567 .iter_mut()
3568 .for_each(|(_, project_path)| {
3569 if let Some(project_path_to_open) = project_path {
3570 if restored_project_paths.contains(project_path_to_open) {
3571 *project_path = None;
3572 }
3573 }
3574 });
3575 } else {
3576 for _ in 0..project_paths_to_open.len() {
3577 opened_items.push(None);
3578 }
3579 }
3580 assert!(opened_items.len() == project_paths_to_open.len());
3581
3582 let tasks =
3583 project_paths_to_open
3584 .into_iter()
3585 .enumerate()
3586 .map(|(i, (abs_path, project_path))| {
3587 let workspace = workspace.clone();
3588 cx.spawn(|mut cx| {
3589 let fs = app_state.fs.clone();
3590 async move {
3591 let file_project_path = project_path?;
3592 if fs.is_file(&abs_path).await {
3593 Some((
3594 i,
3595 workspace
3596 .update(&mut cx, |workspace, cx| {
3597 workspace.open_path(file_project_path, None, true, cx)
3598 })
3599 .log_err()?
3600 .await,
3601 ))
3602 } else {
3603 None
3604 }
3605 }
3606 })
3607 });
3608
3609 for maybe_opened_path in futures::future::join_all(tasks.into_iter())
3610 .await
3611 .into_iter()
3612 {
3613 if let Some((i, path_open_result)) = maybe_opened_path {
3614 opened_items[i] = Some(path_open_result);
3615 }
3616 }
3617
3618 opened_items
3619}
3620
3621fn notify_of_new_dock(workspace: &WeakViewHandle<Workspace>, cx: &mut AsyncAppContext) {
3622 const NEW_PANEL_BLOG_POST: &str = "https://zed.dev/blog/new-panel-system";
3623 const NEW_DOCK_HINT_KEY: &str = "show_new_dock_key";
3624 const MESSAGE_ID: usize = 2;
3625
3626 if workspace
3627 .read_with(cx, |workspace, cx| {
3628 workspace.has_shown_notification_once::<MessageNotification>(MESSAGE_ID, cx)
3629 })
3630 .unwrap_or(false)
3631 {
3632 return;
3633 }
3634
3635 if db::kvp::KEY_VALUE_STORE
3636 .read_kvp(NEW_DOCK_HINT_KEY)
3637 .ok()
3638 .flatten()
3639 .is_some()
3640 {
3641 if !workspace
3642 .read_with(cx, |workspace, cx| {
3643 workspace.has_shown_notification_once::<MessageNotification>(MESSAGE_ID, cx)
3644 })
3645 .unwrap_or(false)
3646 {
3647 cx.update(|cx| {
3648 cx.update_global::<NotificationTracker, _, _>(|tracker, _| {
3649 let entry = tracker
3650 .entry(TypeId::of::<MessageNotification>())
3651 .or_default();
3652 if !entry.contains(&MESSAGE_ID) {
3653 entry.push(MESSAGE_ID);
3654 }
3655 });
3656 });
3657 }
3658
3659 return;
3660 }
3661
3662 cx.spawn(|_| async move {
3663 db::kvp::KEY_VALUE_STORE
3664 .write_kvp(NEW_DOCK_HINT_KEY.to_string(), "seen".to_string())
3665 .await
3666 .ok();
3667 })
3668 .detach();
3669
3670 workspace
3671 .update(cx, |workspace, cx| {
3672 workspace.show_notification_once(2, cx, |cx| {
3673 cx.add_view(|_| {
3674 MessageNotification::new_element(|text, _| {
3675 Text::new(
3676 "Looking for the dock? Try ctrl-`!\nshift-escape now zooms your pane.",
3677 text,
3678 )
3679 .with_custom_runs(vec![26..32, 34..46], |_, bounds, cx| {
3680 let code_span_background_color = settings::get::<ThemeSettings>(cx)
3681 .theme
3682 .editor
3683 .document_highlight_read_background;
3684
3685 cx.scene().push_quad(gpui::Quad {
3686 bounds,
3687 background: Some(code_span_background_color),
3688 border: Default::default(),
3689 corner_radii: (2.0).into(),
3690 })
3691 })
3692 .into_any()
3693 })
3694 .with_click_message("Read more about the new panel system")
3695 .on_click(|cx| cx.platform().open_url(NEW_PANEL_BLOG_POST))
3696 })
3697 })
3698 })
3699 .ok();
3700}
3701
3702fn notify_if_database_failed(workspace: &WeakViewHandle<Workspace>, cx: &mut AsyncAppContext) {
3703 const REPORT_ISSUE_URL: &str ="https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";
3704
3705 workspace
3706 .update(cx, |workspace, cx| {
3707 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
3708 workspace.show_notification_once(0, cx, |cx| {
3709 cx.add_view(|_| {
3710 MessageNotification::new("Failed to load the database file.")
3711 .with_click_message("Click to let us know about this error")
3712 .on_click(|cx| cx.platform().open_url(REPORT_ISSUE_URL))
3713 })
3714 });
3715 }
3716 })
3717 .log_err();
3718}
3719
3720impl Entity for Workspace {
3721 type Event = Event;
3722}
3723
3724impl View for Workspace {
3725 fn ui_name() -> &'static str {
3726 "Workspace"
3727 }
3728
3729 fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
3730 let theme = theme::current(cx).clone();
3731 Stack::new()
3732 .with_child(
3733 Flex::column()
3734 .with_child(self.render_titlebar(&theme, cx))
3735 .with_child(
3736 Stack::new()
3737 .with_child({
3738 let project = self.project.clone();
3739 Flex::row()
3740 .with_children(self.render_dock(DockPosition::Left, cx))
3741 .with_child(
3742 Flex::column()
3743 .with_child(
3744 FlexItem::new(
3745 self.center.render(
3746 &project,
3747 &theme,
3748 &self.follower_states_by_leader,
3749 self.active_call(),
3750 self.active_pane(),
3751 self.zoomed
3752 .as_ref()
3753 .and_then(|zoomed| zoomed.upgrade(cx))
3754 .as_ref(),
3755 &self.app_state,
3756 cx,
3757 ),
3758 )
3759 .flex(1., true),
3760 )
3761 .with_children(
3762 self.render_dock(DockPosition::Bottom, cx),
3763 )
3764 .flex(1., true),
3765 )
3766 .with_children(self.render_dock(DockPosition::Right, cx))
3767 })
3768 .with_child(Overlay::new(
3769 Stack::new()
3770 .with_children(self.zoomed.as_ref().and_then(|zoomed| {
3771 enum ZoomBackground {}
3772 let zoomed = zoomed.upgrade(cx)?;
3773
3774 let mut foreground_style =
3775 theme.workspace.zoomed_pane_foreground;
3776 if let Some(zoomed_dock_position) = self.zoomed_position {
3777 foreground_style =
3778 theme.workspace.zoomed_panel_foreground;
3779 let margin = foreground_style.margin.top;
3780 let border = foreground_style.border.top;
3781
3782 // Only include a margin and border on the opposite side.
3783 foreground_style.margin.top = 0.;
3784 foreground_style.margin.left = 0.;
3785 foreground_style.margin.bottom = 0.;
3786 foreground_style.margin.right = 0.;
3787 foreground_style.border.top = false;
3788 foreground_style.border.left = false;
3789 foreground_style.border.bottom = false;
3790 foreground_style.border.right = false;
3791 match zoomed_dock_position {
3792 DockPosition::Left => {
3793 foreground_style.margin.right = margin;
3794 foreground_style.border.right = border;
3795 }
3796 DockPosition::Right => {
3797 foreground_style.margin.left = margin;
3798 foreground_style.border.left = border;
3799 }
3800 DockPosition::Bottom => {
3801 foreground_style.margin.top = margin;
3802 foreground_style.border.top = border;
3803 }
3804 }
3805 }
3806
3807 Some(
3808 ChildView::new(&zoomed, cx)
3809 .contained()
3810 .with_style(foreground_style)
3811 .aligned()
3812 .contained()
3813 .with_style(theme.workspace.zoomed_background)
3814 .mouse::<ZoomBackground>(0)
3815 .capture_all()
3816 .on_down(
3817 MouseButton::Left,
3818 |_, this: &mut Self, cx| {
3819 this.zoom_out(cx);
3820 },
3821 ),
3822 )
3823 }))
3824 .with_children(self.modal.as_ref().map(|modal| {
3825 // Prevent clicks within the modal from falling
3826 // through to the rest of the workspace.
3827 enum ModalBackground {}
3828 MouseEventHandler::new::<ModalBackground, _>(
3829 0,
3830 cx,
3831 |_, cx| ChildView::new(modal.view.as_any(), cx),
3832 )
3833 .on_click(MouseButton::Left, |_, _, _| {})
3834 .contained()
3835 .with_style(theme.workspace.modal)
3836 .aligned()
3837 .top()
3838 }))
3839 .with_children(self.render_notifications(&theme.workspace, cx)),
3840 ))
3841 .provide_resize_bounds::<WorkspaceBounds>()
3842 .flex(1.0, true),
3843 )
3844 .with_child(ChildView::new(&self.status_bar, cx))
3845 .contained()
3846 .with_background_color(theme.workspace.background),
3847 )
3848 .with_children(DragAndDrop::render(cx))
3849 .with_children(self.render_disconnected_overlay(cx))
3850 .into_any_named("workspace")
3851 }
3852
3853 fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
3854 if cx.is_self_focused() {
3855 cx.focus(&self.active_pane);
3856 }
3857 }
3858
3859 fn modifiers_changed(&mut self, e: &ModifiersChangedEvent, cx: &mut ViewContext<Self>) -> bool {
3860 DragAndDrop::<Workspace>::update_modifiers(e.modifiers, cx)
3861 }
3862}
3863
3864impl ViewId {
3865 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
3866 Ok(Self {
3867 creator: message
3868 .creator
3869 .ok_or_else(|| anyhow!("creator is missing"))?,
3870 id: message.id,
3871 })
3872 }
3873
3874 pub(crate) fn to_proto(&self) -> proto::ViewId {
3875 proto::ViewId {
3876 creator: Some(self.creator),
3877 id: self.id,
3878 }
3879 }
3880}
3881
3882pub trait WorkspaceHandle {
3883 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
3884}
3885
3886impl WorkspaceHandle for ViewHandle<Workspace> {
3887 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
3888 self.read(cx)
3889 .worktrees(cx)
3890 .flat_map(|worktree| {
3891 let worktree_id = worktree.read(cx).id();
3892 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
3893 worktree_id,
3894 path: f.path.clone(),
3895 })
3896 })
3897 .collect::<Vec<_>>()
3898 }
3899}
3900
3901impl std::fmt::Debug for OpenPaths {
3902 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3903 f.debug_struct("OpenPaths")
3904 .field("paths", &self.paths)
3905 .finish()
3906 }
3907}
3908
3909pub struct WorkspaceCreated(pub WeakViewHandle<Workspace>);
3910
3911pub fn activate_workspace_for_project(
3912 cx: &mut AsyncAppContext,
3913 predicate: impl Fn(&mut Project, &mut ModelContext<Project>) -> bool,
3914) -> Option<WeakViewHandle<Workspace>> {
3915 for window in cx.windows() {
3916 let handle = window
3917 .update(cx, |cx| {
3918 if let Some(workspace_handle) = cx.root_view().clone().downcast::<Workspace>() {
3919 let project = workspace_handle.read(cx).project.clone();
3920 if project.update(cx, &predicate) {
3921 cx.activate_window();
3922 return Some(workspace_handle.clone());
3923 }
3924 }
3925 None
3926 })
3927 .flatten();
3928
3929 if let Some(handle) = handle {
3930 return Some(handle.downgrade());
3931 }
3932 }
3933 None
3934}
3935
3936pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
3937 DB.last_workspace().await.log_err().flatten()
3938}
3939
3940#[allow(clippy::type_complexity)]
3941pub fn open_paths(
3942 abs_paths: &[PathBuf],
3943 app_state: &Arc<AppState>,
3944 requesting_window: Option<WindowHandle<Workspace>>,
3945 cx: &mut AppContext,
3946) -> Task<
3947 Result<(
3948 WeakViewHandle<Workspace>,
3949 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
3950 )>,
3951> {
3952 let app_state = app_state.clone();
3953 let abs_paths = abs_paths.to_vec();
3954 cx.spawn(|mut cx| async move {
3955 // Open paths in existing workspace if possible
3956 let existing = activate_workspace_for_project(&mut cx, |project, cx| {
3957 project.contains_paths(&abs_paths, cx)
3958 });
3959
3960 if let Some(existing) = existing {
3961 Ok((
3962 existing.clone(),
3963 existing
3964 .update(&mut cx, |workspace, cx| {
3965 workspace.open_paths(abs_paths, true, cx)
3966 })?
3967 .await,
3968 ))
3969 } else {
3970 Ok(cx
3971 .update(|cx| {
3972 Workspace::new_local(abs_paths, app_state.clone(), requesting_window, cx)
3973 })
3974 .await)
3975 }
3976 })
3977}
3978
3979pub fn open_new(
3980 app_state: &Arc<AppState>,
3981 cx: &mut AppContext,
3982 init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static,
3983) -> Task<()> {
3984 let task = Workspace::new_local(Vec::new(), app_state.clone(), None, cx);
3985 cx.spawn(|mut cx| async move {
3986 let (workspace, opened_paths) = task.await;
3987
3988 workspace
3989 .update(&mut cx, |workspace, cx| {
3990 if opened_paths.is_empty() {
3991 init(workspace, cx)
3992 }
3993 })
3994 .log_err();
3995 })
3996}
3997
3998pub fn create_and_open_local_file(
3999 path: &'static Path,
4000 cx: &mut ViewContext<Workspace>,
4001 default_content: impl 'static + Send + FnOnce() -> Rope,
4002) -> Task<Result<Box<dyn ItemHandle>>> {
4003 cx.spawn(|workspace, mut cx| async move {
4004 let fs = workspace.read_with(&cx, |workspace, _| workspace.app_state().fs.clone())?;
4005 if !fs.is_file(path).await {
4006 fs.create_file(path, Default::default()).await?;
4007 fs.save(path, &default_content(), Default::default())
4008 .await?;
4009 }
4010
4011 let mut items = workspace
4012 .update(&mut cx, |workspace, cx| {
4013 workspace.with_local_workspace(cx, |workspace, cx| {
4014 workspace.open_paths(vec![path.to_path_buf()], false, cx)
4015 })
4016 })?
4017 .await?
4018 .await;
4019
4020 let item = items.pop().flatten();
4021 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
4022 })
4023}
4024
4025pub fn join_remote_project(
4026 project_id: u64,
4027 follow_user_id: u64,
4028 app_state: Arc<AppState>,
4029 cx: &mut AppContext,
4030) -> Task<Result<()>> {
4031 cx.spawn(|mut cx| async move {
4032 let existing_workspace = cx
4033 .windows()
4034 .into_iter()
4035 .find_map(|window| {
4036 window.downcast::<Workspace>().and_then(|window| {
4037 window.read_root_with(&cx, |workspace, cx| {
4038 if workspace.project().read(cx).remote_id() == Some(project_id) {
4039 Some(cx.handle().downgrade())
4040 } else {
4041 None
4042 }
4043 })
4044 })
4045 })
4046 .flatten();
4047
4048 let workspace = if let Some(existing_workspace) = existing_workspace {
4049 existing_workspace
4050 } else {
4051 let active_call = cx.read(ActiveCall::global);
4052 let room = active_call
4053 .read_with(&cx, |call, _| call.room().cloned())
4054 .ok_or_else(|| anyhow!("not in a call"))?;
4055 let project = room
4056 .update(&mut cx, |room, cx| {
4057 room.join_project(
4058 project_id,
4059 app_state.languages.clone(),
4060 app_state.fs.clone(),
4061 cx,
4062 )
4063 })
4064 .await?;
4065
4066 let window_bounds_override = window_bounds_env_override(&cx);
4067 let window = cx.add_window(
4068 (app_state.build_window_options)(
4069 window_bounds_override,
4070 None,
4071 cx.platform().as_ref(),
4072 ),
4073 |cx| Workspace::new(0, project, app_state.clone(), cx),
4074 );
4075 let workspace = window.root(&cx).unwrap();
4076 (app_state.initialize_workspace)(
4077 workspace.downgrade(),
4078 false,
4079 app_state.clone(),
4080 cx.clone(),
4081 )
4082 .await
4083 .log_err();
4084
4085 workspace.downgrade()
4086 };
4087
4088 workspace.window().activate(&mut cx);
4089 cx.platform().activate(true);
4090
4091 workspace.update(&mut cx, |workspace, cx| {
4092 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
4093 let follow_peer_id = room
4094 .read(cx)
4095 .remote_participants()
4096 .iter()
4097 .find(|(_, participant)| participant.user.id == follow_user_id)
4098 .map(|(_, p)| p.peer_id)
4099 .or_else(|| {
4100 // If we couldn't follow the given user, follow the host instead.
4101 let collaborator = workspace
4102 .project()
4103 .read(cx)
4104 .collaborators()
4105 .values()
4106 .find(|collaborator| collaborator.replica_id == 0)?;
4107 Some(collaborator.peer_id)
4108 });
4109
4110 if let Some(follow_peer_id) = follow_peer_id {
4111 if !workspace.is_being_followed(follow_peer_id) {
4112 workspace
4113 .toggle_follow(follow_peer_id, cx)
4114 .map(|follow| follow.detach_and_log_err(cx));
4115 }
4116 }
4117 }
4118 })?;
4119
4120 anyhow::Ok(())
4121 })
4122}
4123
4124pub fn restart(_: &Restart, cx: &mut AppContext) {
4125 let should_confirm = settings::get::<WorkspaceSettings>(cx).confirm_quit;
4126 cx.spawn(|mut cx| async move {
4127 let mut workspace_windows = cx
4128 .windows()
4129 .into_iter()
4130 .filter_map(|window| window.downcast::<Workspace>())
4131 .collect::<Vec<_>>();
4132
4133 // If multiple windows have unsaved changes, and need a save prompt,
4134 // prompt in the active window before switching to a different window.
4135 workspace_windows.sort_by_key(|window| window.is_active(&cx) == Some(false));
4136
4137 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
4138 let answer = window.prompt(
4139 PromptLevel::Info,
4140 "Are you sure you want to restart?",
4141 &["Restart", "Cancel"],
4142 &mut cx,
4143 );
4144
4145 if let Some(mut answer) = answer {
4146 let answer = answer.next().await;
4147 if answer != Some(0) {
4148 return Ok(());
4149 }
4150 }
4151 }
4152
4153 // If the user cancels any save prompt, then keep the app open.
4154 for window in workspace_windows {
4155 if let Some(should_close) = window.update_root(&mut cx, |workspace, cx| {
4156 workspace.prepare_to_close(true, cx)
4157 }) {
4158 if !should_close.await? {
4159 return Ok(());
4160 }
4161 }
4162 }
4163 cx.platform().restart();
4164 anyhow::Ok(())
4165 })
4166 .detach_and_log_err(cx);
4167}
4168
4169fn parse_pixel_position_env_var(value: &str) -> Option<Vector2F> {
4170 let mut parts = value.split(',');
4171 let width: usize = parts.next()?.parse().ok()?;
4172 let height: usize = parts.next()?.parse().ok()?;
4173 Some(vec2f(width as f32, height as f32))
4174}
4175
4176#[cfg(test)]
4177mod tests {
4178 use super::*;
4179 use crate::{
4180 dock::test::{TestPanel, TestPanelEvent},
4181 item::test::{TestItem, TestItemEvent, TestProjectItem},
4182 };
4183 use fs::FakeFs;
4184 use gpui::{executor::Deterministic, test::EmptyView, TestAppContext};
4185 use project::{Project, ProjectEntryId};
4186 use serde_json::json;
4187 use settings::SettingsStore;
4188 use std::{cell::RefCell, rc::Rc};
4189
4190 #[gpui::test]
4191 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
4192 init_test(cx);
4193
4194 let fs = FakeFs::new(cx.background());
4195 let project = Project::test(fs, [], cx).await;
4196 let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
4197 let workspace = window.root(cx);
4198
4199 // Adding an item with no ambiguity renders the tab without detail.
4200 let item1 = window.add_view(cx, |_| {
4201 let mut item = TestItem::new();
4202 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
4203 item
4204 });
4205 workspace.update(cx, |workspace, cx| {
4206 workspace.add_item(Box::new(item1.clone()), cx);
4207 });
4208 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), None));
4209
4210 // Adding an item that creates ambiguity increases the level of detail on
4211 // both tabs.
4212 let item2 = window.add_view(cx, |_| {
4213 let mut item = TestItem::new();
4214 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
4215 item
4216 });
4217 workspace.update(cx, |workspace, cx| {
4218 workspace.add_item(Box::new(item2.clone()), cx);
4219 });
4220 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4221 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4222
4223 // Adding an item that creates ambiguity increases the level of detail only
4224 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
4225 // we stop at the highest detail available.
4226 let item3 = window.add_view(cx, |_| {
4227 let mut item = TestItem::new();
4228 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
4229 item
4230 });
4231 workspace.update(cx, |workspace, cx| {
4232 workspace.add_item(Box::new(item3.clone()), cx);
4233 });
4234 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4235 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
4236 item3.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
4237 }
4238
4239 #[gpui::test]
4240 async fn test_tracking_active_path(cx: &mut TestAppContext) {
4241 init_test(cx);
4242
4243 let fs = FakeFs::new(cx.background());
4244 fs.insert_tree(
4245 "/root1",
4246 json!({
4247 "one.txt": "",
4248 "two.txt": "",
4249 }),
4250 )
4251 .await;
4252 fs.insert_tree(
4253 "/root2",
4254 json!({
4255 "three.txt": "",
4256 }),
4257 )
4258 .await;
4259
4260 let project = Project::test(fs, ["root1".as_ref()], cx).await;
4261 let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
4262 let workspace = window.root(cx);
4263 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4264 let worktree_id = project.read_with(cx, |project, cx| {
4265 project.worktrees(cx).next().unwrap().read(cx).id()
4266 });
4267
4268 let item1 = window.add_view(cx, |cx| {
4269 TestItem::new().with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
4270 });
4271 let item2 = window.add_view(cx, |cx| {
4272 TestItem::new().with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
4273 });
4274
4275 // Add an item to an empty pane
4276 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx));
4277 project.read_with(cx, |project, cx| {
4278 assert_eq!(
4279 project.active_entry(),
4280 project
4281 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
4282 .map(|e| e.id)
4283 );
4284 });
4285 assert_eq!(window.current_title(cx).as_deref(), Some("one.txt β root1"));
4286
4287 // Add a second item to a non-empty pane
4288 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
4289 assert_eq!(window.current_title(cx).as_deref(), Some("two.txt β root1"));
4290 project.read_with(cx, |project, cx| {
4291 assert_eq!(
4292 project.active_entry(),
4293 project
4294 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
4295 .map(|e| e.id)
4296 );
4297 });
4298
4299 // Close the active item
4300 pane.update(cx, |pane, cx| {
4301 pane.close_active_item(&Default::default(), cx).unwrap()
4302 })
4303 .await
4304 .unwrap();
4305 assert_eq!(window.current_title(cx).as_deref(), Some("one.txt β root1"));
4306 project.read_with(cx, |project, cx| {
4307 assert_eq!(
4308 project.active_entry(),
4309 project
4310 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
4311 .map(|e| e.id)
4312 );
4313 });
4314
4315 // Add a project folder
4316 project
4317 .update(cx, |project, cx| {
4318 project.find_or_create_local_worktree("/root2", true, cx)
4319 })
4320 .await
4321 .unwrap();
4322 assert_eq!(
4323 window.current_title(cx).as_deref(),
4324 Some("one.txt β root1, root2")
4325 );
4326
4327 // Remove a project folder
4328 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
4329 assert_eq!(window.current_title(cx).as_deref(), Some("one.txt β root2"));
4330 }
4331
4332 #[gpui::test]
4333 async fn test_close_window(cx: &mut TestAppContext) {
4334 init_test(cx);
4335
4336 let fs = FakeFs::new(cx.background());
4337 fs.insert_tree("/root", json!({ "one": "" })).await;
4338
4339 let project = Project::test(fs, ["root".as_ref()], cx).await;
4340 let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
4341 let workspace = window.root(cx);
4342
4343 // When there are no dirty items, there's nothing to do.
4344 let item1 = window.add_view(cx, |_| TestItem::new());
4345 workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
4346 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
4347 assert!(task.await.unwrap());
4348
4349 // When there are dirty untitled items, prompt to save each one. If the user
4350 // cancels any prompt, then abort.
4351 let item2 = window.add_view(cx, |_| TestItem::new().with_dirty(true));
4352 let item3 = window.add_view(cx, |cx| {
4353 TestItem::new()
4354 .with_dirty(true)
4355 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4356 });
4357 workspace.update(cx, |w, cx| {
4358 w.add_item(Box::new(item2.clone()), cx);
4359 w.add_item(Box::new(item3.clone()), cx);
4360 });
4361 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
4362 cx.foreground().run_until_parked();
4363 window.simulate_prompt_answer(2, cx); // cancel save all
4364 cx.foreground().run_until_parked();
4365 window.simulate_prompt_answer(2, cx); // cancel save all
4366 cx.foreground().run_until_parked();
4367 assert!(!window.has_pending_prompt(cx));
4368 assert!(!task.await.unwrap());
4369 }
4370
4371 #[gpui::test]
4372 async fn test_close_pane_items(cx: &mut TestAppContext) {
4373 init_test(cx);
4374
4375 let fs = FakeFs::new(cx.background());
4376
4377 let project = Project::test(fs, None, cx).await;
4378 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
4379 let workspace = window.root(cx);
4380
4381 let item1 = window.add_view(cx, |cx| {
4382 TestItem::new()
4383 .with_dirty(true)
4384 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4385 });
4386 let item2 = window.add_view(cx, |cx| {
4387 TestItem::new()
4388 .with_dirty(true)
4389 .with_conflict(true)
4390 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
4391 });
4392 let item3 = window.add_view(cx, |cx| {
4393 TestItem::new()
4394 .with_dirty(true)
4395 .with_conflict(true)
4396 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
4397 });
4398 let item4 = window.add_view(cx, |cx| {
4399 TestItem::new()
4400 .with_dirty(true)
4401 .with_project_items(&[TestProjectItem::new_untitled(cx)])
4402 });
4403 let pane = workspace.update(cx, |workspace, cx| {
4404 workspace.add_item(Box::new(item1.clone()), cx);
4405 workspace.add_item(Box::new(item2.clone()), cx);
4406 workspace.add_item(Box::new(item3.clone()), cx);
4407 workspace.add_item(Box::new(item4.clone()), cx);
4408 workspace.active_pane().clone()
4409 });
4410
4411 let close_items = pane.update(cx, |pane, cx| {
4412 pane.activate_item(1, true, true, cx);
4413 assert_eq!(pane.active_item().unwrap().id(), item2.id());
4414 let item1_id = item1.id();
4415 let item3_id = item3.id();
4416 let item4_id = item4.id();
4417 pane.close_items(cx, SaveIntent::Close, move |id| {
4418 [item1_id, item3_id, item4_id].contains(&id)
4419 })
4420 });
4421 cx.foreground().run_until_parked();
4422
4423 assert!(window.has_pending_prompt(cx));
4424 // Ignore "Save all" prompt
4425 window.simulate_prompt_answer(2, cx);
4426 cx.foreground().run_until_parked();
4427 // There's a prompt to save item 1.
4428 pane.read_with(cx, |pane, _| {
4429 assert_eq!(pane.items_len(), 4);
4430 assert_eq!(pane.active_item().unwrap().id(), item1.id());
4431 });
4432 // Confirm saving item 1.
4433 window.simulate_prompt_answer(0, cx);
4434 cx.foreground().run_until_parked();
4435
4436 // Item 1 is saved. There's a prompt to save item 3.
4437 pane.read_with(cx, |pane, cx| {
4438 assert_eq!(item1.read(cx).save_count, 1);
4439 assert_eq!(item1.read(cx).save_as_count, 0);
4440 assert_eq!(item1.read(cx).reload_count, 0);
4441 assert_eq!(pane.items_len(), 3);
4442 assert_eq!(pane.active_item().unwrap().id(), item3.id());
4443 });
4444 assert!(window.has_pending_prompt(cx));
4445
4446 // Cancel saving item 3.
4447 window.simulate_prompt_answer(1, cx);
4448 cx.foreground().run_until_parked();
4449
4450 // Item 3 is reloaded. There's a prompt to save item 4.
4451 pane.read_with(cx, |pane, cx| {
4452 assert_eq!(item3.read(cx).save_count, 0);
4453 assert_eq!(item3.read(cx).save_as_count, 0);
4454 assert_eq!(item3.read(cx).reload_count, 1);
4455 assert_eq!(pane.items_len(), 2);
4456 assert_eq!(pane.active_item().unwrap().id(), item4.id());
4457 });
4458 assert!(window.has_pending_prompt(cx));
4459
4460 // Confirm saving item 4.
4461 window.simulate_prompt_answer(0, cx);
4462 cx.foreground().run_until_parked();
4463
4464 // There's a prompt for a path for item 4.
4465 cx.simulate_new_path_selection(|_| Some(Default::default()));
4466 close_items.await.unwrap();
4467
4468 // The requested items are closed.
4469 pane.read_with(cx, |pane, cx| {
4470 assert_eq!(item4.read(cx).save_count, 0);
4471 assert_eq!(item4.read(cx).save_as_count, 1);
4472 assert_eq!(item4.read(cx).reload_count, 0);
4473 assert_eq!(pane.items_len(), 1);
4474 assert_eq!(pane.active_item().unwrap().id(), item2.id());
4475 });
4476 }
4477
4478 #[gpui::test]
4479 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
4480 init_test(cx);
4481
4482 let fs = FakeFs::new(cx.background());
4483
4484 let project = Project::test(fs, [], cx).await;
4485 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
4486 let workspace = window.root(cx);
4487
4488 // Create several workspace items with single project entries, and two
4489 // workspace items with multiple project entries.
4490 let single_entry_items = (0..=4)
4491 .map(|project_entry_id| {
4492 window.add_view(cx, |cx| {
4493 TestItem::new()
4494 .with_dirty(true)
4495 .with_project_items(&[TestProjectItem::new(
4496 project_entry_id,
4497 &format!("{project_entry_id}.txt"),
4498 cx,
4499 )])
4500 })
4501 })
4502 .collect::<Vec<_>>();
4503 let item_2_3 = window.add_view(cx, |cx| {
4504 TestItem::new()
4505 .with_dirty(true)
4506 .with_singleton(false)
4507 .with_project_items(&[
4508 single_entry_items[2].read(cx).project_items[0].clone(),
4509 single_entry_items[3].read(cx).project_items[0].clone(),
4510 ])
4511 });
4512 let item_3_4 = window.add_view(cx, |cx| {
4513 TestItem::new()
4514 .with_dirty(true)
4515 .with_singleton(false)
4516 .with_project_items(&[
4517 single_entry_items[3].read(cx).project_items[0].clone(),
4518 single_entry_items[4].read(cx).project_items[0].clone(),
4519 ])
4520 });
4521
4522 // Create two panes that contain the following project entries:
4523 // left pane:
4524 // multi-entry items: (2, 3)
4525 // single-entry items: 0, 1, 2, 3, 4
4526 // right pane:
4527 // single-entry items: 1
4528 // multi-entry items: (3, 4)
4529 let left_pane = workspace.update(cx, |workspace, cx| {
4530 let left_pane = workspace.active_pane().clone();
4531 workspace.add_item(Box::new(item_2_3.clone()), cx);
4532 for item in single_entry_items {
4533 workspace.add_item(Box::new(item), cx);
4534 }
4535 left_pane.update(cx, |pane, cx| {
4536 pane.activate_item(2, true, true, cx);
4537 });
4538
4539 workspace
4540 .split_and_clone(left_pane.clone(), SplitDirection::Right, cx)
4541 .unwrap();
4542
4543 left_pane
4544 });
4545
4546 //Need to cause an effect flush in order to respect new focus
4547 workspace.update(cx, |workspace, cx| {
4548 workspace.add_item(Box::new(item_3_4.clone()), cx);
4549 cx.focus(&left_pane);
4550 });
4551
4552 // When closing all of the items in the left pane, we should be prompted twice:
4553 // once for project entry 0, and once for project entry 2. After those two
4554 // prompts, the task should complete.
4555
4556 let close = left_pane.update(cx, |pane, cx| {
4557 pane.close_items(cx, SaveIntent::Close, move |_| true)
4558 });
4559 cx.foreground().run_until_parked();
4560 // Discard "Save all" prompt
4561 window.simulate_prompt_answer(2, cx);
4562
4563 cx.foreground().run_until_parked();
4564 left_pane.read_with(cx, |pane, cx| {
4565 assert_eq!(
4566 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
4567 &[ProjectEntryId::from_proto(0)]
4568 );
4569 });
4570 window.simulate_prompt_answer(0, cx);
4571
4572 cx.foreground().run_until_parked();
4573 left_pane.read_with(cx, |pane, cx| {
4574 assert_eq!(
4575 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
4576 &[ProjectEntryId::from_proto(2)]
4577 );
4578 });
4579 window.simulate_prompt_answer(0, cx);
4580
4581 cx.foreground().run_until_parked();
4582 close.await.unwrap();
4583 left_pane.read_with(cx, |pane, _| {
4584 assert_eq!(pane.items_len(), 0);
4585 });
4586 }
4587
4588 #[gpui::test]
4589 async fn test_autosave(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
4590 init_test(cx);
4591
4592 let fs = FakeFs::new(cx.background());
4593
4594 let project = Project::test(fs, [], cx).await;
4595 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
4596 let workspace = window.root(cx);
4597 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4598
4599 let item = window.add_view(cx, |cx| {
4600 TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4601 });
4602 let item_id = item.id();
4603 workspace.update(cx, |workspace, cx| {
4604 workspace.add_item(Box::new(item.clone()), cx);
4605 });
4606
4607 // Autosave on window change.
4608 item.update(cx, |item, cx| {
4609 cx.update_global(|settings: &mut SettingsStore, cx| {
4610 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4611 settings.autosave = Some(AutosaveSetting::OnWindowChange);
4612 })
4613 });
4614 item.is_dirty = true;
4615 });
4616
4617 // Deactivating the window saves the file.
4618 window.simulate_deactivation(cx);
4619 deterministic.run_until_parked();
4620 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
4621
4622 // Autosave on focus change.
4623 item.update(cx, |item, cx| {
4624 cx.focus_self();
4625 cx.update_global(|settings: &mut SettingsStore, cx| {
4626 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4627 settings.autosave = Some(AutosaveSetting::OnFocusChange);
4628 })
4629 });
4630 item.is_dirty = true;
4631 });
4632
4633 // Blurring the item saves the file.
4634 item.update(cx, |_, cx| cx.blur());
4635 deterministic.run_until_parked();
4636 item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
4637
4638 // Deactivating the window still saves the file.
4639 window.simulate_activation(cx);
4640 item.update(cx, |item, cx| {
4641 cx.focus_self();
4642 item.is_dirty = true;
4643 });
4644 window.simulate_deactivation(cx);
4645
4646 deterministic.run_until_parked();
4647 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
4648
4649 // Autosave after delay.
4650 item.update(cx, |item, cx| {
4651 cx.update_global(|settings: &mut SettingsStore, cx| {
4652 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4653 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
4654 })
4655 });
4656 item.is_dirty = true;
4657 cx.emit(TestItemEvent::Edit);
4658 });
4659
4660 // Delay hasn't fully expired, so the file is still dirty and unsaved.
4661 deterministic.advance_clock(Duration::from_millis(250));
4662 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
4663
4664 // After delay expires, the file is saved.
4665 deterministic.advance_clock(Duration::from_millis(250));
4666 item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
4667
4668 // Autosave on focus change, ensuring closing the tab counts as such.
4669 item.update(cx, |item, cx| {
4670 cx.update_global(|settings: &mut SettingsStore, cx| {
4671 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4672 settings.autosave = Some(AutosaveSetting::OnFocusChange);
4673 })
4674 });
4675 item.is_dirty = true;
4676 });
4677
4678 pane.update(cx, |pane, cx| {
4679 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
4680 })
4681 .await
4682 .unwrap();
4683 assert!(!window.has_pending_prompt(cx));
4684 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
4685
4686 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
4687 workspace.update(cx, |workspace, cx| {
4688 workspace.add_item(Box::new(item.clone()), cx);
4689 });
4690 item.update(cx, |item, cx| {
4691 item.project_items[0].update(cx, |item, _| {
4692 item.entry_id = None;
4693 });
4694 item.is_dirty = true;
4695 cx.blur();
4696 });
4697 deterministic.run_until_parked();
4698 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
4699
4700 // Ensure autosave is prevented for deleted files also when closing the buffer.
4701 let _close_items = pane.update(cx, |pane, cx| {
4702 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
4703 });
4704 deterministic.run_until_parked();
4705 assert!(window.has_pending_prompt(cx));
4706 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
4707 }
4708
4709 #[gpui::test]
4710 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
4711 init_test(cx);
4712
4713 let fs = FakeFs::new(cx.background());
4714
4715 let project = Project::test(fs, [], cx).await;
4716 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
4717 let workspace = window.root(cx);
4718
4719 let item = window.add_view(cx, |cx| {
4720 TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4721 });
4722 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4723 let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone());
4724 let toolbar_notify_count = Rc::new(RefCell::new(0));
4725
4726 workspace.update(cx, |workspace, cx| {
4727 workspace.add_item(Box::new(item.clone()), cx);
4728 let toolbar_notification_count = toolbar_notify_count.clone();
4729 cx.observe(&toolbar, move |_, _, _| {
4730 *toolbar_notification_count.borrow_mut() += 1
4731 })
4732 .detach();
4733 });
4734
4735 pane.read_with(cx, |pane, _| {
4736 assert!(!pane.can_navigate_backward());
4737 assert!(!pane.can_navigate_forward());
4738 });
4739
4740 item.update(cx, |item, cx| {
4741 item.set_state("one".to_string(), cx);
4742 });
4743
4744 // Toolbar must be notified to re-render the navigation buttons
4745 assert_eq!(*toolbar_notify_count.borrow(), 1);
4746
4747 pane.read_with(cx, |pane, _| {
4748 assert!(pane.can_navigate_backward());
4749 assert!(!pane.can_navigate_forward());
4750 });
4751
4752 workspace
4753 .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
4754 .await
4755 .unwrap();
4756
4757 assert_eq!(*toolbar_notify_count.borrow(), 3);
4758 pane.read_with(cx, |pane, _| {
4759 assert!(!pane.can_navigate_backward());
4760 assert!(pane.can_navigate_forward());
4761 });
4762 }
4763
4764 #[gpui::test]
4765 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
4766 init_test(cx);
4767 let fs = FakeFs::new(cx.background());
4768
4769 let project = Project::test(fs, [], cx).await;
4770 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
4771 let workspace = window.root(cx);
4772
4773 let panel = workspace.update(cx, |workspace, cx| {
4774 let panel = cx.add_view(|_| TestPanel::new(DockPosition::Right));
4775 workspace.add_panel(panel.clone(), cx);
4776
4777 workspace
4778 .right_dock()
4779 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
4780
4781 panel
4782 });
4783
4784 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4785 pane.update(cx, |pane, cx| {
4786 let item = cx.add_view(|_| TestItem::new());
4787 pane.add_item(Box::new(item), true, true, None, cx);
4788 });
4789
4790 // Transfer focus from center to panel
4791 workspace.update(cx, |workspace, cx| {
4792 workspace.toggle_panel_focus::<TestPanel>(cx);
4793 });
4794
4795 workspace.read_with(cx, |workspace, cx| {
4796 assert!(workspace.right_dock().read(cx).is_open());
4797 assert!(!panel.is_zoomed(cx));
4798 assert!(panel.has_focus(cx));
4799 });
4800
4801 // Transfer focus from panel to center
4802 workspace.update(cx, |workspace, cx| {
4803 workspace.toggle_panel_focus::<TestPanel>(cx);
4804 });
4805
4806 workspace.read_with(cx, |workspace, cx| {
4807 assert!(workspace.right_dock().read(cx).is_open());
4808 assert!(!panel.is_zoomed(cx));
4809 assert!(!panel.has_focus(cx));
4810 });
4811
4812 // Close the dock
4813 workspace.update(cx, |workspace, cx| {
4814 workspace.toggle_dock(DockPosition::Right, cx);
4815 });
4816
4817 workspace.read_with(cx, |workspace, cx| {
4818 assert!(!workspace.right_dock().read(cx).is_open());
4819 assert!(!panel.is_zoomed(cx));
4820 assert!(!panel.has_focus(cx));
4821 });
4822
4823 // Open the dock
4824 workspace.update(cx, |workspace, cx| {
4825 workspace.toggle_dock(DockPosition::Right, cx);
4826 });
4827
4828 workspace.read_with(cx, |workspace, cx| {
4829 assert!(workspace.right_dock().read(cx).is_open());
4830 assert!(!panel.is_zoomed(cx));
4831 assert!(panel.has_focus(cx));
4832 });
4833
4834 // Focus and zoom panel
4835 panel.update(cx, |panel, cx| {
4836 cx.focus_self();
4837 panel.set_zoomed(true, cx)
4838 });
4839
4840 workspace.read_with(cx, |workspace, cx| {
4841 assert!(workspace.right_dock().read(cx).is_open());
4842 assert!(panel.is_zoomed(cx));
4843 assert!(panel.has_focus(cx));
4844 });
4845
4846 // Transfer focus to the center closes the dock
4847 workspace.update(cx, |workspace, cx| {
4848 workspace.toggle_panel_focus::<TestPanel>(cx);
4849 });
4850
4851 workspace.read_with(cx, |workspace, cx| {
4852 assert!(!workspace.right_dock().read(cx).is_open());
4853 assert!(panel.is_zoomed(cx));
4854 assert!(!panel.has_focus(cx));
4855 });
4856
4857 // Transferring focus back to the panel keeps it zoomed
4858 workspace.update(cx, |workspace, cx| {
4859 workspace.toggle_panel_focus::<TestPanel>(cx);
4860 });
4861
4862 workspace.read_with(cx, |workspace, cx| {
4863 assert!(workspace.right_dock().read(cx).is_open());
4864 assert!(panel.is_zoomed(cx));
4865 assert!(panel.has_focus(cx));
4866 });
4867
4868 // Close the dock while it is zoomed
4869 workspace.update(cx, |workspace, cx| {
4870 workspace.toggle_dock(DockPosition::Right, cx)
4871 });
4872
4873 workspace.read_with(cx, |workspace, cx| {
4874 assert!(!workspace.right_dock().read(cx).is_open());
4875 assert!(panel.is_zoomed(cx));
4876 assert!(workspace.zoomed.is_none());
4877 assert!(!panel.has_focus(cx));
4878 });
4879
4880 // Opening the dock, when it's zoomed, retains focus
4881 workspace.update(cx, |workspace, cx| {
4882 workspace.toggle_dock(DockPosition::Right, cx)
4883 });
4884
4885 workspace.read_with(cx, |workspace, cx| {
4886 assert!(workspace.right_dock().read(cx).is_open());
4887 assert!(panel.is_zoomed(cx));
4888 assert!(workspace.zoomed.is_some());
4889 assert!(panel.has_focus(cx));
4890 });
4891
4892 // Unzoom and close the panel, zoom the active pane.
4893 panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
4894 workspace.update(cx, |workspace, cx| {
4895 workspace.toggle_dock(DockPosition::Right, cx)
4896 });
4897 pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
4898
4899 // Opening a dock unzooms the pane.
4900 workspace.update(cx, |workspace, cx| {
4901 workspace.toggle_dock(DockPosition::Right, cx)
4902 });
4903 workspace.read_with(cx, |workspace, cx| {
4904 let pane = pane.read(cx);
4905 assert!(!pane.is_zoomed());
4906 assert!(!pane.has_focus());
4907 assert!(workspace.right_dock().read(cx).is_open());
4908 assert!(workspace.zoomed.is_none());
4909 });
4910 }
4911
4912 #[gpui::test]
4913 async fn test_panels(cx: &mut gpui::TestAppContext) {
4914 init_test(cx);
4915 let fs = FakeFs::new(cx.background());
4916
4917 let project = Project::test(fs, [], cx).await;
4918 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
4919 let workspace = window.root(cx);
4920
4921 let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
4922 // Add panel_1 on the left, panel_2 on the right.
4923 let panel_1 = cx.add_view(|_| TestPanel::new(DockPosition::Left));
4924 workspace.add_panel(panel_1.clone(), cx);
4925 workspace
4926 .left_dock()
4927 .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
4928 let panel_2 = cx.add_view(|_| TestPanel::new(DockPosition::Right));
4929 workspace.add_panel(panel_2.clone(), cx);
4930 workspace
4931 .right_dock()
4932 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
4933
4934 let left_dock = workspace.left_dock();
4935 assert_eq!(
4936 left_dock.read(cx).visible_panel().unwrap().id(),
4937 panel_1.id()
4938 );
4939 assert_eq!(
4940 left_dock.read(cx).active_panel_size(cx).unwrap(),
4941 panel_1.size(cx)
4942 );
4943
4944 left_dock.update(cx, |left_dock, cx| {
4945 left_dock.resize_active_panel(Some(1337.), cx)
4946 });
4947 assert_eq!(
4948 workspace
4949 .right_dock()
4950 .read(cx)
4951 .visible_panel()
4952 .unwrap()
4953 .id(),
4954 panel_2.id()
4955 );
4956
4957 (panel_1, panel_2)
4958 });
4959
4960 // Move panel_1 to the right
4961 panel_1.update(cx, |panel_1, cx| {
4962 panel_1.set_position(DockPosition::Right, cx)
4963 });
4964
4965 workspace.update(cx, |workspace, cx| {
4966 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
4967 // Since it was the only panel on the left, the left dock should now be closed.
4968 assert!(!workspace.left_dock().read(cx).is_open());
4969 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
4970 let right_dock = workspace.right_dock();
4971 assert_eq!(
4972 right_dock.read(cx).visible_panel().unwrap().id(),
4973 panel_1.id()
4974 );
4975 assert_eq!(right_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
4976
4977 // Now we move panel_2Β to the left
4978 panel_2.set_position(DockPosition::Left, cx);
4979 });
4980
4981 workspace.update(cx, |workspace, cx| {
4982 // Since panel_2 was not visible on the right, we don't open the left dock.
4983 assert!(!workspace.left_dock().read(cx).is_open());
4984 // And the right dock is unaffected in it's displaying of panel_1
4985 assert!(workspace.right_dock().read(cx).is_open());
4986 assert_eq!(
4987 workspace
4988 .right_dock()
4989 .read(cx)
4990 .visible_panel()
4991 .unwrap()
4992 .id(),
4993 panel_1.id()
4994 );
4995 });
4996
4997 // Move panel_1 back to the left
4998 panel_1.update(cx, |panel_1, cx| {
4999 panel_1.set_position(DockPosition::Left, cx)
5000 });
5001
5002 workspace.update(cx, |workspace, cx| {
5003 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
5004 let left_dock = workspace.left_dock();
5005 assert!(left_dock.read(cx).is_open());
5006 assert_eq!(
5007 left_dock.read(cx).visible_panel().unwrap().id(),
5008 panel_1.id()
5009 );
5010 assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
5011 // And right the dock should be closed as it no longer has any panels.
5012 assert!(!workspace.right_dock().read(cx).is_open());
5013
5014 // Now we move panel_1 to the bottom
5015 panel_1.set_position(DockPosition::Bottom, cx);
5016 });
5017
5018 workspace.update(cx, |workspace, cx| {
5019 // Since panel_1 was visible on the left, we close the left dock.
5020 assert!(!workspace.left_dock().read(cx).is_open());
5021 // The bottom dock is sized based on the panel's default size,
5022 // since the panel orientation changed from vertical to horizontal.
5023 let bottom_dock = workspace.bottom_dock();
5024 assert_eq!(
5025 bottom_dock.read(cx).active_panel_size(cx).unwrap(),
5026 panel_1.size(cx),
5027 );
5028 // Close bottom dock and move panel_1 back to the left.
5029 bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
5030 panel_1.set_position(DockPosition::Left, cx);
5031 });
5032
5033 // Emit activated event on panel 1
5034 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Activated));
5035
5036 // Now the left dock is open and panel_1 is active and focused.
5037 workspace.read_with(cx, |workspace, cx| {
5038 let left_dock = workspace.left_dock();
5039 assert!(left_dock.read(cx).is_open());
5040 assert_eq!(
5041 left_dock.read(cx).visible_panel().unwrap().id(),
5042 panel_1.id()
5043 );
5044 assert!(panel_1.is_focused(cx));
5045 });
5046
5047 // Emit closed event on panel 2, which is not active
5048 panel_2.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed));
5049
5050 // Wo don't close the left dock, because panel_2 wasn't the active panel
5051 workspace.read_with(cx, |workspace, cx| {
5052 let left_dock = workspace.left_dock();
5053 assert!(left_dock.read(cx).is_open());
5054 assert_eq!(
5055 left_dock.read(cx).visible_panel().unwrap().id(),
5056 panel_1.id()
5057 );
5058 });
5059
5060 // Emitting a ZoomIn event shows the panel as zoomed.
5061 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomIn));
5062 workspace.read_with(cx, |workspace, _| {
5063 assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
5064 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
5065 });
5066
5067 // Move panel to another dock while it is zoomed
5068 panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
5069 workspace.read_with(cx, |workspace, _| {
5070 assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
5071 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5072 });
5073
5074 // If focus is transferred to another view that's not a panel or another pane, we still show
5075 // the panel as zoomed.
5076 let focus_receiver = window.add_view(cx, |_| EmptyView);
5077 focus_receiver.update(cx, |_, cx| cx.focus_self());
5078 workspace.read_with(cx, |workspace, _| {
5079 assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
5080 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5081 });
5082
5083 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
5084 workspace.update(cx, |_, cx| cx.focus_self());
5085 workspace.read_with(cx, |workspace, _| {
5086 assert_eq!(workspace.zoomed, None);
5087 assert_eq!(workspace.zoomed_position, None);
5088 });
5089
5090 // If focus is transferred again to another view that's not a panel or a pane, we won't
5091 // show the panel as zoomed because it wasn't zoomed before.
5092 focus_receiver.update(cx, |_, cx| cx.focus_self());
5093 workspace.read_with(cx, |workspace, _| {
5094 assert_eq!(workspace.zoomed, None);
5095 assert_eq!(workspace.zoomed_position, None);
5096 });
5097
5098 // When focus is transferred back to the panel, it is zoomed again.
5099 panel_1.update(cx, |_, cx| cx.focus_self());
5100 workspace.read_with(cx, |workspace, _| {
5101 assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
5102 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5103 });
5104
5105 // Emitting a ZoomOut event unzooms the panel.
5106 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomOut));
5107 workspace.read_with(cx, |workspace, _| {
5108 assert_eq!(workspace.zoomed, None);
5109 assert_eq!(workspace.zoomed_position, None);
5110 });
5111
5112 // Emit closed event on panel 1, which is active
5113 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed));
5114
5115 // Now the left dock is closed, because panel_1 was the active panel
5116 workspace.read_with(cx, |workspace, cx| {
5117 let right_dock = workspace.right_dock();
5118 assert!(!right_dock.read(cx).is_open());
5119 });
5120 }
5121
5122 pub fn init_test(cx: &mut TestAppContext) {
5123 cx.foreground().forbid_parking();
5124 cx.update(|cx| {
5125 cx.set_global(SettingsStore::test(cx));
5126 theme::init((), cx);
5127 language::init(cx);
5128 crate::init_settings(cx);
5129 Project::init_settings(cx);
5130 });
5131 }
5132}