1pub mod dock;
2pub mod item;
3pub mod notifications;
4pub mod pane;
5pub mod pane_group;
6mod persistence;
7pub mod searchable;
8pub mod shared_screen;
9mod status_bar;
10mod toolbar;
11mod workspace_settings;
12
13use anyhow::{anyhow, Context, Result};
14use call::ActiveCall;
15use channel::ChannelStore;
16use client::{
17 proto::{self, PeerId},
18 Client, TypedEnvelope, UserStore,
19};
20use collections::{hash_map, HashMap, HashSet};
21use drag_and_drop::DragAndDrop;
22use futures::{
23 channel::{mpsc, oneshot},
24 future::try_join_all,
25 FutureExt, StreamExt,
26};
27use gpui::{
28 actions,
29 elements::*,
30 geometry::{
31 rect::RectF,
32 vector::{vec2f, Vector2F},
33 },
34 impl_actions,
35 platform::{
36 CursorStyle, ModifiersChangedEvent, MouseButton, PathPromptOptions, Platform, PromptLevel,
37 WindowBounds, WindowOptions,
38 },
39 AnyModelHandle, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity,
40 ModelContext, ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle,
41 WeakViewHandle, WindowContext, WindowHandle,
42};
43use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem};
44use itertools::Itertools;
45use language::{LanguageRegistry, Rope};
46use std::{
47 any::TypeId,
48 borrow::Cow,
49 cmp, env,
50 future::Future,
51 path::{Path, PathBuf},
52 rc::Rc,
53 str,
54 sync::{atomic::AtomicUsize, Arc},
55 time::Duration,
56};
57
58use crate::{
59 notifications::{simple_message_notification::MessageNotification, NotificationTracker},
60 persistence::model::{
61 DockData, DockStructure, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
62 },
63};
64use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle};
65use lazy_static::lazy_static;
66use notifications::{NotificationHandle, NotifyResultExt};
67pub use pane::*;
68pub use pane_group::*;
69use persistence::{model::SerializedItem, DB};
70pub use persistence::{
71 model::{ItemId, WorkspaceLocation},
72 WorkspaceDb, DB as WORKSPACE_DB,
73};
74use postage::prelude::Stream;
75use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
76use serde::Deserialize;
77use shared_screen::SharedScreen;
78use status_bar::StatusBar;
79pub use status_bar::StatusItemView;
80use theme::{Theme, ThemeSettings};
81pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
82use util::{async_iife, ResultExt};
83pub use workspace_settings::{AutosaveSetting, GitGutterSetting, WorkspaceSettings};
84
85lazy_static! {
86 static ref ZED_WINDOW_SIZE: Option<Vector2F> = env::var("ZED_WINDOW_SIZE")
87 .ok()
88 .as_deref()
89 .and_then(parse_pixel_position_env_var);
90 static ref ZED_WINDOW_POSITION: Option<Vector2F> = env::var("ZED_WINDOW_POSITION")
91 .ok()
92 .as_deref()
93 .and_then(parse_pixel_position_env_var);
94}
95
96pub trait Modal: View {
97 fn has_focus(&self) -> bool;
98 fn dismiss_on_event(event: &Self::Event) -> bool;
99}
100
101trait ModalHandle {
102 fn as_any(&self) -> &AnyViewHandle;
103 fn has_focus(&self, cx: &WindowContext) -> bool;
104}
105
106impl<T: Modal> ModalHandle for ViewHandle<T> {
107 fn as_any(&self) -> &AnyViewHandle {
108 self
109 }
110
111 fn has_focus(&self, cx: &WindowContext) -> bool {
112 self.read(cx).has_focus()
113 }
114}
115
116#[derive(Clone, PartialEq)]
117pub struct RemoveWorktreeFromProject(pub WorktreeId);
118
119actions!(
120 workspace,
121 [
122 Open,
123 NewFile,
124 NewWindow,
125 CloseWindow,
126 CloseInactiveTabsAndPanes,
127 AddFolderToProject,
128 Unfollow,
129 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 fn modifiers_changed(&mut self, e: &ModifiersChangedEvent, cx: &mut ViewContext<Self>) -> bool {
3812 DragAndDrop::<Workspace>::update_modifiers(e.modifiers, cx)
3813 }
3814}
3815
3816impl ViewId {
3817 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
3818 Ok(Self {
3819 creator: message
3820 .creator
3821 .ok_or_else(|| anyhow!("creator is missing"))?,
3822 id: message.id,
3823 })
3824 }
3825
3826 pub(crate) fn to_proto(&self) -> proto::ViewId {
3827 proto::ViewId {
3828 creator: Some(self.creator),
3829 id: self.id,
3830 }
3831 }
3832}
3833
3834pub trait WorkspaceHandle {
3835 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
3836}
3837
3838impl WorkspaceHandle for ViewHandle<Workspace> {
3839 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
3840 self.read(cx)
3841 .worktrees(cx)
3842 .flat_map(|worktree| {
3843 let worktree_id = worktree.read(cx).id();
3844 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
3845 worktree_id,
3846 path: f.path.clone(),
3847 })
3848 })
3849 .collect::<Vec<_>>()
3850 }
3851}
3852
3853impl std::fmt::Debug for OpenPaths {
3854 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3855 f.debug_struct("OpenPaths")
3856 .field("paths", &self.paths)
3857 .finish()
3858 }
3859}
3860
3861pub struct WorkspaceCreated(pub WeakViewHandle<Workspace>);
3862
3863pub fn activate_workspace_for_project(
3864 cx: &mut AsyncAppContext,
3865 predicate: impl Fn(&mut Project, &mut ModelContext<Project>) -> bool,
3866) -> Option<WeakViewHandle<Workspace>> {
3867 for window in cx.windows() {
3868 let handle = window
3869 .update(cx, |cx| {
3870 if let Some(workspace_handle) = cx.root_view().clone().downcast::<Workspace>() {
3871 let project = workspace_handle.read(cx).project.clone();
3872 if project.update(cx, &predicate) {
3873 cx.activate_window();
3874 return Some(workspace_handle.clone());
3875 }
3876 }
3877 None
3878 })
3879 .flatten();
3880
3881 if let Some(handle) = handle {
3882 return Some(handle.downgrade());
3883 }
3884 }
3885 None
3886}
3887
3888pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
3889 DB.last_workspace().await.log_err().flatten()
3890}
3891
3892#[allow(clippy::type_complexity)]
3893pub fn open_paths(
3894 abs_paths: &[PathBuf],
3895 app_state: &Arc<AppState>,
3896 requesting_window: Option<WindowHandle<Workspace>>,
3897 cx: &mut AppContext,
3898) -> Task<
3899 Result<(
3900 WeakViewHandle<Workspace>,
3901 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
3902 )>,
3903> {
3904 let app_state = app_state.clone();
3905 let abs_paths = abs_paths.to_vec();
3906 cx.spawn(|mut cx| async move {
3907 // Open paths in existing workspace if possible
3908 let existing = activate_workspace_for_project(&mut cx, |project, cx| {
3909 project.contains_paths(&abs_paths, cx)
3910 });
3911
3912 if let Some(existing) = existing {
3913 Ok((
3914 existing.clone(),
3915 existing
3916 .update(&mut cx, |workspace, cx| {
3917 workspace.open_paths(abs_paths, true, cx)
3918 })?
3919 .await,
3920 ))
3921 } else {
3922 Ok(cx
3923 .update(|cx| {
3924 Workspace::new_local(abs_paths, app_state.clone(), requesting_window, cx)
3925 })
3926 .await)
3927 }
3928 })
3929}
3930
3931pub fn open_new(
3932 app_state: &Arc<AppState>,
3933 cx: &mut AppContext,
3934 init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static,
3935) -> Task<()> {
3936 let task = Workspace::new_local(Vec::new(), app_state.clone(), None, cx);
3937 cx.spawn(|mut cx| async move {
3938 let (workspace, opened_paths) = task.await;
3939
3940 workspace
3941 .update(&mut cx, |workspace, cx| {
3942 if opened_paths.is_empty() {
3943 init(workspace, cx)
3944 }
3945 })
3946 .log_err();
3947 })
3948}
3949
3950pub fn create_and_open_local_file(
3951 path: &'static Path,
3952 cx: &mut ViewContext<Workspace>,
3953 default_content: impl 'static + Send + FnOnce() -> Rope,
3954) -> Task<Result<Box<dyn ItemHandle>>> {
3955 cx.spawn(|workspace, mut cx| async move {
3956 let fs = workspace.read_with(&cx, |workspace, _| workspace.app_state().fs.clone())?;
3957 if !fs.is_file(path).await {
3958 fs.create_file(path, Default::default()).await?;
3959 fs.save(path, &default_content(), Default::default())
3960 .await?;
3961 }
3962
3963 let mut items = workspace
3964 .update(&mut cx, |workspace, cx| {
3965 workspace.with_local_workspace(cx, |workspace, cx| {
3966 workspace.open_paths(vec![path.to_path_buf()], false, cx)
3967 })
3968 })?
3969 .await?
3970 .await;
3971
3972 let item = items.pop().flatten();
3973 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
3974 })
3975}
3976
3977pub fn join_remote_project(
3978 project_id: u64,
3979 follow_user_id: u64,
3980 app_state: Arc<AppState>,
3981 cx: &mut AppContext,
3982) -> Task<Result<()>> {
3983 cx.spawn(|mut cx| async move {
3984 let existing_workspace = cx
3985 .windows()
3986 .into_iter()
3987 .find_map(|window| {
3988 window.downcast::<Workspace>().and_then(|window| {
3989 window.read_root_with(&cx, |workspace, cx| {
3990 if workspace.project().read(cx).remote_id() == Some(project_id) {
3991 Some(cx.handle().downgrade())
3992 } else {
3993 None
3994 }
3995 })
3996 })
3997 })
3998 .flatten();
3999
4000 let workspace = if let Some(existing_workspace) = existing_workspace {
4001 existing_workspace
4002 } else {
4003 let active_call = cx.read(ActiveCall::global);
4004 let room = active_call
4005 .read_with(&cx, |call, _| call.room().cloned())
4006 .ok_or_else(|| anyhow!("not in a call"))?;
4007 let project = room
4008 .update(&mut cx, |room, cx| {
4009 room.join_project(
4010 project_id,
4011 app_state.languages.clone(),
4012 app_state.fs.clone(),
4013 cx,
4014 )
4015 })
4016 .await?;
4017
4018 let window_bounds_override = window_bounds_env_override(&cx);
4019 let window = cx.add_window(
4020 (app_state.build_window_options)(
4021 window_bounds_override,
4022 None,
4023 cx.platform().as_ref(),
4024 ),
4025 |cx| Workspace::new(0, project, app_state.clone(), cx),
4026 );
4027 let workspace = window.root(&cx).unwrap();
4028 (app_state.initialize_workspace)(
4029 workspace.downgrade(),
4030 false,
4031 app_state.clone(),
4032 cx.clone(),
4033 )
4034 .await
4035 .log_err();
4036
4037 workspace.downgrade()
4038 };
4039
4040 workspace.window().activate(&mut cx);
4041 cx.platform().activate(true);
4042
4043 workspace.update(&mut cx, |workspace, cx| {
4044 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
4045 let follow_peer_id = room
4046 .read(cx)
4047 .remote_participants()
4048 .iter()
4049 .find(|(_, participant)| participant.user.id == follow_user_id)
4050 .map(|(_, p)| p.peer_id)
4051 .or_else(|| {
4052 // If we couldn't follow the given user, follow the host instead.
4053 let collaborator = workspace
4054 .project()
4055 .read(cx)
4056 .collaborators()
4057 .values()
4058 .find(|collaborator| collaborator.replica_id == 0)?;
4059 Some(collaborator.peer_id)
4060 });
4061
4062 if let Some(follow_peer_id) = follow_peer_id {
4063 if !workspace.is_being_followed(follow_peer_id) {
4064 workspace
4065 .toggle_follow(follow_peer_id, cx)
4066 .map(|follow| follow.detach_and_log_err(cx));
4067 }
4068 }
4069 }
4070 })?;
4071
4072 anyhow::Ok(())
4073 })
4074}
4075
4076pub fn restart(_: &Restart, cx: &mut AppContext) {
4077 let should_confirm = settings::get::<WorkspaceSettings>(cx).confirm_quit;
4078 cx.spawn(|mut cx| async move {
4079 let mut workspace_windows = cx
4080 .windows()
4081 .into_iter()
4082 .filter_map(|window| window.downcast::<Workspace>())
4083 .collect::<Vec<_>>();
4084
4085 // If multiple windows have unsaved changes, and need a save prompt,
4086 // prompt in the active window before switching to a different window.
4087 workspace_windows.sort_by_key(|window| window.is_active(&cx) == Some(false));
4088
4089 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
4090 let answer = window.prompt(
4091 PromptLevel::Info,
4092 "Are you sure you want to restart?",
4093 &["Restart", "Cancel"],
4094 &mut cx,
4095 );
4096
4097 if let Some(mut answer) = answer {
4098 let answer = answer.next().await;
4099 if answer != Some(0) {
4100 return Ok(());
4101 }
4102 }
4103 }
4104
4105 // If the user cancels any save prompt, then keep the app open.
4106 for window in workspace_windows {
4107 if let Some(should_close) = window.update_root(&mut cx, |workspace, cx| {
4108 workspace.prepare_to_close(true, cx)
4109 }) {
4110 if !should_close.await? {
4111 return Ok(());
4112 }
4113 }
4114 }
4115 cx.platform().restart();
4116 anyhow::Ok(())
4117 })
4118 .detach_and_log_err(cx);
4119}
4120
4121fn parse_pixel_position_env_var(value: &str) -> Option<Vector2F> {
4122 let mut parts = value.split(',');
4123 let width: usize = parts.next()?.parse().ok()?;
4124 let height: usize = parts.next()?.parse().ok()?;
4125 Some(vec2f(width as f32, height as f32))
4126}
4127
4128#[cfg(test)]
4129mod tests {
4130 use super::*;
4131 use crate::{
4132 dock::test::{TestPanel, TestPanelEvent},
4133 item::test::{TestItem, TestItemEvent, TestProjectItem},
4134 };
4135 use fs::FakeFs;
4136 use gpui::{executor::Deterministic, test::EmptyView, TestAppContext};
4137 use project::{Project, ProjectEntryId};
4138 use serde_json::json;
4139 use settings::SettingsStore;
4140 use std::{cell::RefCell, rc::Rc};
4141
4142 #[gpui::test]
4143 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
4144 init_test(cx);
4145
4146 let fs = FakeFs::new(cx.background());
4147 let project = Project::test(fs, [], cx).await;
4148 let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
4149 let workspace = window.root(cx);
4150
4151 // Adding an item with no ambiguity renders the tab without detail.
4152 let item1 = window.add_view(cx, |_| {
4153 let mut item = TestItem::new();
4154 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
4155 item
4156 });
4157 workspace.update(cx, |workspace, cx| {
4158 workspace.add_item(Box::new(item1.clone()), cx);
4159 });
4160 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), None));
4161
4162 // Adding an item that creates ambiguity increases the level of detail on
4163 // both tabs.
4164 let item2 = window.add_view(cx, |_| {
4165 let mut item = TestItem::new();
4166 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
4167 item
4168 });
4169 workspace.update(cx, |workspace, cx| {
4170 workspace.add_item(Box::new(item2.clone()), cx);
4171 });
4172 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4173 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4174
4175 // Adding an item that creates ambiguity increases the level of detail only
4176 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
4177 // we stop at the highest detail available.
4178 let item3 = window.add_view(cx, |_| {
4179 let mut item = TestItem::new();
4180 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
4181 item
4182 });
4183 workspace.update(cx, |workspace, cx| {
4184 workspace.add_item(Box::new(item3.clone()), cx);
4185 });
4186 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4187 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
4188 item3.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
4189 }
4190
4191 #[gpui::test]
4192 async fn test_tracking_active_path(cx: &mut TestAppContext) {
4193 init_test(cx);
4194
4195 let fs = FakeFs::new(cx.background());
4196 fs.insert_tree(
4197 "/root1",
4198 json!({
4199 "one.txt": "",
4200 "two.txt": "",
4201 }),
4202 )
4203 .await;
4204 fs.insert_tree(
4205 "/root2",
4206 json!({
4207 "three.txt": "",
4208 }),
4209 )
4210 .await;
4211
4212 let project = Project::test(fs, ["root1".as_ref()], cx).await;
4213 let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
4214 let workspace = window.root(cx);
4215 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4216 let worktree_id = project.read_with(cx, |project, cx| {
4217 project.worktrees(cx).next().unwrap().read(cx).id()
4218 });
4219
4220 let item1 = window.add_view(cx, |cx| {
4221 TestItem::new().with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
4222 });
4223 let item2 = window.add_view(cx, |cx| {
4224 TestItem::new().with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
4225 });
4226
4227 // Add an item to an empty pane
4228 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx));
4229 project.read_with(cx, |project, cx| {
4230 assert_eq!(
4231 project.active_entry(),
4232 project
4233 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
4234 .map(|e| e.id)
4235 );
4236 });
4237 assert_eq!(window.current_title(cx).as_deref(), Some("one.txt β root1"));
4238
4239 // Add a second item to a non-empty pane
4240 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
4241 assert_eq!(window.current_title(cx).as_deref(), Some("two.txt β root1"));
4242 project.read_with(cx, |project, cx| {
4243 assert_eq!(
4244 project.active_entry(),
4245 project
4246 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
4247 .map(|e| e.id)
4248 );
4249 });
4250
4251 // Close the active item
4252 pane.update(cx, |pane, cx| {
4253 pane.close_active_item(&Default::default(), cx).unwrap()
4254 })
4255 .await
4256 .unwrap();
4257 assert_eq!(window.current_title(cx).as_deref(), Some("one.txt β root1"));
4258 project.read_with(cx, |project, cx| {
4259 assert_eq!(
4260 project.active_entry(),
4261 project
4262 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
4263 .map(|e| e.id)
4264 );
4265 });
4266
4267 // Add a project folder
4268 project
4269 .update(cx, |project, cx| {
4270 project.find_or_create_local_worktree("/root2", true, cx)
4271 })
4272 .await
4273 .unwrap();
4274 assert_eq!(
4275 window.current_title(cx).as_deref(),
4276 Some("one.txt β root1, root2")
4277 );
4278
4279 // Remove a project folder
4280 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
4281 assert_eq!(window.current_title(cx).as_deref(), Some("one.txt β root2"));
4282 }
4283
4284 #[gpui::test]
4285 async fn test_close_window(cx: &mut TestAppContext) {
4286 init_test(cx);
4287
4288 let fs = FakeFs::new(cx.background());
4289 fs.insert_tree("/root", json!({ "one": "" })).await;
4290
4291 let project = Project::test(fs, ["root".as_ref()], cx).await;
4292 let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
4293 let workspace = window.root(cx);
4294
4295 // When there are no dirty items, there's nothing to do.
4296 let item1 = window.add_view(cx, |_| TestItem::new());
4297 workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
4298 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
4299 assert!(task.await.unwrap());
4300
4301 // When there are dirty untitled items, prompt to save each one. If the user
4302 // cancels any prompt, then abort.
4303 let item2 = window.add_view(cx, |_| TestItem::new().with_dirty(true));
4304 let item3 = window.add_view(cx, |cx| {
4305 TestItem::new()
4306 .with_dirty(true)
4307 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4308 });
4309 workspace.update(cx, |w, cx| {
4310 w.add_item(Box::new(item2.clone()), cx);
4311 w.add_item(Box::new(item3.clone()), cx);
4312 });
4313 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
4314 cx.foreground().run_until_parked();
4315 window.simulate_prompt_answer(2, cx); // cancel
4316 cx.foreground().run_until_parked();
4317 assert!(!window.has_pending_prompt(cx));
4318 assert!(!task.await.unwrap());
4319 }
4320
4321 #[gpui::test]
4322 async fn test_close_pane_items(cx: &mut TestAppContext) {
4323 init_test(cx);
4324
4325 let fs = FakeFs::new(cx.background());
4326
4327 let project = Project::test(fs, None, cx).await;
4328 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
4329 let workspace = window.root(cx);
4330
4331 let item1 = window.add_view(cx, |cx| {
4332 TestItem::new()
4333 .with_dirty(true)
4334 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4335 });
4336 let item2 = window.add_view(cx, |cx| {
4337 TestItem::new()
4338 .with_dirty(true)
4339 .with_conflict(true)
4340 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
4341 });
4342 let item3 = window.add_view(cx, |cx| {
4343 TestItem::new()
4344 .with_dirty(true)
4345 .with_conflict(true)
4346 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
4347 });
4348 let item4 = window.add_view(cx, |cx| {
4349 TestItem::new()
4350 .with_dirty(true)
4351 .with_project_items(&[TestProjectItem::new_untitled(cx)])
4352 });
4353 let pane = workspace.update(cx, |workspace, cx| {
4354 workspace.add_item(Box::new(item1.clone()), cx);
4355 workspace.add_item(Box::new(item2.clone()), cx);
4356 workspace.add_item(Box::new(item3.clone()), cx);
4357 workspace.add_item(Box::new(item4.clone()), cx);
4358 workspace.active_pane().clone()
4359 });
4360
4361 let close_items = pane.update(cx, |pane, cx| {
4362 pane.activate_item(1, true, true, cx);
4363 assert_eq!(pane.active_item().unwrap().id(), item2.id());
4364 let item1_id = item1.id();
4365 let item3_id = item3.id();
4366 let item4_id = item4.id();
4367 pane.close_items(cx, SaveBehavior::PromptOnWrite, move |id| {
4368 [item1_id, item3_id, item4_id].contains(&id)
4369 })
4370 });
4371 cx.foreground().run_until_parked();
4372
4373 // There's a prompt to save item 1.
4374 pane.read_with(cx, |pane, _| {
4375 assert_eq!(pane.items_len(), 4);
4376 assert_eq!(pane.active_item().unwrap().id(), item1.id());
4377 });
4378 assert!(window.has_pending_prompt(cx));
4379
4380 // Confirm saving item 1.
4381 window.simulate_prompt_answer(0, cx);
4382 cx.foreground().run_until_parked();
4383
4384 // Item 1 is saved. There's a prompt to save item 3.
4385 pane.read_with(cx, |pane, cx| {
4386 assert_eq!(item1.read(cx).save_count, 1);
4387 assert_eq!(item1.read(cx).save_as_count, 0);
4388 assert_eq!(item1.read(cx).reload_count, 0);
4389 assert_eq!(pane.items_len(), 3);
4390 assert_eq!(pane.active_item().unwrap().id(), item3.id());
4391 });
4392 assert!(window.has_pending_prompt(cx));
4393
4394 // Cancel saving item 3.
4395 window.simulate_prompt_answer(1, cx);
4396 cx.foreground().run_until_parked();
4397
4398 // Item 3 is reloaded. There's a prompt to save item 4.
4399 pane.read_with(cx, |pane, cx| {
4400 assert_eq!(item3.read(cx).save_count, 0);
4401 assert_eq!(item3.read(cx).save_as_count, 0);
4402 assert_eq!(item3.read(cx).reload_count, 1);
4403 assert_eq!(pane.items_len(), 2);
4404 assert_eq!(pane.active_item().unwrap().id(), item4.id());
4405 });
4406 assert!(window.has_pending_prompt(cx));
4407
4408 // Confirm saving item 4.
4409 window.simulate_prompt_answer(0, cx);
4410 cx.foreground().run_until_parked();
4411
4412 // There's a prompt for a path for item 4.
4413 cx.simulate_new_path_selection(|_| Some(Default::default()));
4414 close_items.await.unwrap();
4415
4416 // The requested items are closed.
4417 pane.read_with(cx, |pane, cx| {
4418 assert_eq!(item4.read(cx).save_count, 0);
4419 assert_eq!(item4.read(cx).save_as_count, 1);
4420 assert_eq!(item4.read(cx).reload_count, 0);
4421 assert_eq!(pane.items_len(), 1);
4422 assert_eq!(pane.active_item().unwrap().id(), item2.id());
4423 });
4424 }
4425
4426 #[gpui::test]
4427 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
4428 init_test(cx);
4429
4430 let fs = FakeFs::new(cx.background());
4431
4432 let project = Project::test(fs, [], cx).await;
4433 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
4434 let workspace = window.root(cx);
4435
4436 // Create several workspace items with single project entries, and two
4437 // workspace items with multiple project entries.
4438 let single_entry_items = (0..=4)
4439 .map(|project_entry_id| {
4440 window.add_view(cx, |cx| {
4441 TestItem::new()
4442 .with_dirty(true)
4443 .with_project_items(&[TestProjectItem::new(
4444 project_entry_id,
4445 &format!("{project_entry_id}.txt"),
4446 cx,
4447 )])
4448 })
4449 })
4450 .collect::<Vec<_>>();
4451 let item_2_3 = window.add_view(cx, |cx| {
4452 TestItem::new()
4453 .with_dirty(true)
4454 .with_singleton(false)
4455 .with_project_items(&[
4456 single_entry_items[2].read(cx).project_items[0].clone(),
4457 single_entry_items[3].read(cx).project_items[0].clone(),
4458 ])
4459 });
4460 let item_3_4 = window.add_view(cx, |cx| {
4461 TestItem::new()
4462 .with_dirty(true)
4463 .with_singleton(false)
4464 .with_project_items(&[
4465 single_entry_items[3].read(cx).project_items[0].clone(),
4466 single_entry_items[4].read(cx).project_items[0].clone(),
4467 ])
4468 });
4469
4470 // Create two panes that contain the following project entries:
4471 // left pane:
4472 // multi-entry items: (2, 3)
4473 // single-entry items: 0, 1, 2, 3, 4
4474 // right pane:
4475 // single-entry items: 1
4476 // multi-entry items: (3, 4)
4477 let left_pane = workspace.update(cx, |workspace, cx| {
4478 let left_pane = workspace.active_pane().clone();
4479 workspace.add_item(Box::new(item_2_3.clone()), cx);
4480 for item in single_entry_items {
4481 workspace.add_item(Box::new(item), cx);
4482 }
4483 left_pane.update(cx, |pane, cx| {
4484 pane.activate_item(2, true, true, cx);
4485 });
4486
4487 workspace
4488 .split_and_clone(left_pane.clone(), SplitDirection::Right, cx)
4489 .unwrap();
4490
4491 left_pane
4492 });
4493
4494 //Need to cause an effect flush in order to respect new focus
4495 workspace.update(cx, |workspace, cx| {
4496 workspace.add_item(Box::new(item_3_4.clone()), cx);
4497 cx.focus(&left_pane);
4498 });
4499
4500 // When closing all of the items in the left pane, we should be prompted twice:
4501 // once for project entry 0, and once for project entry 2. After those two
4502 // prompts, the task should complete.
4503
4504 let close = left_pane.update(cx, |pane, cx| {
4505 pane.close_items(cx, SaveBehavior::PromptOnWrite, move |_| true)
4506 });
4507 cx.foreground().run_until_parked();
4508 left_pane.read_with(cx, |pane, cx| {
4509 assert_eq!(
4510 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
4511 &[ProjectEntryId::from_proto(0)]
4512 );
4513 });
4514 window.simulate_prompt_answer(0, cx);
4515
4516 cx.foreground().run_until_parked();
4517 left_pane.read_with(cx, |pane, cx| {
4518 assert_eq!(
4519 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
4520 &[ProjectEntryId::from_proto(2)]
4521 );
4522 });
4523 window.simulate_prompt_answer(0, cx);
4524
4525 cx.foreground().run_until_parked();
4526 close.await.unwrap();
4527 left_pane.read_with(cx, |pane, _| {
4528 assert_eq!(pane.items_len(), 0);
4529 });
4530 }
4531
4532 #[gpui::test]
4533 async fn test_autosave(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
4534 init_test(cx);
4535
4536 let fs = FakeFs::new(cx.background());
4537
4538 let project = Project::test(fs, [], cx).await;
4539 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
4540 let workspace = window.root(cx);
4541 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4542
4543 let item = window.add_view(cx, |cx| {
4544 TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4545 });
4546 let item_id = item.id();
4547 workspace.update(cx, |workspace, cx| {
4548 workspace.add_item(Box::new(item.clone()), cx);
4549 });
4550
4551 // Autosave on window change.
4552 item.update(cx, |item, cx| {
4553 cx.update_global(|settings: &mut SettingsStore, cx| {
4554 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4555 settings.autosave = Some(AutosaveSetting::OnWindowChange);
4556 })
4557 });
4558 item.is_dirty = true;
4559 });
4560
4561 // Deactivating the window saves the file.
4562 window.simulate_deactivation(cx);
4563 deterministic.run_until_parked();
4564 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
4565
4566 // Autosave on focus change.
4567 item.update(cx, |item, cx| {
4568 cx.focus_self();
4569 cx.update_global(|settings: &mut SettingsStore, cx| {
4570 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4571 settings.autosave = Some(AutosaveSetting::OnFocusChange);
4572 })
4573 });
4574 item.is_dirty = true;
4575 });
4576
4577 // Blurring the item saves the file.
4578 item.update(cx, |_, cx| cx.blur());
4579 deterministic.run_until_parked();
4580 item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
4581
4582 // Deactivating the window still saves the file.
4583 window.simulate_activation(cx);
4584 item.update(cx, |item, cx| {
4585 cx.focus_self();
4586 item.is_dirty = true;
4587 });
4588 window.simulate_deactivation(cx);
4589
4590 deterministic.run_until_parked();
4591 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
4592
4593 // Autosave after delay.
4594 item.update(cx, |item, cx| {
4595 cx.update_global(|settings: &mut SettingsStore, cx| {
4596 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4597 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
4598 })
4599 });
4600 item.is_dirty = true;
4601 cx.emit(TestItemEvent::Edit);
4602 });
4603
4604 // Delay hasn't fully expired, so the file is still dirty and unsaved.
4605 deterministic.advance_clock(Duration::from_millis(250));
4606 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
4607
4608 // After delay expires, the file is saved.
4609 deterministic.advance_clock(Duration::from_millis(250));
4610 item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
4611
4612 // Autosave on focus change, ensuring closing the tab counts as such.
4613 item.update(cx, |item, cx| {
4614 cx.update_global(|settings: &mut SettingsStore, cx| {
4615 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4616 settings.autosave = Some(AutosaveSetting::OnFocusChange);
4617 })
4618 });
4619 item.is_dirty = true;
4620 });
4621
4622 pane.update(cx, |pane, cx| {
4623 pane.close_items(cx, SaveBehavior::PromptOnWrite, move |id| id == item_id)
4624 })
4625 .await
4626 .unwrap();
4627 assert!(!window.has_pending_prompt(cx));
4628 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
4629
4630 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
4631 workspace.update(cx, |workspace, cx| {
4632 workspace.add_item(Box::new(item.clone()), cx);
4633 });
4634 item.update(cx, |item, cx| {
4635 item.project_items[0].update(cx, |item, _| {
4636 item.entry_id = None;
4637 });
4638 item.is_dirty = true;
4639 cx.blur();
4640 });
4641 deterministic.run_until_parked();
4642 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
4643
4644 // Ensure autosave is prevented for deleted files also when closing the buffer.
4645 let _close_items = pane.update(cx, |pane, cx| {
4646 pane.close_items(cx, SaveBehavior::PromptOnWrite, move |id| id == item_id)
4647 });
4648 deterministic.run_until_parked();
4649 assert!(window.has_pending_prompt(cx));
4650 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
4651 }
4652
4653 #[gpui::test]
4654 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
4655 init_test(cx);
4656
4657 let fs = FakeFs::new(cx.background());
4658
4659 let project = Project::test(fs, [], cx).await;
4660 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
4661 let workspace = window.root(cx);
4662
4663 let item = window.add_view(cx, |cx| {
4664 TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4665 });
4666 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4667 let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone());
4668 let toolbar_notify_count = Rc::new(RefCell::new(0));
4669
4670 workspace.update(cx, |workspace, cx| {
4671 workspace.add_item(Box::new(item.clone()), cx);
4672 let toolbar_notification_count = toolbar_notify_count.clone();
4673 cx.observe(&toolbar, move |_, _, _| {
4674 *toolbar_notification_count.borrow_mut() += 1
4675 })
4676 .detach();
4677 });
4678
4679 pane.read_with(cx, |pane, _| {
4680 assert!(!pane.can_navigate_backward());
4681 assert!(!pane.can_navigate_forward());
4682 });
4683
4684 item.update(cx, |item, cx| {
4685 item.set_state("one".to_string(), cx);
4686 });
4687
4688 // Toolbar must be notified to re-render the navigation buttons
4689 assert_eq!(*toolbar_notify_count.borrow(), 1);
4690
4691 pane.read_with(cx, |pane, _| {
4692 assert!(pane.can_navigate_backward());
4693 assert!(!pane.can_navigate_forward());
4694 });
4695
4696 workspace
4697 .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
4698 .await
4699 .unwrap();
4700
4701 assert_eq!(*toolbar_notify_count.borrow(), 3);
4702 pane.read_with(cx, |pane, _| {
4703 assert!(!pane.can_navigate_backward());
4704 assert!(pane.can_navigate_forward());
4705 });
4706 }
4707
4708 #[gpui::test]
4709 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
4710 init_test(cx);
4711 let fs = FakeFs::new(cx.background());
4712
4713 let project = Project::test(fs, [], cx).await;
4714 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
4715 let workspace = window.root(cx);
4716
4717 let panel = workspace.update(cx, |workspace, cx| {
4718 let panel = cx.add_view(|_| TestPanel::new(DockPosition::Right));
4719 workspace.add_panel(panel.clone(), cx);
4720
4721 workspace
4722 .right_dock()
4723 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
4724
4725 panel
4726 });
4727
4728 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4729 pane.update(cx, |pane, cx| {
4730 let item = cx.add_view(|_| TestItem::new());
4731 pane.add_item(Box::new(item), true, true, None, cx);
4732 });
4733
4734 // Transfer focus from center to panel
4735 workspace.update(cx, |workspace, cx| {
4736 workspace.toggle_panel_focus::<TestPanel>(cx);
4737 });
4738
4739 workspace.read_with(cx, |workspace, cx| {
4740 assert!(workspace.right_dock().read(cx).is_open());
4741 assert!(!panel.is_zoomed(cx));
4742 assert!(panel.has_focus(cx));
4743 });
4744
4745 // Transfer focus from panel to center
4746 workspace.update(cx, |workspace, cx| {
4747 workspace.toggle_panel_focus::<TestPanel>(cx);
4748 });
4749
4750 workspace.read_with(cx, |workspace, cx| {
4751 assert!(workspace.right_dock().read(cx).is_open());
4752 assert!(!panel.is_zoomed(cx));
4753 assert!(!panel.has_focus(cx));
4754 });
4755
4756 // Close the dock
4757 workspace.update(cx, |workspace, cx| {
4758 workspace.toggle_dock(DockPosition::Right, cx);
4759 });
4760
4761 workspace.read_with(cx, |workspace, cx| {
4762 assert!(!workspace.right_dock().read(cx).is_open());
4763 assert!(!panel.is_zoomed(cx));
4764 assert!(!panel.has_focus(cx));
4765 });
4766
4767 // Open the dock
4768 workspace.update(cx, |workspace, cx| {
4769 workspace.toggle_dock(DockPosition::Right, cx);
4770 });
4771
4772 workspace.read_with(cx, |workspace, cx| {
4773 assert!(workspace.right_dock().read(cx).is_open());
4774 assert!(!panel.is_zoomed(cx));
4775 assert!(panel.has_focus(cx));
4776 });
4777
4778 // Focus and zoom panel
4779 panel.update(cx, |panel, cx| {
4780 cx.focus_self();
4781 panel.set_zoomed(true, cx)
4782 });
4783
4784 workspace.read_with(cx, |workspace, cx| {
4785 assert!(workspace.right_dock().read(cx).is_open());
4786 assert!(panel.is_zoomed(cx));
4787 assert!(panel.has_focus(cx));
4788 });
4789
4790 // Transfer focus to the center closes the dock
4791 workspace.update(cx, |workspace, cx| {
4792 workspace.toggle_panel_focus::<TestPanel>(cx);
4793 });
4794
4795 workspace.read_with(cx, |workspace, cx| {
4796 assert!(!workspace.right_dock().read(cx).is_open());
4797 assert!(panel.is_zoomed(cx));
4798 assert!(!panel.has_focus(cx));
4799 });
4800
4801 // Transferring focus back to the panel keeps it zoomed
4802 workspace.update(cx, |workspace, cx| {
4803 workspace.toggle_panel_focus::<TestPanel>(cx);
4804 });
4805
4806 workspace.read_with(cx, |workspace, cx| {
4807 assert!(workspace.right_dock().read(cx).is_open());
4808 assert!(panel.is_zoomed(cx));
4809 assert!(panel.has_focus(cx));
4810 });
4811
4812 // Close the dock while it is zoomed
4813 workspace.update(cx, |workspace, cx| {
4814 workspace.toggle_dock(DockPosition::Right, cx)
4815 });
4816
4817 workspace.read_with(cx, |workspace, cx| {
4818 assert!(!workspace.right_dock().read(cx).is_open());
4819 assert!(panel.is_zoomed(cx));
4820 assert!(workspace.zoomed.is_none());
4821 assert!(!panel.has_focus(cx));
4822 });
4823
4824 // Opening the dock, when it's zoomed, retains focus
4825 workspace.update(cx, |workspace, cx| {
4826 workspace.toggle_dock(DockPosition::Right, cx)
4827 });
4828
4829 workspace.read_with(cx, |workspace, cx| {
4830 assert!(workspace.right_dock().read(cx).is_open());
4831 assert!(panel.is_zoomed(cx));
4832 assert!(workspace.zoomed.is_some());
4833 assert!(panel.has_focus(cx));
4834 });
4835
4836 // Unzoom and close the panel, zoom the active pane.
4837 panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
4838 workspace.update(cx, |workspace, cx| {
4839 workspace.toggle_dock(DockPosition::Right, cx)
4840 });
4841 pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
4842
4843 // Opening a dock unzooms the pane.
4844 workspace.update(cx, |workspace, cx| {
4845 workspace.toggle_dock(DockPosition::Right, cx)
4846 });
4847 workspace.read_with(cx, |workspace, cx| {
4848 let pane = pane.read(cx);
4849 assert!(!pane.is_zoomed());
4850 assert!(!pane.has_focus());
4851 assert!(workspace.right_dock().read(cx).is_open());
4852 assert!(workspace.zoomed.is_none());
4853 });
4854 }
4855
4856 #[gpui::test]
4857 async fn test_panels(cx: &mut gpui::TestAppContext) {
4858 init_test(cx);
4859 let fs = FakeFs::new(cx.background());
4860
4861 let project = Project::test(fs, [], cx).await;
4862 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
4863 let workspace = window.root(cx);
4864
4865 let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
4866 // Add panel_1 on the left, panel_2 on the right.
4867 let panel_1 = cx.add_view(|_| TestPanel::new(DockPosition::Left));
4868 workspace.add_panel(panel_1.clone(), cx);
4869 workspace
4870 .left_dock()
4871 .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
4872 let panel_2 = cx.add_view(|_| TestPanel::new(DockPosition::Right));
4873 workspace.add_panel(panel_2.clone(), cx);
4874 workspace
4875 .right_dock()
4876 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
4877
4878 let left_dock = workspace.left_dock();
4879 assert_eq!(
4880 left_dock.read(cx).visible_panel().unwrap().id(),
4881 panel_1.id()
4882 );
4883 assert_eq!(
4884 left_dock.read(cx).active_panel_size(cx).unwrap(),
4885 panel_1.size(cx)
4886 );
4887
4888 left_dock.update(cx, |left_dock, cx| {
4889 left_dock.resize_active_panel(Some(1337.), cx)
4890 });
4891 assert_eq!(
4892 workspace
4893 .right_dock()
4894 .read(cx)
4895 .visible_panel()
4896 .unwrap()
4897 .id(),
4898 panel_2.id()
4899 );
4900
4901 (panel_1, panel_2)
4902 });
4903
4904 // Move panel_1 to the right
4905 panel_1.update(cx, |panel_1, cx| {
4906 panel_1.set_position(DockPosition::Right, cx)
4907 });
4908
4909 workspace.update(cx, |workspace, cx| {
4910 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
4911 // Since it was the only panel on the left, the left dock should now be closed.
4912 assert!(!workspace.left_dock().read(cx).is_open());
4913 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
4914 let right_dock = workspace.right_dock();
4915 assert_eq!(
4916 right_dock.read(cx).visible_panel().unwrap().id(),
4917 panel_1.id()
4918 );
4919 assert_eq!(right_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
4920
4921 // Now we move panel_2Β to the left
4922 panel_2.set_position(DockPosition::Left, cx);
4923 });
4924
4925 workspace.update(cx, |workspace, cx| {
4926 // Since panel_2 was not visible on the right, we don't open the left dock.
4927 assert!(!workspace.left_dock().read(cx).is_open());
4928 // And the right dock is unaffected in it's displaying of panel_1
4929 assert!(workspace.right_dock().read(cx).is_open());
4930 assert_eq!(
4931 workspace
4932 .right_dock()
4933 .read(cx)
4934 .visible_panel()
4935 .unwrap()
4936 .id(),
4937 panel_1.id()
4938 );
4939 });
4940
4941 // Move panel_1 back to the left
4942 panel_1.update(cx, |panel_1, cx| {
4943 panel_1.set_position(DockPosition::Left, cx)
4944 });
4945
4946 workspace.update(cx, |workspace, cx| {
4947 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
4948 let left_dock = workspace.left_dock();
4949 assert!(left_dock.read(cx).is_open());
4950 assert_eq!(
4951 left_dock.read(cx).visible_panel().unwrap().id(),
4952 panel_1.id()
4953 );
4954 assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
4955 // And right the dock should be closed as it no longer has any panels.
4956 assert!(!workspace.right_dock().read(cx).is_open());
4957
4958 // Now we move panel_1 to the bottom
4959 panel_1.set_position(DockPosition::Bottom, cx);
4960 });
4961
4962 workspace.update(cx, |workspace, cx| {
4963 // Since panel_1 was visible on the left, we close the left dock.
4964 assert!(!workspace.left_dock().read(cx).is_open());
4965 // The bottom dock is sized based on the panel's default size,
4966 // since the panel orientation changed from vertical to horizontal.
4967 let bottom_dock = workspace.bottom_dock();
4968 assert_eq!(
4969 bottom_dock.read(cx).active_panel_size(cx).unwrap(),
4970 panel_1.size(cx),
4971 );
4972 // Close bottom dock and move panel_1 back to the left.
4973 bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
4974 panel_1.set_position(DockPosition::Left, cx);
4975 });
4976
4977 // Emit activated event on panel 1
4978 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Activated));
4979
4980 // Now the left dock is open and panel_1 is active and focused.
4981 workspace.read_with(cx, |workspace, cx| {
4982 let left_dock = workspace.left_dock();
4983 assert!(left_dock.read(cx).is_open());
4984 assert_eq!(
4985 left_dock.read(cx).visible_panel().unwrap().id(),
4986 panel_1.id()
4987 );
4988 assert!(panel_1.is_focused(cx));
4989 });
4990
4991 // Emit closed event on panel 2, which is not active
4992 panel_2.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed));
4993
4994 // Wo don't close the left dock, because panel_2 wasn't the active panel
4995 workspace.read_with(cx, |workspace, cx| {
4996 let left_dock = workspace.left_dock();
4997 assert!(left_dock.read(cx).is_open());
4998 assert_eq!(
4999 left_dock.read(cx).visible_panel().unwrap().id(),
5000 panel_1.id()
5001 );
5002 });
5003
5004 // Emitting a ZoomIn event shows the panel as zoomed.
5005 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomIn));
5006 workspace.read_with(cx, |workspace, _| {
5007 assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
5008 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
5009 });
5010
5011 // Move panel to another dock while it is zoomed
5012 panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
5013 workspace.read_with(cx, |workspace, _| {
5014 assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
5015 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5016 });
5017
5018 // If focus is transferred to another view that's not a panel or another pane, we still show
5019 // the panel as zoomed.
5020 let focus_receiver = window.add_view(cx, |_| EmptyView);
5021 focus_receiver.update(cx, |_, cx| cx.focus_self());
5022 workspace.read_with(cx, |workspace, _| {
5023 assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
5024 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5025 });
5026
5027 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
5028 workspace.update(cx, |_, cx| cx.focus_self());
5029 workspace.read_with(cx, |workspace, _| {
5030 assert_eq!(workspace.zoomed, None);
5031 assert_eq!(workspace.zoomed_position, None);
5032 });
5033
5034 // If focus is transferred again to another view that's not a panel or a pane, we won't
5035 // show the panel as zoomed because it wasn't zoomed before.
5036 focus_receiver.update(cx, |_, cx| cx.focus_self());
5037 workspace.read_with(cx, |workspace, _| {
5038 assert_eq!(workspace.zoomed, None);
5039 assert_eq!(workspace.zoomed_position, None);
5040 });
5041
5042 // When focus is transferred back to the panel, it is zoomed again.
5043 panel_1.update(cx, |_, cx| cx.focus_self());
5044 workspace.read_with(cx, |workspace, _| {
5045 assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
5046 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5047 });
5048
5049 // Emitting a ZoomOut event unzooms the panel.
5050 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomOut));
5051 workspace.read_with(cx, |workspace, _| {
5052 assert_eq!(workspace.zoomed, None);
5053 assert_eq!(workspace.zoomed_position, None);
5054 });
5055
5056 // Emit closed event on panel 1, which is active
5057 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed));
5058
5059 // Now the left dock is closed, because panel_1 was the active panel
5060 workspace.read_with(cx, |workspace, cx| {
5061 let right_dock = workspace.right_dock();
5062 assert!(!right_dock.read(cx).is_open());
5063 });
5064 }
5065
5066 pub fn init_test(cx: &mut TestAppContext) {
5067 cx.foreground().forbid_parking();
5068 cx.update(|cx| {
5069 cx.set_global(SettingsStore::test(cx));
5070 theme::init((), cx);
5071 language::init(cx);
5072 crate::init_settings(cx);
5073 Project::init_settings(cx);
5074 });
5075 }
5076}