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