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