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