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