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