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