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