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_behavior: Option<SaveIntent>,
167}
168
169#[derive(Clone, PartialEq, Debug, Deserialize)]
170#[serde(rename_all = "camelCase")]
171pub struct Save {
172 pub save_behavior: Option<SaveIntent>,
173}
174
175#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
176#[serde(rename_all = "camelCase")]
177pub struct CloseAllItemsAndPanes {
178 pub save_behavior: 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_behavior.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_behavior.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_behaviour: 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_behaviour,
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_behavior: 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(
1694 project,
1695 &pane,
1696 item_ix,
1697 item.as_ref(),
1698 save_behavior,
1699 &mut cx,
1700 )
1701 .await
1702 .map(|_| ())
1703 } else {
1704 Ok(())
1705 }
1706 })
1707 }
1708
1709 pub fn close_inactive_items_and_panes(
1710 &mut self,
1711 _: &CloseInactiveTabsAndPanes,
1712 cx: &mut ViewContext<Self>,
1713 ) -> Option<Task<Result<()>>> {
1714 self.close_all_internal(true, SaveIntent::Close, cx)
1715 }
1716
1717 pub fn close_all_items_and_panes(
1718 &mut self,
1719 action: &CloseAllItemsAndPanes,
1720 cx: &mut ViewContext<Self>,
1721 ) -> Option<Task<Result<()>>> {
1722 self.close_all_internal(false, action.save_behavior.unwrap_or(SaveIntent::Close), cx)
1723 }
1724
1725 fn close_all_internal(
1726 &mut self,
1727 retain_active_pane: bool,
1728 save_behavior: SaveIntent,
1729 cx: &mut ViewContext<Self>,
1730 ) -> Option<Task<Result<()>>> {
1731 let current_pane = self.active_pane();
1732
1733 let mut tasks = Vec::new();
1734
1735 if retain_active_pane {
1736 if let Some(current_pane_close) = current_pane.update(cx, |pane, cx| {
1737 pane.close_inactive_items(&CloseInactiveItems, cx)
1738 }) {
1739 tasks.push(current_pane_close);
1740 };
1741 }
1742
1743 for pane in self.panes() {
1744 if retain_active_pane && pane.id() == current_pane.id() {
1745 continue;
1746 }
1747
1748 if let Some(close_pane_items) = pane.update(cx, |pane: &mut Pane, cx| {
1749 pane.close_all_items(
1750 &CloseAllItems {
1751 save_behavior: Some(save_behavior),
1752 },
1753 cx,
1754 )
1755 }) {
1756 tasks.push(close_pane_items)
1757 }
1758 }
1759
1760 if tasks.is_empty() {
1761 None
1762 } else {
1763 Some(cx.spawn(|_, _| async move {
1764 for task in tasks {
1765 task.await?
1766 }
1767 Ok(())
1768 }))
1769 }
1770 }
1771
1772 pub fn toggle_dock(&mut self, dock_side: DockPosition, cx: &mut ViewContext<Self>) {
1773 let dock = match dock_side {
1774 DockPosition::Left => &self.left_dock,
1775 DockPosition::Bottom => &self.bottom_dock,
1776 DockPosition::Right => &self.right_dock,
1777 };
1778 let mut focus_center = false;
1779 let mut reveal_dock = false;
1780 dock.update(cx, |dock, cx| {
1781 let other_is_zoomed = self.zoomed.is_some() && self.zoomed_position != Some(dock_side);
1782 let was_visible = dock.is_open() && !other_is_zoomed;
1783 dock.set_open(!was_visible, cx);
1784
1785 if let Some(active_panel) = dock.active_panel() {
1786 if was_visible {
1787 if active_panel.has_focus(cx) {
1788 focus_center = true;
1789 }
1790 } else {
1791 cx.focus(active_panel.as_any());
1792 reveal_dock = true;
1793 }
1794 }
1795 });
1796
1797 if reveal_dock {
1798 self.dismiss_zoomed_items_to_reveal(Some(dock_side), cx);
1799 }
1800
1801 if focus_center {
1802 cx.focus_self();
1803 }
1804
1805 cx.notify();
1806 self.serialize_workspace(cx);
1807 }
1808
1809 pub fn close_all_docks(&mut self, cx: &mut ViewContext<Self>) {
1810 let docks = [&self.left_dock, &self.bottom_dock, &self.right_dock];
1811
1812 for dock in docks {
1813 dock.update(cx, |dock, cx| {
1814 dock.set_open(false, cx);
1815 });
1816 }
1817
1818 cx.focus_self();
1819 cx.notify();
1820 self.serialize_workspace(cx);
1821 }
1822
1823 /// Transfer focus to the panel of the given type.
1824 pub fn focus_panel<T: Panel>(&mut self, cx: &mut ViewContext<Self>) -> Option<ViewHandle<T>> {
1825 self.focus_or_unfocus_panel::<T>(cx, |_, _| true)?
1826 .as_any()
1827 .clone()
1828 .downcast()
1829 }
1830
1831 /// Focus the panel of the given type if it isn't already focused. If it is
1832 /// already focused, then transfer focus back to the workspace center.
1833 pub fn toggle_panel_focus<T: Panel>(&mut self, cx: &mut ViewContext<Self>) {
1834 self.focus_or_unfocus_panel::<T>(cx, |panel, cx| !panel.has_focus(cx));
1835 }
1836
1837 /// Focus or unfocus the given panel type, depending on the given callback.
1838 fn focus_or_unfocus_panel<T: Panel>(
1839 &mut self,
1840 cx: &mut ViewContext<Self>,
1841 should_focus: impl Fn(&dyn PanelHandle, &mut ViewContext<Dock>) -> bool,
1842 ) -> Option<Rc<dyn PanelHandle>> {
1843 for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
1844 if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
1845 let mut focus_center = false;
1846 let mut reveal_dock = false;
1847 let panel = dock.update(cx, |dock, cx| {
1848 dock.activate_panel(panel_index, cx);
1849
1850 let panel = dock.active_panel().cloned();
1851 if let Some(panel) = panel.as_ref() {
1852 if should_focus(&**panel, cx) {
1853 dock.set_open(true, cx);
1854 cx.focus(panel.as_any());
1855 reveal_dock = true;
1856 } else {
1857 // if panel.is_zoomed(cx) {
1858 // dock.set_open(false, cx);
1859 // }
1860 focus_center = true;
1861 }
1862 }
1863 panel
1864 });
1865
1866 if focus_center {
1867 cx.focus_self();
1868 }
1869
1870 self.serialize_workspace(cx);
1871 cx.notify();
1872 return panel;
1873 }
1874 }
1875 None
1876 }
1877
1878 pub fn panel<T: Panel>(&self, cx: &WindowContext) -> Option<ViewHandle<T>> {
1879 for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
1880 let dock = dock.read(cx);
1881 if let Some(panel) = dock.panel::<T>() {
1882 return Some(panel);
1883 }
1884 }
1885 None
1886 }
1887
1888 fn zoom_out(&mut self, cx: &mut ViewContext<Self>) {
1889 for pane in &self.panes {
1890 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
1891 }
1892
1893 self.left_dock.update(cx, |dock, cx| dock.zoom_out(cx));
1894 self.bottom_dock.update(cx, |dock, cx| dock.zoom_out(cx));
1895 self.right_dock.update(cx, |dock, cx| dock.zoom_out(cx));
1896 self.zoomed = None;
1897 self.zoomed_position = None;
1898
1899 cx.notify();
1900 }
1901
1902 #[cfg(any(test, feature = "test-support"))]
1903 pub fn zoomed_view(&self, cx: &AppContext) -> Option<AnyViewHandle> {
1904 self.zoomed.and_then(|view| view.upgrade(cx))
1905 }
1906
1907 fn dismiss_zoomed_items_to_reveal(
1908 &mut self,
1909 dock_to_reveal: Option<DockPosition>,
1910 cx: &mut ViewContext<Self>,
1911 ) {
1912 // If a center pane is zoomed, unzoom it.
1913 for pane in &self.panes {
1914 if pane != &self.active_pane || dock_to_reveal.is_some() {
1915 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
1916 }
1917 }
1918
1919 // If another dock is zoomed, hide it.
1920 let mut focus_center = false;
1921 for dock in [&self.left_dock, &self.right_dock, &self.bottom_dock] {
1922 dock.update(cx, |dock, cx| {
1923 if Some(dock.position()) != dock_to_reveal {
1924 if let Some(panel) = dock.active_panel() {
1925 if panel.is_zoomed(cx) {
1926 focus_center |= panel.has_focus(cx);
1927 dock.set_open(false, cx);
1928 }
1929 }
1930 }
1931 });
1932 }
1933
1934 if focus_center {
1935 cx.focus_self();
1936 }
1937
1938 if self.zoomed_position != dock_to_reveal {
1939 self.zoomed = None;
1940 self.zoomed_position = None;
1941 }
1942
1943 cx.notify();
1944 }
1945
1946 fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
1947 let pane = cx.add_view(|cx| {
1948 Pane::new(
1949 self.weak_handle(),
1950 self.project.clone(),
1951 self.app_state.background_actions,
1952 self.pane_history_timestamp.clone(),
1953 cx,
1954 )
1955 });
1956 cx.subscribe(&pane, Self::handle_pane_event).detach();
1957 self.panes.push(pane.clone());
1958 cx.focus(&pane);
1959 cx.emit(Event::PaneAdded(pane.clone()));
1960 pane
1961 }
1962
1963 pub fn add_item_to_center(
1964 &mut self,
1965 item: Box<dyn ItemHandle>,
1966 cx: &mut ViewContext<Self>,
1967 ) -> bool {
1968 if let Some(center_pane) = self.last_active_center_pane.clone() {
1969 if let Some(center_pane) = center_pane.upgrade(cx) {
1970 center_pane.update(cx, |pane, cx| pane.add_item(item, true, true, None, cx));
1971 true
1972 } else {
1973 false
1974 }
1975 } else {
1976 false
1977 }
1978 }
1979
1980 pub fn add_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
1981 self.active_pane
1982 .update(cx, |pane, cx| pane.add_item(item, true, true, None, cx));
1983 }
1984
1985 pub fn split_item(
1986 &mut self,
1987 split_direction: SplitDirection,
1988 item: Box<dyn ItemHandle>,
1989 cx: &mut ViewContext<Self>,
1990 ) {
1991 let new_pane = self.split_pane(self.active_pane.clone(), split_direction, cx);
1992 new_pane.update(cx, move |new_pane, cx| {
1993 new_pane.add_item(item, true, true, None, cx)
1994 })
1995 }
1996
1997 pub fn open_abs_path(
1998 &mut self,
1999 abs_path: PathBuf,
2000 visible: bool,
2001 cx: &mut ViewContext<Self>,
2002 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
2003 cx.spawn(|workspace, mut cx| async move {
2004 let open_paths_task_result = workspace
2005 .update(&mut cx, |workspace, cx| {
2006 workspace.open_paths(vec![abs_path.clone()], visible, cx)
2007 })
2008 .with_context(|| format!("open abs path {abs_path:?} task spawn"))?
2009 .await;
2010 anyhow::ensure!(
2011 open_paths_task_result.len() == 1,
2012 "open abs path {abs_path:?} task returned incorrect number of results"
2013 );
2014 match open_paths_task_result
2015 .into_iter()
2016 .next()
2017 .expect("ensured single task result")
2018 {
2019 Some(open_result) => {
2020 open_result.with_context(|| format!("open abs path {abs_path:?} task join"))
2021 }
2022 None => anyhow::bail!("open abs path {abs_path:?} task returned None"),
2023 }
2024 })
2025 }
2026
2027 pub fn split_abs_path(
2028 &mut self,
2029 abs_path: PathBuf,
2030 visible: bool,
2031 cx: &mut ViewContext<Self>,
2032 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
2033 let project_path_task =
2034 Workspace::project_path_for_path(self.project.clone(), &abs_path, visible, cx);
2035 cx.spawn(|this, mut cx| async move {
2036 let (_, path) = project_path_task.await?;
2037 this.update(&mut cx, |this, cx| this.split_path(path, cx))?
2038 .await
2039 })
2040 }
2041
2042 pub fn open_path(
2043 &mut self,
2044 path: impl Into<ProjectPath>,
2045 pane: Option<WeakViewHandle<Pane>>,
2046 focus_item: bool,
2047 cx: &mut ViewContext<Self>,
2048 ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
2049 let pane = pane.unwrap_or_else(|| {
2050 self.last_active_center_pane.clone().unwrap_or_else(|| {
2051 self.panes
2052 .first()
2053 .expect("There must be an active pane")
2054 .downgrade()
2055 })
2056 });
2057
2058 let task = self.load_path(path.into(), cx);
2059 cx.spawn(|_, mut cx| async move {
2060 let (project_entry_id, build_item) = task.await?;
2061 pane.update(&mut cx, |pane, cx| {
2062 pane.open_item(project_entry_id, focus_item, cx, build_item)
2063 })
2064 })
2065 }
2066
2067 pub fn split_path(
2068 &mut self,
2069 path: impl Into<ProjectPath>,
2070 cx: &mut ViewContext<Self>,
2071 ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
2072 let pane = self.last_active_center_pane.clone().unwrap_or_else(|| {
2073 self.panes
2074 .first()
2075 .expect("There must be an active pane")
2076 .downgrade()
2077 });
2078
2079 if let Member::Pane(center_pane) = &self.center.root {
2080 if center_pane.read(cx).items_len() == 0 {
2081 return self.open_path(path, Some(pane), true, cx);
2082 }
2083 }
2084
2085 let task = self.load_path(path.into(), cx);
2086 cx.spawn(|this, mut cx| async move {
2087 let (project_entry_id, build_item) = task.await?;
2088 this.update(&mut cx, move |this, cx| -> Option<_> {
2089 let pane = pane.upgrade(cx)?;
2090 let new_pane = this.split_pane(pane, SplitDirection::Right, cx);
2091 new_pane.update(cx, |new_pane, cx| {
2092 Some(new_pane.open_item(project_entry_id, true, cx, build_item))
2093 })
2094 })
2095 .map(|option| option.ok_or_else(|| anyhow!("pane was dropped")))?
2096 })
2097 }
2098
2099 pub(crate) fn load_path(
2100 &mut self,
2101 path: ProjectPath,
2102 cx: &mut ViewContext<Self>,
2103 ) -> Task<
2104 Result<(
2105 ProjectEntryId,
2106 impl 'static + FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
2107 )>,
2108 > {
2109 let project = self.project().clone();
2110 let project_item = project.update(cx, |project, cx| project.open_path(path, cx));
2111 cx.spawn(|_, mut cx| async move {
2112 let (project_entry_id, project_item) = project_item.await?;
2113 let build_item = cx.update(|cx| {
2114 cx.default_global::<ProjectItemBuilders>()
2115 .get(&project_item.model_type())
2116 .ok_or_else(|| anyhow!("no item builder for project item"))
2117 .cloned()
2118 })?;
2119 let build_item =
2120 move |cx: &mut ViewContext<Pane>| build_item(project, project_item, cx);
2121 Ok((project_entry_id, build_item))
2122 })
2123 }
2124
2125 pub fn open_project_item<T>(
2126 &mut self,
2127 project_item: ModelHandle<T::Item>,
2128 cx: &mut ViewContext<Self>,
2129 ) -> ViewHandle<T>
2130 where
2131 T: ProjectItem,
2132 {
2133 use project::Item as _;
2134
2135 let entry_id = project_item.read(cx).entry_id(cx);
2136 if let Some(item) = entry_id
2137 .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id, cx))
2138 .and_then(|item| item.downcast())
2139 {
2140 self.activate_item(&item, cx);
2141 return item;
2142 }
2143
2144 let item = cx.add_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
2145 self.add_item(Box::new(item.clone()), cx);
2146 item
2147 }
2148
2149 pub fn split_project_item<T>(
2150 &mut self,
2151 project_item: ModelHandle<T::Item>,
2152 cx: &mut ViewContext<Self>,
2153 ) -> ViewHandle<T>
2154 where
2155 T: ProjectItem,
2156 {
2157 use project::Item as _;
2158
2159 let entry_id = project_item.read(cx).entry_id(cx);
2160 if let Some(item) = entry_id
2161 .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id, cx))
2162 .and_then(|item| item.downcast())
2163 {
2164 self.activate_item(&item, cx);
2165 return item;
2166 }
2167
2168 let item = cx.add_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
2169 self.split_item(SplitDirection::Right, Box::new(item.clone()), cx);
2170 item
2171 }
2172
2173 pub fn open_shared_screen(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
2174 if let Some(shared_screen) = self.shared_screen_for_peer(peer_id, &self.active_pane, cx) {
2175 self.active_pane.update(cx, |pane, cx| {
2176 pane.add_item(Box::new(shared_screen), false, true, None, cx)
2177 });
2178 }
2179 }
2180
2181 pub fn activate_item(&mut self, item: &dyn ItemHandle, cx: &mut ViewContext<Self>) -> bool {
2182 let result = self.panes.iter().find_map(|pane| {
2183 pane.read(cx)
2184 .index_for_item(item)
2185 .map(|ix| (pane.clone(), ix))
2186 });
2187 if let Some((pane, ix)) = result {
2188 pane.update(cx, |pane, cx| pane.activate_item(ix, true, true, cx));
2189 true
2190 } else {
2191 false
2192 }
2193 }
2194
2195 fn activate_pane_at_index(&mut self, action: &ActivatePane, cx: &mut ViewContext<Self>) {
2196 let panes = self.center.panes();
2197 if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
2198 cx.focus(&pane);
2199 } else {
2200 self.split_and_clone(self.active_pane.clone(), SplitDirection::Right, cx);
2201 }
2202 }
2203
2204 pub fn activate_next_pane(&mut self, cx: &mut ViewContext<Self>) {
2205 let panes = self.center.panes();
2206 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
2207 let next_ix = (ix + 1) % panes.len();
2208 let next_pane = panes[next_ix].clone();
2209 cx.focus(&next_pane);
2210 }
2211 }
2212
2213 pub fn activate_previous_pane(&mut self, cx: &mut ViewContext<Self>) {
2214 let panes = self.center.panes();
2215 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
2216 let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
2217 let prev_pane = panes[prev_ix].clone();
2218 cx.focus(&prev_pane);
2219 }
2220 }
2221
2222 pub fn activate_pane_in_direction(
2223 &mut self,
2224 direction: SplitDirection,
2225 cx: &mut ViewContext<Self>,
2226 ) {
2227 let bounding_box = match self.center.bounding_box_for_pane(&self.active_pane) {
2228 Some(coordinates) => coordinates,
2229 None => {
2230 return;
2231 }
2232 };
2233 let cursor = self.active_pane.read(cx).pixel_position_of_cursor(cx);
2234 let center = match cursor {
2235 Some(cursor) if bounding_box.contains_point(cursor) => cursor,
2236 _ => bounding_box.center(),
2237 };
2238
2239 let distance_to_next = theme::current(cx).workspace.pane_divider.width + 1.;
2240
2241 let target = match direction {
2242 SplitDirection::Left => vec2f(bounding_box.origin_x() - distance_to_next, center.y()),
2243 SplitDirection::Right => vec2f(bounding_box.max_x() + distance_to_next, center.y()),
2244 SplitDirection::Up => vec2f(center.x(), bounding_box.origin_y() - distance_to_next),
2245 SplitDirection::Down => vec2f(center.x(), bounding_box.max_y() + distance_to_next),
2246 };
2247
2248 if let Some(pane) = self.center.pane_at_pixel_position(target) {
2249 cx.focus(pane);
2250 }
2251 }
2252
2253 fn handle_pane_focused(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
2254 if self.active_pane != pane {
2255 self.active_pane = pane.clone();
2256 self.status_bar.update(cx, |status_bar, cx| {
2257 status_bar.set_active_pane(&self.active_pane, cx);
2258 });
2259 self.active_item_path_changed(cx);
2260 self.last_active_center_pane = Some(pane.downgrade());
2261 }
2262
2263 self.dismiss_zoomed_items_to_reveal(None, cx);
2264 if pane.read(cx).is_zoomed() {
2265 self.zoomed = Some(pane.downgrade().into_any());
2266 } else {
2267 self.zoomed = None;
2268 }
2269 self.zoomed_position = None;
2270 self.update_active_view_for_followers(cx);
2271
2272 cx.notify();
2273 }
2274
2275 fn handle_pane_event(
2276 &mut self,
2277 pane: ViewHandle<Pane>,
2278 event: &pane::Event,
2279 cx: &mut ViewContext<Self>,
2280 ) {
2281 match event {
2282 pane::Event::AddItem { item } => item.added_to_pane(self, pane, cx),
2283 pane::Event::Split(direction) => {
2284 self.split_and_clone(pane, *direction, cx);
2285 }
2286 pane::Event::Remove => self.remove_pane(pane, cx),
2287 pane::Event::ActivateItem { local } => {
2288 if *local {
2289 self.unfollow(&pane, cx);
2290 }
2291 if &pane == self.active_pane() {
2292 self.active_item_path_changed(cx);
2293 }
2294 }
2295 pane::Event::ChangeItemTitle => {
2296 if pane == self.active_pane {
2297 self.active_item_path_changed(cx);
2298 }
2299 self.update_window_edited(cx);
2300 }
2301 pane::Event::RemoveItem { item_id } => {
2302 self.update_window_edited(cx);
2303 if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) {
2304 if entry.get().id() == pane.id() {
2305 entry.remove();
2306 }
2307 }
2308 }
2309 pane::Event::Focus => {
2310 self.handle_pane_focused(pane.clone(), cx);
2311 }
2312 pane::Event::ZoomIn => {
2313 if pane == self.active_pane {
2314 pane.update(cx, |pane, cx| pane.set_zoomed(true, cx));
2315 if pane.read(cx).has_focus() {
2316 self.zoomed = Some(pane.downgrade().into_any());
2317 self.zoomed_position = None;
2318 }
2319 cx.notify();
2320 }
2321 }
2322 pane::Event::ZoomOut => {
2323 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
2324 if self.zoomed_position.is_none() {
2325 self.zoomed = None;
2326 }
2327 cx.notify();
2328 }
2329 }
2330
2331 self.serialize_workspace(cx);
2332 }
2333
2334 pub fn split_pane(
2335 &mut self,
2336 pane_to_split: ViewHandle<Pane>,
2337 split_direction: SplitDirection,
2338 cx: &mut ViewContext<Self>,
2339 ) -> ViewHandle<Pane> {
2340 let new_pane = self.add_pane(cx);
2341 self.center
2342 .split(&pane_to_split, &new_pane, split_direction)
2343 .unwrap();
2344 cx.notify();
2345 new_pane
2346 }
2347
2348 pub fn split_and_clone(
2349 &mut self,
2350 pane: ViewHandle<Pane>,
2351 direction: SplitDirection,
2352 cx: &mut ViewContext<Self>,
2353 ) -> Option<ViewHandle<Pane>> {
2354 let item = pane.read(cx).active_item()?;
2355 let maybe_pane_handle = if let Some(clone) = item.clone_on_split(self.database_id(), cx) {
2356 let new_pane = self.add_pane(cx);
2357 new_pane.update(cx, |pane, cx| pane.add_item(clone, true, true, None, cx));
2358 self.center.split(&pane, &new_pane, direction).unwrap();
2359 Some(new_pane)
2360 } else {
2361 None
2362 };
2363 cx.notify();
2364 maybe_pane_handle
2365 }
2366
2367 pub fn split_pane_with_item(
2368 &mut self,
2369 pane_to_split: WeakViewHandle<Pane>,
2370 split_direction: SplitDirection,
2371 from: WeakViewHandle<Pane>,
2372 item_id_to_move: usize,
2373 cx: &mut ViewContext<Self>,
2374 ) {
2375 let Some(pane_to_split) = pane_to_split.upgrade(cx) else {
2376 return;
2377 };
2378 let Some(from) = from.upgrade(cx) else {
2379 return;
2380 };
2381
2382 let new_pane = self.add_pane(cx);
2383 self.move_item(from.clone(), new_pane.clone(), item_id_to_move, 0, cx);
2384 self.center
2385 .split(&pane_to_split, &new_pane, split_direction)
2386 .unwrap();
2387 cx.notify();
2388 }
2389
2390 pub fn split_pane_with_project_entry(
2391 &mut self,
2392 pane_to_split: WeakViewHandle<Pane>,
2393 split_direction: SplitDirection,
2394 project_entry: ProjectEntryId,
2395 cx: &mut ViewContext<Self>,
2396 ) -> Option<Task<Result<()>>> {
2397 let pane_to_split = pane_to_split.upgrade(cx)?;
2398 let new_pane = self.add_pane(cx);
2399 self.center
2400 .split(&pane_to_split, &new_pane, split_direction)
2401 .unwrap();
2402
2403 let path = self.project.read(cx).path_for_entry(project_entry, cx)?;
2404 let task = self.open_path(path, Some(new_pane.downgrade()), true, cx);
2405 Some(cx.foreground().spawn(async move {
2406 task.await?;
2407 Ok(())
2408 }))
2409 }
2410
2411 pub fn move_item(
2412 &mut self,
2413 source: ViewHandle<Pane>,
2414 destination: ViewHandle<Pane>,
2415 item_id_to_move: usize,
2416 destination_index: usize,
2417 cx: &mut ViewContext<Self>,
2418 ) {
2419 let item_to_move = source
2420 .read(cx)
2421 .items()
2422 .enumerate()
2423 .find(|(_, item_handle)| item_handle.id() == item_id_to_move);
2424
2425 if item_to_move.is_none() {
2426 log::warn!("Tried to move item handle which was not in `from` pane. Maybe tab was closed during drop");
2427 return;
2428 }
2429 let (item_ix, item_handle) = item_to_move.unwrap();
2430 let item_handle = item_handle.clone();
2431
2432 if source != destination {
2433 // Close item from previous pane
2434 source.update(cx, |source, cx| {
2435 source.remove_item(item_ix, false, cx);
2436 });
2437 }
2438
2439 // This automatically removes duplicate items in the pane
2440 destination.update(cx, |destination, cx| {
2441 destination.add_item(item_handle, true, true, Some(destination_index), cx);
2442 cx.focus_self();
2443 });
2444 }
2445
2446 fn remove_pane(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
2447 if self.center.remove(&pane).unwrap() {
2448 self.force_remove_pane(&pane, cx);
2449 self.unfollow(&pane, cx);
2450 self.last_leaders_by_pane.remove(&pane.downgrade());
2451 for removed_item in pane.read(cx).items() {
2452 self.panes_by_item.remove(&removed_item.id());
2453 }
2454
2455 cx.notify();
2456 } else {
2457 self.active_item_path_changed(cx);
2458 }
2459 }
2460
2461 pub fn panes(&self) -> &[ViewHandle<Pane>] {
2462 &self.panes
2463 }
2464
2465 pub fn active_pane(&self) -> &ViewHandle<Pane> {
2466 &self.active_pane
2467 }
2468
2469 fn project_remote_id_changed(&mut self, remote_id: Option<u64>, cx: &mut ViewContext<Self>) {
2470 if let Some(remote_id) = remote_id {
2471 self.remote_entity_subscription = Some(
2472 self.app_state
2473 .client
2474 .add_view_for_remote_entity(remote_id, cx),
2475 );
2476 } else {
2477 self.remote_entity_subscription.take();
2478 }
2479 }
2480
2481 fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
2482 self.leader_state.followers.remove(&peer_id);
2483 if let Some(states_by_pane) = self.follower_states_by_leader.remove(&peer_id) {
2484 for state in states_by_pane.into_values() {
2485 for item in state.items_by_leader_view_id.into_values() {
2486 item.set_leader_replica_id(None, cx);
2487 }
2488 }
2489 }
2490 cx.notify();
2491 }
2492
2493 pub fn toggle_follow(
2494 &mut self,
2495 leader_id: PeerId,
2496 cx: &mut ViewContext<Self>,
2497 ) -> Option<Task<Result<()>>> {
2498 let pane = self.active_pane().clone();
2499
2500 if let Some(prev_leader_id) = self.unfollow(&pane, cx) {
2501 if leader_id == prev_leader_id {
2502 return None;
2503 }
2504 }
2505
2506 self.last_leaders_by_pane
2507 .insert(pane.downgrade(), leader_id);
2508 self.follower_states_by_leader
2509 .entry(leader_id)
2510 .or_default()
2511 .insert(pane.clone(), Default::default());
2512 cx.notify();
2513
2514 let project_id = self.project.read(cx).remote_id()?;
2515 let request = self.app_state.client.request(proto::Follow {
2516 project_id,
2517 leader_id: Some(leader_id),
2518 });
2519
2520 Some(cx.spawn(|this, mut cx| async move {
2521 let response = request.await?;
2522 this.update(&mut cx, |this, _| {
2523 let state = this
2524 .follower_states_by_leader
2525 .get_mut(&leader_id)
2526 .and_then(|states_by_pane| states_by_pane.get_mut(&pane))
2527 .ok_or_else(|| anyhow!("following interrupted"))?;
2528 state.active_view_id = if let Some(active_view_id) = response.active_view_id {
2529 Some(ViewId::from_proto(active_view_id)?)
2530 } else {
2531 None
2532 };
2533 Ok::<_, anyhow::Error>(())
2534 })??;
2535 Self::add_views_from_leader(
2536 this.clone(),
2537 leader_id,
2538 vec![pane],
2539 response.views,
2540 &mut cx,
2541 )
2542 .await?;
2543 this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx))?;
2544 Ok(())
2545 }))
2546 }
2547
2548 pub fn follow_next_collaborator(
2549 &mut self,
2550 _: &FollowNextCollaborator,
2551 cx: &mut ViewContext<Self>,
2552 ) -> Option<Task<Result<()>>> {
2553 let collaborators = self.project.read(cx).collaborators();
2554 let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
2555 let mut collaborators = collaborators.keys().copied();
2556 for peer_id in collaborators.by_ref() {
2557 if peer_id == leader_id {
2558 break;
2559 }
2560 }
2561 collaborators.next()
2562 } else if let Some(last_leader_id) =
2563 self.last_leaders_by_pane.get(&self.active_pane.downgrade())
2564 {
2565 if collaborators.contains_key(last_leader_id) {
2566 Some(*last_leader_id)
2567 } else {
2568 None
2569 }
2570 } else {
2571 None
2572 };
2573
2574 next_leader_id
2575 .or_else(|| collaborators.keys().copied().next())
2576 .and_then(|leader_id| self.toggle_follow(leader_id, cx))
2577 }
2578
2579 pub fn unfollow(
2580 &mut self,
2581 pane: &ViewHandle<Pane>,
2582 cx: &mut ViewContext<Self>,
2583 ) -> Option<PeerId> {
2584 for (leader_id, states_by_pane) in &mut self.follower_states_by_leader {
2585 let leader_id = *leader_id;
2586 if let Some(state) = states_by_pane.remove(pane) {
2587 for (_, item) in state.items_by_leader_view_id {
2588 item.set_leader_replica_id(None, cx);
2589 }
2590
2591 if states_by_pane.is_empty() {
2592 self.follower_states_by_leader.remove(&leader_id);
2593 if let Some(project_id) = self.project.read(cx).remote_id() {
2594 self.app_state
2595 .client
2596 .send(proto::Unfollow {
2597 project_id,
2598 leader_id: Some(leader_id),
2599 })
2600 .log_err();
2601 }
2602 }
2603
2604 cx.notify();
2605 return Some(leader_id);
2606 }
2607 }
2608 None
2609 }
2610
2611 pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
2612 self.follower_states_by_leader.contains_key(&peer_id)
2613 }
2614
2615 pub fn is_followed_by(&self, peer_id: PeerId) -> bool {
2616 self.leader_state.followers.contains(&peer_id)
2617 }
2618
2619 fn render_titlebar(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
2620 // TODO: There should be a better system in place for this
2621 // (https://github.com/zed-industries/zed/issues/1290)
2622 let is_fullscreen = cx.window_is_fullscreen();
2623 let container_theme = if is_fullscreen {
2624 let mut container_theme = theme.titlebar.container;
2625 container_theme.padding.left = container_theme.padding.right;
2626 container_theme
2627 } else {
2628 theme.titlebar.container
2629 };
2630
2631 enum TitleBar {}
2632 MouseEventHandler::new::<TitleBar, _>(0, cx, |_, cx| {
2633 Stack::new()
2634 .with_children(
2635 self.titlebar_item
2636 .as_ref()
2637 .map(|item| ChildView::new(item, cx)),
2638 )
2639 .contained()
2640 .with_style(container_theme)
2641 })
2642 .on_click(MouseButton::Left, |event, _, cx| {
2643 if event.click_count == 2 {
2644 cx.zoom_window();
2645 }
2646 })
2647 .constrained()
2648 .with_height(theme.titlebar.height)
2649 .into_any_named("titlebar")
2650 }
2651
2652 fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
2653 let active_entry = self.active_project_path(cx);
2654 self.project
2655 .update(cx, |project, cx| project.set_active_path(active_entry, cx));
2656 self.update_window_title(cx);
2657 }
2658
2659 fn update_window_title(&mut self, cx: &mut ViewContext<Self>) {
2660 let project = self.project().read(cx);
2661 let mut title = String::new();
2662
2663 if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
2664 let filename = path
2665 .path
2666 .file_name()
2667 .map(|s| s.to_string_lossy())
2668 .or_else(|| {
2669 Some(Cow::Borrowed(
2670 project
2671 .worktree_for_id(path.worktree_id, cx)?
2672 .read(cx)
2673 .root_name(),
2674 ))
2675 });
2676
2677 if let Some(filename) = filename {
2678 title.push_str(filename.as_ref());
2679 title.push_str(" β ");
2680 }
2681 }
2682
2683 for (i, name) in project.worktree_root_names(cx).enumerate() {
2684 if i > 0 {
2685 title.push_str(", ");
2686 }
2687 title.push_str(name);
2688 }
2689
2690 if title.is_empty() {
2691 title = "empty project".to_string();
2692 }
2693
2694 if project.is_remote() {
2695 title.push_str(" β");
2696 } else if project.is_shared() {
2697 title.push_str(" β");
2698 }
2699
2700 cx.set_window_title(&title);
2701 }
2702
2703 fn update_window_edited(&mut self, cx: &mut ViewContext<Self>) {
2704 let is_edited = !self.project.read(cx).is_read_only()
2705 && self
2706 .items(cx)
2707 .any(|item| item.has_conflict(cx) || item.is_dirty(cx));
2708 if is_edited != self.window_edited {
2709 self.window_edited = is_edited;
2710 cx.set_window_edited(self.window_edited)
2711 }
2712 }
2713
2714 fn render_disconnected_overlay(
2715 &self,
2716 cx: &mut ViewContext<Workspace>,
2717 ) -> Option<AnyElement<Workspace>> {
2718 if self.project.read(cx).is_read_only() {
2719 enum DisconnectedOverlay {}
2720 Some(
2721 MouseEventHandler::new::<DisconnectedOverlay, _>(0, cx, |_, cx| {
2722 let theme = &theme::current(cx);
2723 Label::new(
2724 "Your connection to the remote project has been lost.",
2725 theme.workspace.disconnected_overlay.text.clone(),
2726 )
2727 .aligned()
2728 .contained()
2729 .with_style(theme.workspace.disconnected_overlay.container)
2730 })
2731 .with_cursor_style(CursorStyle::Arrow)
2732 .capture_all()
2733 .into_any_named("disconnected overlay"),
2734 )
2735 } else {
2736 None
2737 }
2738 }
2739
2740 fn render_notifications(
2741 &self,
2742 theme: &theme::Workspace,
2743 cx: &AppContext,
2744 ) -> Option<AnyElement<Workspace>> {
2745 if self.notifications.is_empty() {
2746 None
2747 } else {
2748 Some(
2749 Flex::column()
2750 .with_children(self.notifications.iter().map(|(_, _, notification)| {
2751 ChildView::new(notification.as_any(), cx)
2752 .contained()
2753 .with_style(theme.notification)
2754 }))
2755 .constrained()
2756 .with_width(theme.notifications.width)
2757 .contained()
2758 .with_style(theme.notifications.container)
2759 .aligned()
2760 .bottom()
2761 .right()
2762 .into_any(),
2763 )
2764 }
2765 }
2766
2767 // RPC handlers
2768
2769 async fn handle_follow(
2770 this: WeakViewHandle<Self>,
2771 envelope: TypedEnvelope<proto::Follow>,
2772 _: Arc<Client>,
2773 mut cx: AsyncAppContext,
2774 ) -> Result<proto::FollowResponse> {
2775 this.update(&mut cx, |this, cx| {
2776 let client = &this.app_state.client;
2777 this.leader_state
2778 .followers
2779 .insert(envelope.original_sender_id()?);
2780
2781 let active_view_id = this.active_item(cx).and_then(|i| {
2782 Some(
2783 i.to_followable_item_handle(cx)?
2784 .remote_id(client, cx)?
2785 .to_proto(),
2786 )
2787 });
2788
2789 cx.notify();
2790
2791 Ok(proto::FollowResponse {
2792 active_view_id,
2793 views: this
2794 .panes()
2795 .iter()
2796 .flat_map(|pane| {
2797 let leader_id = this.leader_for_pane(pane);
2798 pane.read(cx).items().filter_map({
2799 let cx = &cx;
2800 move |item| {
2801 let item = item.to_followable_item_handle(cx)?;
2802 let id = item.remote_id(client, cx)?.to_proto();
2803 let variant = item.to_state_proto(cx)?;
2804 Some(proto::View {
2805 id: Some(id),
2806 leader_id,
2807 variant: Some(variant),
2808 })
2809 }
2810 })
2811 })
2812 .collect(),
2813 })
2814 })?
2815 }
2816
2817 async fn handle_unfollow(
2818 this: WeakViewHandle<Self>,
2819 envelope: TypedEnvelope<proto::Unfollow>,
2820 _: Arc<Client>,
2821 mut cx: AsyncAppContext,
2822 ) -> Result<()> {
2823 this.update(&mut cx, |this, cx| {
2824 this.leader_state
2825 .followers
2826 .remove(&envelope.original_sender_id()?);
2827 cx.notify();
2828 Ok(())
2829 })?
2830 }
2831
2832 async fn handle_update_followers(
2833 this: WeakViewHandle<Self>,
2834 envelope: TypedEnvelope<proto::UpdateFollowers>,
2835 _: Arc<Client>,
2836 cx: AsyncAppContext,
2837 ) -> Result<()> {
2838 let leader_id = envelope.original_sender_id()?;
2839 this.read_with(&cx, |this, _| {
2840 this.leader_updates_tx
2841 .unbounded_send((leader_id, envelope.payload))
2842 })??;
2843 Ok(())
2844 }
2845
2846 async fn process_leader_update(
2847 this: &WeakViewHandle<Self>,
2848 leader_id: PeerId,
2849 update: proto::UpdateFollowers,
2850 cx: &mut AsyncAppContext,
2851 ) -> Result<()> {
2852 match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
2853 proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
2854 this.update(cx, |this, _| {
2855 if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
2856 for state in state.values_mut() {
2857 state.active_view_id =
2858 if let Some(active_view_id) = update_active_view.id.clone() {
2859 Some(ViewId::from_proto(active_view_id)?)
2860 } else {
2861 None
2862 };
2863 }
2864 }
2865 anyhow::Ok(())
2866 })??;
2867 }
2868 proto::update_followers::Variant::UpdateView(update_view) => {
2869 let variant = update_view
2870 .variant
2871 .ok_or_else(|| anyhow!("missing update view variant"))?;
2872 let id = update_view
2873 .id
2874 .ok_or_else(|| anyhow!("missing update view id"))?;
2875 let mut tasks = Vec::new();
2876 this.update(cx, |this, cx| {
2877 let project = this.project.clone();
2878 if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
2879 for state in state.values_mut() {
2880 let view_id = ViewId::from_proto(id.clone())?;
2881 if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
2882 tasks.push(item.apply_update_proto(&project, variant.clone(), cx));
2883 }
2884 }
2885 }
2886 anyhow::Ok(())
2887 })??;
2888 try_join_all(tasks).await.log_err();
2889 }
2890 proto::update_followers::Variant::CreateView(view) => {
2891 let panes = this.read_with(cx, |this, _| {
2892 this.follower_states_by_leader
2893 .get(&leader_id)
2894 .into_iter()
2895 .flat_map(|states_by_pane| states_by_pane.keys())
2896 .cloned()
2897 .collect()
2898 })?;
2899 Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], cx).await?;
2900 }
2901 }
2902 this.update(cx, |this, cx| this.leader_updated(leader_id, cx))?;
2903 Ok(())
2904 }
2905
2906 async fn add_views_from_leader(
2907 this: WeakViewHandle<Self>,
2908 leader_id: PeerId,
2909 panes: Vec<ViewHandle<Pane>>,
2910 views: Vec<proto::View>,
2911 cx: &mut AsyncAppContext,
2912 ) -> Result<()> {
2913 let this = this
2914 .upgrade(cx)
2915 .ok_or_else(|| anyhow!("workspace dropped"))?;
2916 let project = this
2917 .read_with(cx, |this, _| this.project.clone())
2918 .ok_or_else(|| anyhow!("window dropped"))?;
2919
2920 let replica_id = project
2921 .read_with(cx, |project, _| {
2922 project
2923 .collaborators()
2924 .get(&leader_id)
2925 .map(|c| c.replica_id)
2926 })
2927 .ok_or_else(|| anyhow!("no such collaborator {}", leader_id))?;
2928
2929 let item_builders = cx.update(|cx| {
2930 cx.default_global::<FollowableItemBuilders>()
2931 .values()
2932 .map(|b| b.0)
2933 .collect::<Vec<_>>()
2934 });
2935
2936 let mut item_tasks_by_pane = HashMap::default();
2937 for pane in panes {
2938 let mut item_tasks = Vec::new();
2939 let mut leader_view_ids = Vec::new();
2940 for view in &views {
2941 let Some(id) = &view.id else { continue };
2942 let id = ViewId::from_proto(id.clone())?;
2943 let mut variant = view.variant.clone();
2944 if variant.is_none() {
2945 Err(anyhow!("missing view variant"))?;
2946 }
2947 for build_item in &item_builders {
2948 let task = cx
2949 .update(|cx| build_item(pane.clone(), this.clone(), id, &mut variant, cx));
2950 if let Some(task) = task {
2951 item_tasks.push(task);
2952 leader_view_ids.push(id);
2953 break;
2954 } else {
2955 assert!(variant.is_some());
2956 }
2957 }
2958 }
2959
2960 item_tasks_by_pane.insert(pane, (item_tasks, leader_view_ids));
2961 }
2962
2963 for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane {
2964 let items = futures::future::try_join_all(item_tasks).await?;
2965 this.update(cx, |this, cx| {
2966 let state = this
2967 .follower_states_by_leader
2968 .get_mut(&leader_id)?
2969 .get_mut(&pane)?;
2970
2971 for (id, item) in leader_view_ids.into_iter().zip(items) {
2972 item.set_leader_replica_id(Some(replica_id), cx);
2973 state.items_by_leader_view_id.insert(id, item);
2974 }
2975
2976 Some(())
2977 });
2978 }
2979 Ok(())
2980 }
2981
2982 fn update_active_view_for_followers(&self, cx: &AppContext) {
2983 if self.active_pane.read(cx).has_focus() {
2984 self.update_followers(
2985 proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView {
2986 id: self.active_item(cx).and_then(|item| {
2987 item.to_followable_item_handle(cx)?
2988 .remote_id(&self.app_state.client, cx)
2989 .map(|id| id.to_proto())
2990 }),
2991 leader_id: self.leader_for_pane(&self.active_pane),
2992 }),
2993 cx,
2994 );
2995 } else {
2996 self.update_followers(
2997 proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView {
2998 id: None,
2999 leader_id: None,
3000 }),
3001 cx,
3002 );
3003 }
3004 }
3005
3006 fn update_followers(
3007 &self,
3008 update: proto::update_followers::Variant,
3009 cx: &AppContext,
3010 ) -> Option<()> {
3011 let project_id = self.project.read(cx).remote_id()?;
3012 if !self.leader_state.followers.is_empty() {
3013 self.app_state
3014 .client
3015 .send(proto::UpdateFollowers {
3016 project_id,
3017 follower_ids: self.leader_state.followers.iter().copied().collect(),
3018 variant: Some(update),
3019 })
3020 .log_err();
3021 }
3022 None
3023 }
3024
3025 pub fn leader_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<PeerId> {
3026 self.follower_states_by_leader
3027 .iter()
3028 .find_map(|(leader_id, state)| {
3029 if state.contains_key(pane) {
3030 Some(*leader_id)
3031 } else {
3032 None
3033 }
3034 })
3035 }
3036
3037 fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
3038 cx.notify();
3039
3040 let call = self.active_call()?;
3041 let room = call.read(cx).room()?.read(cx);
3042 let participant = room.remote_participant_for_peer_id(leader_id)?;
3043 let mut items_to_activate = Vec::new();
3044 match participant.location {
3045 call::ParticipantLocation::SharedProject { project_id } => {
3046 if Some(project_id) == self.project.read(cx).remote_id() {
3047 for (pane, state) in self.follower_states_by_leader.get(&leader_id)? {
3048 if let Some(item) = state
3049 .active_view_id
3050 .and_then(|id| state.items_by_leader_view_id.get(&id))
3051 {
3052 items_to_activate.push((pane.clone(), item.boxed_clone()));
3053 } else if let Some(shared_screen) =
3054 self.shared_screen_for_peer(leader_id, pane, cx)
3055 {
3056 items_to_activate.push((pane.clone(), Box::new(shared_screen)));
3057 }
3058 }
3059 }
3060 }
3061 call::ParticipantLocation::UnsharedProject => {}
3062 call::ParticipantLocation::External => {
3063 for (pane, _) in self.follower_states_by_leader.get(&leader_id)? {
3064 if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
3065 items_to_activate.push((pane.clone(), Box::new(shared_screen)));
3066 }
3067 }
3068 }
3069 }
3070
3071 for (pane, item) in items_to_activate {
3072 let pane_was_focused = pane.read(cx).has_focus();
3073 if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) {
3074 pane.update(cx, |pane, cx| pane.activate_item(index, false, false, cx));
3075 } else {
3076 pane.update(cx, |pane, cx| {
3077 pane.add_item(item.boxed_clone(), false, false, None, cx)
3078 });
3079 }
3080
3081 if pane_was_focused {
3082 pane.update(cx, |pane, cx| pane.focus_active_item(cx));
3083 }
3084 }
3085
3086 None
3087 }
3088
3089 fn shared_screen_for_peer(
3090 &self,
3091 peer_id: PeerId,
3092 pane: &ViewHandle<Pane>,
3093 cx: &mut ViewContext<Self>,
3094 ) -> Option<ViewHandle<SharedScreen>> {
3095 let call = self.active_call()?;
3096 let room = call.read(cx).room()?.read(cx);
3097 let participant = room.remote_participant_for_peer_id(peer_id)?;
3098 let track = participant.video_tracks.values().next()?.clone();
3099 let user = participant.user.clone();
3100
3101 for item in pane.read(cx).items_of_type::<SharedScreen>() {
3102 if item.read(cx).peer_id == peer_id {
3103 return Some(item);
3104 }
3105 }
3106
3107 Some(cx.add_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx)))
3108 }
3109
3110 pub fn on_window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
3111 if active {
3112 cx.background()
3113 .spawn(persistence::DB.update_timestamp(self.database_id()))
3114 .detach();
3115 } else {
3116 for pane in &self.panes {
3117 pane.update(cx, |pane, cx| {
3118 if let Some(item) = pane.active_item() {
3119 item.workspace_deactivated(cx);
3120 }
3121 if matches!(
3122 settings::get::<WorkspaceSettings>(cx).autosave,
3123 AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
3124 ) {
3125 for item in pane.items() {
3126 Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
3127 .detach_and_log_err(cx);
3128 }
3129 }
3130 });
3131 }
3132 }
3133 }
3134
3135 fn active_call(&self) -> Option<&ModelHandle<ActiveCall>> {
3136 self.active_call.as_ref().map(|(call, _)| call)
3137 }
3138
3139 fn on_active_call_event(
3140 &mut self,
3141 _: ModelHandle<ActiveCall>,
3142 event: &call::room::Event,
3143 cx: &mut ViewContext<Self>,
3144 ) {
3145 match event {
3146 call::room::Event::ParticipantLocationChanged { participant_id }
3147 | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
3148 self.leader_updated(*participant_id, cx);
3149 }
3150 _ => {}
3151 }
3152 }
3153
3154 pub fn database_id(&self) -> WorkspaceId {
3155 self.database_id
3156 }
3157
3158 fn location(&self, cx: &AppContext) -> Option<WorkspaceLocation> {
3159 let project = self.project().read(cx);
3160
3161 if project.is_local() {
3162 Some(
3163 project
3164 .visible_worktrees(cx)
3165 .map(|worktree| worktree.read(cx).abs_path())
3166 .collect::<Vec<_>>()
3167 .into(),
3168 )
3169 } else {
3170 None
3171 }
3172 }
3173
3174 fn remove_panes(&mut self, member: Member, cx: &mut ViewContext<Workspace>) {
3175 match member {
3176 Member::Axis(PaneAxis { members, .. }) => {
3177 for child in members.iter() {
3178 self.remove_panes(child.clone(), cx)
3179 }
3180 }
3181 Member::Pane(pane) => {
3182 self.force_remove_pane(&pane, cx);
3183 }
3184 }
3185 }
3186
3187 fn force_remove_pane(&mut self, pane: &ViewHandle<Pane>, cx: &mut ViewContext<Workspace>) {
3188 self.panes.retain(|p| p != pane);
3189 cx.focus(self.panes.last().unwrap());
3190 if self.last_active_center_pane == Some(pane.downgrade()) {
3191 self.last_active_center_pane = None;
3192 }
3193 cx.notify();
3194 }
3195
3196 fn schedule_serialize(&mut self, cx: &mut ViewContext<Self>) {
3197 self._schedule_serialize = Some(cx.spawn(|this, cx| async move {
3198 cx.background().timer(Duration::from_millis(100)).await;
3199 this.read_with(&cx, |this, cx| this.serialize_workspace(cx))
3200 .ok();
3201 }));
3202 }
3203
3204 fn serialize_workspace(&self, cx: &ViewContext<Self>) {
3205 fn serialize_pane_handle(
3206 pane_handle: &ViewHandle<Pane>,
3207 cx: &AppContext,
3208 ) -> SerializedPane {
3209 let (items, active) = {
3210 let pane = pane_handle.read(cx);
3211 let active_item_id = pane.active_item().map(|item| item.id());
3212 (
3213 pane.items()
3214 .filter_map(|item_handle| {
3215 Some(SerializedItem {
3216 kind: Arc::from(item_handle.serialized_item_kind()?),
3217 item_id: item_handle.id(),
3218 active: Some(item_handle.id()) == active_item_id,
3219 })
3220 })
3221 .collect::<Vec<_>>(),
3222 pane.has_focus(),
3223 )
3224 };
3225
3226 SerializedPane::new(items, active)
3227 }
3228
3229 fn build_serialized_pane_group(
3230 pane_group: &Member,
3231 cx: &AppContext,
3232 ) -> SerializedPaneGroup {
3233 match pane_group {
3234 Member::Axis(PaneAxis {
3235 axis,
3236 members,
3237 flexes,
3238 bounding_boxes: _,
3239 }) => SerializedPaneGroup::Group {
3240 axis: *axis,
3241 children: members
3242 .iter()
3243 .map(|member| build_serialized_pane_group(member, cx))
3244 .collect::<Vec<_>>(),
3245 flexes: Some(flexes.borrow().clone()),
3246 },
3247 Member::Pane(pane_handle) => {
3248 SerializedPaneGroup::Pane(serialize_pane_handle(&pane_handle, cx))
3249 }
3250 }
3251 }
3252
3253 fn build_serialized_docks(this: &Workspace, cx: &ViewContext<Workspace>) -> DockStructure {
3254 let left_dock = this.left_dock.read(cx);
3255 let left_visible = left_dock.is_open();
3256 let left_active_panel = left_dock.visible_panel().and_then(|panel| {
3257 Some(
3258 cx.view_ui_name(panel.as_any().window(), panel.id())?
3259 .to_string(),
3260 )
3261 });
3262 let left_dock_zoom = left_dock
3263 .visible_panel()
3264 .map(|panel| panel.is_zoomed(cx))
3265 .unwrap_or(false);
3266
3267 let right_dock = this.right_dock.read(cx);
3268 let right_visible = right_dock.is_open();
3269 let right_active_panel = right_dock.visible_panel().and_then(|panel| {
3270 Some(
3271 cx.view_ui_name(panel.as_any().window(), panel.id())?
3272 .to_string(),
3273 )
3274 });
3275 let right_dock_zoom = right_dock
3276 .visible_panel()
3277 .map(|panel| panel.is_zoomed(cx))
3278 .unwrap_or(false);
3279
3280 let bottom_dock = this.bottom_dock.read(cx);
3281 let bottom_visible = bottom_dock.is_open();
3282 let bottom_active_panel = bottom_dock.visible_panel().and_then(|panel| {
3283 Some(
3284 cx.view_ui_name(panel.as_any().window(), panel.id())?
3285 .to_string(),
3286 )
3287 });
3288 let bottom_dock_zoom = bottom_dock
3289 .visible_panel()
3290 .map(|panel| panel.is_zoomed(cx))
3291 .unwrap_or(false);
3292
3293 DockStructure {
3294 left: DockData {
3295 visible: left_visible,
3296 active_panel: left_active_panel,
3297 zoom: left_dock_zoom,
3298 },
3299 right: DockData {
3300 visible: right_visible,
3301 active_panel: right_active_panel,
3302 zoom: right_dock_zoom,
3303 },
3304 bottom: DockData {
3305 visible: bottom_visible,
3306 active_panel: bottom_active_panel,
3307 zoom: bottom_dock_zoom,
3308 },
3309 }
3310 }
3311
3312 if let Some(location) = self.location(cx) {
3313 // Load bearing special case:
3314 // - with_local_workspace() relies on this to not have other stuff open
3315 // when you open your log
3316 if !location.paths().is_empty() {
3317 let center_group = build_serialized_pane_group(&self.center.root, cx);
3318 let docks = build_serialized_docks(self, cx);
3319
3320 let serialized_workspace = SerializedWorkspace {
3321 id: self.database_id,
3322 location,
3323 center_group,
3324 bounds: Default::default(),
3325 display: Default::default(),
3326 docks,
3327 };
3328
3329 cx.background()
3330 .spawn(persistence::DB.save_workspace(serialized_workspace))
3331 .detach();
3332 }
3333 }
3334 }
3335
3336 pub(crate) fn load_workspace(
3337 workspace: WeakViewHandle<Workspace>,
3338 serialized_workspace: SerializedWorkspace,
3339 paths_to_open: Vec<Option<ProjectPath>>,
3340 cx: &mut AppContext,
3341 ) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
3342 cx.spawn(|mut cx| async move {
3343 let result = async_iife! {{
3344 let (project, old_center_pane) =
3345 workspace.read_with(&cx, |workspace, _| {
3346 (
3347 workspace.project().clone(),
3348 workspace.last_active_center_pane.clone(),
3349 )
3350 })?;
3351
3352 let mut center_items = None;
3353 let mut center_group = None;
3354 // Traverse the splits tree and add to things
3355 if let Some((group, active_pane, items)) = serialized_workspace
3356 .center_group
3357 .deserialize(&project, serialized_workspace.id, &workspace, &mut cx)
3358 .await {
3359 center_items = Some(items);
3360 center_group = Some((group, active_pane))
3361 }
3362
3363 let resulting_list = cx.read(|cx| {
3364 let mut opened_items = center_items
3365 .unwrap_or_default()
3366 .into_iter()
3367 .filter_map(|item| {
3368 let item = item?;
3369 let project_path = item.project_path(cx)?;
3370 Some((project_path, item))
3371 })
3372 .collect::<HashMap<_, _>>();
3373
3374 paths_to_open
3375 .into_iter()
3376 .map(|path_to_open| {
3377 path_to_open.map(|path_to_open| {
3378 Ok(opened_items.remove(&path_to_open))
3379 })
3380 .transpose()
3381 .map(|item| item.flatten())
3382 .transpose()
3383 })
3384 .collect::<Vec<_>>()
3385 });
3386
3387 // Remove old panes from workspace panes list
3388 workspace.update(&mut cx, |workspace, cx| {
3389 if let Some((center_group, active_pane)) = center_group {
3390 workspace.remove_panes(workspace.center.root.clone(), cx);
3391
3392 // Swap workspace center group
3393 workspace.center = PaneGroup::with_root(center_group);
3394
3395 // Change the focus to the workspace first so that we retrigger focus in on the pane.
3396 cx.focus_self();
3397
3398 if let Some(active_pane) = active_pane {
3399 cx.focus(&active_pane);
3400 } else {
3401 cx.focus(workspace.panes.last().unwrap());
3402 }
3403 } else {
3404 let old_center_handle = old_center_pane.and_then(|weak| weak.upgrade(cx));
3405 if let Some(old_center_handle) = old_center_handle {
3406 cx.focus(&old_center_handle)
3407 } else {
3408 cx.focus_self()
3409 }
3410 }
3411
3412 let docks = serialized_workspace.docks;
3413 workspace.left_dock.update(cx, |dock, cx| {
3414 dock.set_open(docks.left.visible, cx);
3415 if let Some(active_panel) = docks.left.active_panel {
3416 if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
3417 dock.activate_panel(ix, cx);
3418 }
3419 }
3420 dock.active_panel()
3421 .map(|panel| {
3422 panel.set_zoomed(docks.left.zoom, cx)
3423 });
3424 if docks.left.visible && docks.left.zoom {
3425 cx.focus_self()
3426 }
3427 });
3428 // TODO: I think the bug is that setting zoom or active undoes the bottom zoom or something
3429 workspace.right_dock.update(cx, |dock, cx| {
3430 dock.set_open(docks.right.visible, cx);
3431 if let Some(active_panel) = docks.right.active_panel {
3432 if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
3433 dock.activate_panel(ix, cx);
3434
3435 }
3436 }
3437 dock.active_panel()
3438 .map(|panel| {
3439 panel.set_zoomed(docks.right.zoom, cx)
3440 });
3441
3442 if docks.right.visible && docks.right.zoom {
3443 cx.focus_self()
3444 }
3445 });
3446 workspace.bottom_dock.update(cx, |dock, cx| {
3447 dock.set_open(docks.bottom.visible, cx);
3448 if let Some(active_panel) = docks.bottom.active_panel {
3449 if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
3450 dock.activate_panel(ix, cx);
3451 }
3452 }
3453
3454 dock.active_panel()
3455 .map(|panel| {
3456 panel.set_zoomed(docks.bottom.zoom, cx)
3457 });
3458
3459 if docks.bottom.visible && docks.bottom.zoom {
3460 cx.focus_self()
3461 }
3462 });
3463
3464
3465 cx.notify();
3466 })?;
3467
3468 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
3469 workspace.read_with(&cx, |workspace, cx| workspace.serialize_workspace(cx))?;
3470
3471 Ok::<_, anyhow::Error>(resulting_list)
3472 }};
3473
3474 result.await.unwrap_or_default()
3475 })
3476 }
3477
3478 #[cfg(any(test, feature = "test-support"))]
3479 pub fn test_new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
3480 let client = project.read(cx).client();
3481 let user_store = project.read(cx).user_store();
3482
3483 let channel_store =
3484 cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
3485 let app_state = Arc::new(AppState {
3486 languages: project.read(cx).languages().clone(),
3487 client,
3488 user_store,
3489 channel_store,
3490 fs: project.read(cx).fs().clone(),
3491 build_window_options: |_, _, _| Default::default(),
3492 initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
3493 background_actions: || &[],
3494 });
3495 Self::new(0, project, app_state, cx)
3496 }
3497
3498 fn render_dock(&self, position: DockPosition, cx: &WindowContext) -> Option<AnyElement<Self>> {
3499 let dock = match position {
3500 DockPosition::Left => &self.left_dock,
3501 DockPosition::Right => &self.right_dock,
3502 DockPosition::Bottom => &self.bottom_dock,
3503 };
3504 let active_panel = dock.read(cx).visible_panel()?;
3505 let element = if Some(active_panel.id()) == self.zoomed.as_ref().map(|zoomed| zoomed.id()) {
3506 dock.read(cx).render_placeholder(cx)
3507 } else {
3508 ChildView::new(dock, cx).into_any()
3509 };
3510
3511 Some(
3512 element
3513 .constrained()
3514 .dynamically(move |constraint, _, cx| match position {
3515 DockPosition::Left | DockPosition::Right => SizeConstraint::new(
3516 Vector2F::new(20., constraint.min.y()),
3517 Vector2F::new(cx.window_size().x() * 0.8, constraint.max.y()),
3518 ),
3519 DockPosition::Bottom => SizeConstraint::new(
3520 Vector2F::new(constraint.min.x(), 20.),
3521 Vector2F::new(constraint.max.x(), cx.window_size().y() * 0.8),
3522 ),
3523 })
3524 .into_any(),
3525 )
3526 }
3527}
3528
3529fn window_bounds_env_override(cx: &AsyncAppContext) -> Option<WindowBounds> {
3530 ZED_WINDOW_POSITION
3531 .zip(*ZED_WINDOW_SIZE)
3532 .map(|(position, size)| {
3533 WindowBounds::Fixed(RectF::new(
3534 cx.platform().screens()[0].bounds().origin() + position,
3535 size,
3536 ))
3537 })
3538}
3539
3540async fn open_items(
3541 serialized_workspace: Option<SerializedWorkspace>,
3542 workspace: &WeakViewHandle<Workspace>,
3543 mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
3544 app_state: Arc<AppState>,
3545 mut cx: AsyncAppContext,
3546) -> Vec<Option<anyhow::Result<Box<dyn ItemHandle>>>> {
3547 let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
3548
3549 if let Some(serialized_workspace) = serialized_workspace {
3550 let workspace = workspace.clone();
3551 let restored_items = cx
3552 .update(|cx| {
3553 Workspace::load_workspace(
3554 workspace,
3555 serialized_workspace,
3556 project_paths_to_open
3557 .iter()
3558 .map(|(_, project_path)| project_path)
3559 .cloned()
3560 .collect(),
3561 cx,
3562 )
3563 })
3564 .await;
3565
3566 let restored_project_paths = cx.read(|cx| {
3567 restored_items
3568 .iter()
3569 .filter_map(|item| item.as_ref()?.as_ref().ok()?.project_path(cx))
3570 .collect::<HashSet<_>>()
3571 });
3572
3573 opened_items = restored_items;
3574 project_paths_to_open
3575 .iter_mut()
3576 .for_each(|(_, project_path)| {
3577 if let Some(project_path_to_open) = project_path {
3578 if restored_project_paths.contains(project_path_to_open) {
3579 *project_path = None;
3580 }
3581 }
3582 });
3583 } else {
3584 for _ in 0..project_paths_to_open.len() {
3585 opened_items.push(None);
3586 }
3587 }
3588 assert!(opened_items.len() == project_paths_to_open.len());
3589
3590 let tasks =
3591 project_paths_to_open
3592 .into_iter()
3593 .enumerate()
3594 .map(|(i, (abs_path, project_path))| {
3595 let workspace = workspace.clone();
3596 cx.spawn(|mut cx| {
3597 let fs = app_state.fs.clone();
3598 async move {
3599 let file_project_path = project_path?;
3600 if fs.is_file(&abs_path).await {
3601 Some((
3602 i,
3603 workspace
3604 .update(&mut cx, |workspace, cx| {
3605 workspace.open_path(file_project_path, None, true, cx)
3606 })
3607 .log_err()?
3608 .await,
3609 ))
3610 } else {
3611 None
3612 }
3613 }
3614 })
3615 });
3616
3617 for maybe_opened_path in futures::future::join_all(tasks.into_iter())
3618 .await
3619 .into_iter()
3620 {
3621 if let Some((i, path_open_result)) = maybe_opened_path {
3622 opened_items[i] = Some(path_open_result);
3623 }
3624 }
3625
3626 opened_items
3627}
3628
3629fn notify_of_new_dock(workspace: &WeakViewHandle<Workspace>, cx: &mut AsyncAppContext) {
3630 const NEW_PANEL_BLOG_POST: &str = "https://zed.dev/blog/new-panel-system";
3631 const NEW_DOCK_HINT_KEY: &str = "show_new_dock_key";
3632 const MESSAGE_ID: usize = 2;
3633
3634 if workspace
3635 .read_with(cx, |workspace, cx| {
3636 workspace.has_shown_notification_once::<MessageNotification>(MESSAGE_ID, cx)
3637 })
3638 .unwrap_or(false)
3639 {
3640 return;
3641 }
3642
3643 if db::kvp::KEY_VALUE_STORE
3644 .read_kvp(NEW_DOCK_HINT_KEY)
3645 .ok()
3646 .flatten()
3647 .is_some()
3648 {
3649 if !workspace
3650 .read_with(cx, |workspace, cx| {
3651 workspace.has_shown_notification_once::<MessageNotification>(MESSAGE_ID, cx)
3652 })
3653 .unwrap_or(false)
3654 {
3655 cx.update(|cx| {
3656 cx.update_global::<NotificationTracker, _, _>(|tracker, _| {
3657 let entry = tracker
3658 .entry(TypeId::of::<MessageNotification>())
3659 .or_default();
3660 if !entry.contains(&MESSAGE_ID) {
3661 entry.push(MESSAGE_ID);
3662 }
3663 });
3664 });
3665 }
3666
3667 return;
3668 }
3669
3670 cx.spawn(|_| async move {
3671 db::kvp::KEY_VALUE_STORE
3672 .write_kvp(NEW_DOCK_HINT_KEY.to_string(), "seen".to_string())
3673 .await
3674 .ok();
3675 })
3676 .detach();
3677
3678 workspace
3679 .update(cx, |workspace, cx| {
3680 workspace.show_notification_once(2, cx, |cx| {
3681 cx.add_view(|_| {
3682 MessageNotification::new_element(|text, _| {
3683 Text::new(
3684 "Looking for the dock? Try ctrl-`!\nshift-escape now zooms your pane.",
3685 text,
3686 )
3687 .with_custom_runs(vec![26..32, 34..46], |_, bounds, cx| {
3688 let code_span_background_color = settings::get::<ThemeSettings>(cx)
3689 .theme
3690 .editor
3691 .document_highlight_read_background;
3692
3693 cx.scene().push_quad(gpui::Quad {
3694 bounds,
3695 background: Some(code_span_background_color),
3696 border: Default::default(),
3697 corner_radii: (2.0).into(),
3698 })
3699 })
3700 .into_any()
3701 })
3702 .with_click_message("Read more about the new panel system")
3703 .on_click(|cx| cx.platform().open_url(NEW_PANEL_BLOG_POST))
3704 })
3705 })
3706 })
3707 .ok();
3708}
3709
3710fn notify_if_database_failed(workspace: &WeakViewHandle<Workspace>, cx: &mut AsyncAppContext) {
3711 const REPORT_ISSUE_URL: &str ="https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";
3712
3713 workspace
3714 .update(cx, |workspace, cx| {
3715 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
3716 workspace.show_notification_once(0, cx, |cx| {
3717 cx.add_view(|_| {
3718 MessageNotification::new("Failed to load the database file.")
3719 .with_click_message("Click to let us know about this error")
3720 .on_click(|cx| cx.platform().open_url(REPORT_ISSUE_URL))
3721 })
3722 });
3723 }
3724 })
3725 .log_err();
3726}
3727
3728impl Entity for Workspace {
3729 type Event = Event;
3730}
3731
3732impl View for Workspace {
3733 fn ui_name() -> &'static str {
3734 "Workspace"
3735 }
3736
3737 fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
3738 let theme = theme::current(cx).clone();
3739 Stack::new()
3740 .with_child(
3741 Flex::column()
3742 .with_child(self.render_titlebar(&theme, cx))
3743 .with_child(
3744 Stack::new()
3745 .with_child({
3746 let project = self.project.clone();
3747 Flex::row()
3748 .with_children(self.render_dock(DockPosition::Left, cx))
3749 .with_child(
3750 Flex::column()
3751 .with_child(
3752 FlexItem::new(
3753 self.center.render(
3754 &project,
3755 &theme,
3756 &self.follower_states_by_leader,
3757 self.active_call(),
3758 self.active_pane(),
3759 self.zoomed
3760 .as_ref()
3761 .and_then(|zoomed| zoomed.upgrade(cx))
3762 .as_ref(),
3763 &self.app_state,
3764 cx,
3765 ),
3766 )
3767 .flex(1., true),
3768 )
3769 .with_children(
3770 self.render_dock(DockPosition::Bottom, cx),
3771 )
3772 .flex(1., true),
3773 )
3774 .with_children(self.render_dock(DockPosition::Right, cx))
3775 })
3776 .with_child(Overlay::new(
3777 Stack::new()
3778 .with_children(self.zoomed.as_ref().and_then(|zoomed| {
3779 enum ZoomBackground {}
3780 let zoomed = zoomed.upgrade(cx)?;
3781
3782 let mut foreground_style =
3783 theme.workspace.zoomed_pane_foreground;
3784 if let Some(zoomed_dock_position) = self.zoomed_position {
3785 foreground_style =
3786 theme.workspace.zoomed_panel_foreground;
3787 let margin = foreground_style.margin.top;
3788 let border = foreground_style.border.top;
3789
3790 // Only include a margin and border on the opposite side.
3791 foreground_style.margin.top = 0.;
3792 foreground_style.margin.left = 0.;
3793 foreground_style.margin.bottom = 0.;
3794 foreground_style.margin.right = 0.;
3795 foreground_style.border.top = false;
3796 foreground_style.border.left = false;
3797 foreground_style.border.bottom = false;
3798 foreground_style.border.right = false;
3799 match zoomed_dock_position {
3800 DockPosition::Left => {
3801 foreground_style.margin.right = margin;
3802 foreground_style.border.right = border;
3803 }
3804 DockPosition::Right => {
3805 foreground_style.margin.left = margin;
3806 foreground_style.border.left = border;
3807 }
3808 DockPosition::Bottom => {
3809 foreground_style.margin.top = margin;
3810 foreground_style.border.top = border;
3811 }
3812 }
3813 }
3814
3815 Some(
3816 ChildView::new(&zoomed, cx)
3817 .contained()
3818 .with_style(foreground_style)
3819 .aligned()
3820 .contained()
3821 .with_style(theme.workspace.zoomed_background)
3822 .mouse::<ZoomBackground>(0)
3823 .capture_all()
3824 .on_down(
3825 MouseButton::Left,
3826 |_, this: &mut Self, cx| {
3827 this.zoom_out(cx);
3828 },
3829 ),
3830 )
3831 }))
3832 .with_children(self.modal.as_ref().map(|modal| {
3833 // Prevent clicks within the modal from falling
3834 // through to the rest of the workspace.
3835 enum ModalBackground {}
3836 MouseEventHandler::new::<ModalBackground, _>(
3837 0,
3838 cx,
3839 |_, cx| ChildView::new(modal.view.as_any(), cx),
3840 )
3841 .on_click(MouseButton::Left, |_, _, _| {})
3842 .contained()
3843 .with_style(theme.workspace.modal)
3844 .aligned()
3845 .top()
3846 }))
3847 .with_children(self.render_notifications(&theme.workspace, cx)),
3848 ))
3849 .provide_resize_bounds::<WorkspaceBounds>()
3850 .flex(1.0, true),
3851 )
3852 .with_child(ChildView::new(&self.status_bar, cx))
3853 .contained()
3854 .with_background_color(theme.workspace.background),
3855 )
3856 .with_children(DragAndDrop::render(cx))
3857 .with_children(self.render_disconnected_overlay(cx))
3858 .into_any_named("workspace")
3859 }
3860
3861 fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
3862 if cx.is_self_focused() {
3863 cx.focus(&self.active_pane);
3864 }
3865 }
3866
3867 fn modifiers_changed(&mut self, e: &ModifiersChangedEvent, cx: &mut ViewContext<Self>) -> bool {
3868 DragAndDrop::<Workspace>::update_modifiers(e.modifiers, cx)
3869 }
3870}
3871
3872impl ViewId {
3873 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
3874 Ok(Self {
3875 creator: message
3876 .creator
3877 .ok_or_else(|| anyhow!("creator is missing"))?,
3878 id: message.id,
3879 })
3880 }
3881
3882 pub(crate) fn to_proto(&self) -> proto::ViewId {
3883 proto::ViewId {
3884 creator: Some(self.creator),
3885 id: self.id,
3886 }
3887 }
3888}
3889
3890pub trait WorkspaceHandle {
3891 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
3892}
3893
3894impl WorkspaceHandle for ViewHandle<Workspace> {
3895 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
3896 self.read(cx)
3897 .worktrees(cx)
3898 .flat_map(|worktree| {
3899 let worktree_id = worktree.read(cx).id();
3900 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
3901 worktree_id,
3902 path: f.path.clone(),
3903 })
3904 })
3905 .collect::<Vec<_>>()
3906 }
3907}
3908
3909impl std::fmt::Debug for OpenPaths {
3910 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3911 f.debug_struct("OpenPaths")
3912 .field("paths", &self.paths)
3913 .finish()
3914 }
3915}
3916
3917pub struct WorkspaceCreated(pub WeakViewHandle<Workspace>);
3918
3919pub fn activate_workspace_for_project(
3920 cx: &mut AsyncAppContext,
3921 predicate: impl Fn(&mut Project, &mut ModelContext<Project>) -> bool,
3922) -> Option<WeakViewHandle<Workspace>> {
3923 for window in cx.windows() {
3924 let handle = window
3925 .update(cx, |cx| {
3926 if let Some(workspace_handle) = cx.root_view().clone().downcast::<Workspace>() {
3927 let project = workspace_handle.read(cx).project.clone();
3928 if project.update(cx, &predicate) {
3929 cx.activate_window();
3930 return Some(workspace_handle.clone());
3931 }
3932 }
3933 None
3934 })
3935 .flatten();
3936
3937 if let Some(handle) = handle {
3938 return Some(handle.downgrade());
3939 }
3940 }
3941 None
3942}
3943
3944pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
3945 DB.last_workspace().await.log_err().flatten()
3946}
3947
3948#[allow(clippy::type_complexity)]
3949pub fn open_paths(
3950 abs_paths: &[PathBuf],
3951 app_state: &Arc<AppState>,
3952 requesting_window: Option<WindowHandle<Workspace>>,
3953 cx: &mut AppContext,
3954) -> Task<
3955 Result<(
3956 WeakViewHandle<Workspace>,
3957 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
3958 )>,
3959> {
3960 let app_state = app_state.clone();
3961 let abs_paths = abs_paths.to_vec();
3962 cx.spawn(|mut cx| async move {
3963 // Open paths in existing workspace if possible
3964 let existing = activate_workspace_for_project(&mut cx, |project, cx| {
3965 project.contains_paths(&abs_paths, cx)
3966 });
3967
3968 if let Some(existing) = existing {
3969 Ok((
3970 existing.clone(),
3971 existing
3972 .update(&mut cx, |workspace, cx| {
3973 workspace.open_paths(abs_paths, true, cx)
3974 })?
3975 .await,
3976 ))
3977 } else {
3978 Ok(cx
3979 .update(|cx| {
3980 Workspace::new_local(abs_paths, app_state.clone(), requesting_window, cx)
3981 })
3982 .await)
3983 }
3984 })
3985}
3986
3987pub fn open_new(
3988 app_state: &Arc<AppState>,
3989 cx: &mut AppContext,
3990 init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static,
3991) -> Task<()> {
3992 let task = Workspace::new_local(Vec::new(), app_state.clone(), None, cx);
3993 cx.spawn(|mut cx| async move {
3994 let (workspace, opened_paths) = task.await;
3995
3996 workspace
3997 .update(&mut cx, |workspace, cx| {
3998 if opened_paths.is_empty() {
3999 init(workspace, cx)
4000 }
4001 })
4002 .log_err();
4003 })
4004}
4005
4006pub fn create_and_open_local_file(
4007 path: &'static Path,
4008 cx: &mut ViewContext<Workspace>,
4009 default_content: impl 'static + Send + FnOnce() -> Rope,
4010) -> Task<Result<Box<dyn ItemHandle>>> {
4011 cx.spawn(|workspace, mut cx| async move {
4012 let fs = workspace.read_with(&cx, |workspace, _| workspace.app_state().fs.clone())?;
4013 if !fs.is_file(path).await {
4014 fs.create_file(path, Default::default()).await?;
4015 fs.save(path, &default_content(), Default::default())
4016 .await?;
4017 }
4018
4019 let mut items = workspace
4020 .update(&mut cx, |workspace, cx| {
4021 workspace.with_local_workspace(cx, |workspace, cx| {
4022 workspace.open_paths(vec![path.to_path_buf()], false, cx)
4023 })
4024 })?
4025 .await?
4026 .await;
4027
4028 let item = items.pop().flatten();
4029 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
4030 })
4031}
4032
4033pub fn join_remote_project(
4034 project_id: u64,
4035 follow_user_id: u64,
4036 app_state: Arc<AppState>,
4037 cx: &mut AppContext,
4038) -> Task<Result<()>> {
4039 cx.spawn(|mut cx| async move {
4040 let existing_workspace = cx
4041 .windows()
4042 .into_iter()
4043 .find_map(|window| {
4044 window.downcast::<Workspace>().and_then(|window| {
4045 window.read_root_with(&cx, |workspace, cx| {
4046 if workspace.project().read(cx).remote_id() == Some(project_id) {
4047 Some(cx.handle().downgrade())
4048 } else {
4049 None
4050 }
4051 })
4052 })
4053 })
4054 .flatten();
4055
4056 let workspace = if let Some(existing_workspace) = existing_workspace {
4057 existing_workspace
4058 } else {
4059 let active_call = cx.read(ActiveCall::global);
4060 let room = active_call
4061 .read_with(&cx, |call, _| call.room().cloned())
4062 .ok_or_else(|| anyhow!("not in a call"))?;
4063 let project = room
4064 .update(&mut cx, |room, cx| {
4065 room.join_project(
4066 project_id,
4067 app_state.languages.clone(),
4068 app_state.fs.clone(),
4069 cx,
4070 )
4071 })
4072 .await?;
4073
4074 let window_bounds_override = window_bounds_env_override(&cx);
4075 let window = cx.add_window(
4076 (app_state.build_window_options)(
4077 window_bounds_override,
4078 None,
4079 cx.platform().as_ref(),
4080 ),
4081 |cx| Workspace::new(0, project, app_state.clone(), cx),
4082 );
4083 let workspace = window.root(&cx).unwrap();
4084 (app_state.initialize_workspace)(
4085 workspace.downgrade(),
4086 false,
4087 app_state.clone(),
4088 cx.clone(),
4089 )
4090 .await
4091 .log_err();
4092
4093 workspace.downgrade()
4094 };
4095
4096 workspace.window().activate(&mut cx);
4097 cx.platform().activate(true);
4098
4099 workspace.update(&mut cx, |workspace, cx| {
4100 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
4101 let follow_peer_id = room
4102 .read(cx)
4103 .remote_participants()
4104 .iter()
4105 .find(|(_, participant)| participant.user.id == follow_user_id)
4106 .map(|(_, p)| p.peer_id)
4107 .or_else(|| {
4108 // If we couldn't follow the given user, follow the host instead.
4109 let collaborator = workspace
4110 .project()
4111 .read(cx)
4112 .collaborators()
4113 .values()
4114 .find(|collaborator| collaborator.replica_id == 0)?;
4115 Some(collaborator.peer_id)
4116 });
4117
4118 if let Some(follow_peer_id) = follow_peer_id {
4119 if !workspace.is_being_followed(follow_peer_id) {
4120 workspace
4121 .toggle_follow(follow_peer_id, cx)
4122 .map(|follow| follow.detach_and_log_err(cx));
4123 }
4124 }
4125 }
4126 })?;
4127
4128 anyhow::Ok(())
4129 })
4130}
4131
4132pub fn restart(_: &Restart, cx: &mut AppContext) {
4133 let should_confirm = settings::get::<WorkspaceSettings>(cx).confirm_quit;
4134 cx.spawn(|mut cx| async move {
4135 let mut workspace_windows = cx
4136 .windows()
4137 .into_iter()
4138 .filter_map(|window| window.downcast::<Workspace>())
4139 .collect::<Vec<_>>();
4140
4141 // If multiple windows have unsaved changes, and need a save prompt,
4142 // prompt in the active window before switching to a different window.
4143 workspace_windows.sort_by_key(|window| window.is_active(&cx) == Some(false));
4144
4145 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
4146 let answer = window.prompt(
4147 PromptLevel::Info,
4148 "Are you sure you want to restart?",
4149 &["Restart", "Cancel"],
4150 &mut cx,
4151 );
4152
4153 if let Some(mut answer) = answer {
4154 let answer = answer.next().await;
4155 if answer != Some(0) {
4156 return Ok(());
4157 }
4158 }
4159 }
4160
4161 // If the user cancels any save prompt, then keep the app open.
4162 for window in workspace_windows {
4163 if let Some(should_close) = window.update_root(&mut cx, |workspace, cx| {
4164 workspace.prepare_to_close(true, cx)
4165 }) {
4166 if !should_close.await? {
4167 return Ok(());
4168 }
4169 }
4170 }
4171 cx.platform().restart();
4172 anyhow::Ok(())
4173 })
4174 .detach_and_log_err(cx);
4175}
4176
4177fn parse_pixel_position_env_var(value: &str) -> Option<Vector2F> {
4178 let mut parts = value.split(',');
4179 let width: usize = parts.next()?.parse().ok()?;
4180 let height: usize = parts.next()?.parse().ok()?;
4181 Some(vec2f(width as f32, height as f32))
4182}
4183
4184#[cfg(test)]
4185mod tests {
4186 use super::*;
4187 use crate::{
4188 dock::test::{TestPanel, TestPanelEvent},
4189 item::test::{TestItem, TestItemEvent, TestProjectItem},
4190 };
4191 use fs::FakeFs;
4192 use gpui::{executor::Deterministic, test::EmptyView, TestAppContext};
4193 use project::{Project, ProjectEntryId};
4194 use serde_json::json;
4195 use settings::SettingsStore;
4196 use std::{cell::RefCell, rc::Rc};
4197
4198 #[gpui::test]
4199 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
4200 init_test(cx);
4201
4202 let fs = FakeFs::new(cx.background());
4203 let project = Project::test(fs, [], cx).await;
4204 let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
4205 let workspace = window.root(cx);
4206
4207 // Adding an item with no ambiguity renders the tab without detail.
4208 let item1 = window.add_view(cx, |_| {
4209 let mut item = TestItem::new();
4210 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
4211 item
4212 });
4213 workspace.update(cx, |workspace, cx| {
4214 workspace.add_item(Box::new(item1.clone()), cx);
4215 });
4216 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), None));
4217
4218 // Adding an item that creates ambiguity increases the level of detail on
4219 // both tabs.
4220 let item2 = window.add_view(cx, |_| {
4221 let mut item = TestItem::new();
4222 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
4223 item
4224 });
4225 workspace.update(cx, |workspace, cx| {
4226 workspace.add_item(Box::new(item2.clone()), cx);
4227 });
4228 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4229 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4230
4231 // Adding an item that creates ambiguity increases the level of detail only
4232 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
4233 // we stop at the highest detail available.
4234 let item3 = window.add_view(cx, |_| {
4235 let mut item = TestItem::new();
4236 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
4237 item
4238 });
4239 workspace.update(cx, |workspace, cx| {
4240 workspace.add_item(Box::new(item3.clone()), cx);
4241 });
4242 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4243 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
4244 item3.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
4245 }
4246
4247 #[gpui::test]
4248 async fn test_tracking_active_path(cx: &mut TestAppContext) {
4249 init_test(cx);
4250
4251 let fs = FakeFs::new(cx.background());
4252 fs.insert_tree(
4253 "/root1",
4254 json!({
4255 "one.txt": "",
4256 "two.txt": "",
4257 }),
4258 )
4259 .await;
4260 fs.insert_tree(
4261 "/root2",
4262 json!({
4263 "three.txt": "",
4264 }),
4265 )
4266 .await;
4267
4268 let project = Project::test(fs, ["root1".as_ref()], cx).await;
4269 let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
4270 let workspace = window.root(cx);
4271 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4272 let worktree_id = project.read_with(cx, |project, cx| {
4273 project.worktrees(cx).next().unwrap().read(cx).id()
4274 });
4275
4276 let item1 = window.add_view(cx, |cx| {
4277 TestItem::new().with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
4278 });
4279 let item2 = window.add_view(cx, |cx| {
4280 TestItem::new().with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
4281 });
4282
4283 // Add an item to an empty pane
4284 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx));
4285 project.read_with(cx, |project, cx| {
4286 assert_eq!(
4287 project.active_entry(),
4288 project
4289 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
4290 .map(|e| e.id)
4291 );
4292 });
4293 assert_eq!(window.current_title(cx).as_deref(), Some("one.txt β root1"));
4294
4295 // Add a second item to a non-empty pane
4296 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
4297 assert_eq!(window.current_title(cx).as_deref(), Some("two.txt β root1"));
4298 project.read_with(cx, |project, cx| {
4299 assert_eq!(
4300 project.active_entry(),
4301 project
4302 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
4303 .map(|e| e.id)
4304 );
4305 });
4306
4307 // Close the active item
4308 pane.update(cx, |pane, cx| {
4309 pane.close_active_item(&Default::default(), cx).unwrap()
4310 })
4311 .await
4312 .unwrap();
4313 assert_eq!(window.current_title(cx).as_deref(), Some("one.txt β root1"));
4314 project.read_with(cx, |project, cx| {
4315 assert_eq!(
4316 project.active_entry(),
4317 project
4318 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
4319 .map(|e| e.id)
4320 );
4321 });
4322
4323 // Add a project folder
4324 project
4325 .update(cx, |project, cx| {
4326 project.find_or_create_local_worktree("/root2", true, cx)
4327 })
4328 .await
4329 .unwrap();
4330 assert_eq!(
4331 window.current_title(cx).as_deref(),
4332 Some("one.txt β root1, root2")
4333 );
4334
4335 // Remove a project folder
4336 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
4337 assert_eq!(window.current_title(cx).as_deref(), Some("one.txt β root2"));
4338 }
4339
4340 #[gpui::test]
4341 async fn test_close_window(cx: &mut TestAppContext) {
4342 init_test(cx);
4343
4344 let fs = FakeFs::new(cx.background());
4345 fs.insert_tree("/root", json!({ "one": "" })).await;
4346
4347 let project = Project::test(fs, ["root".as_ref()], cx).await;
4348 let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
4349 let workspace = window.root(cx);
4350
4351 // When there are no dirty items, there's nothing to do.
4352 let item1 = window.add_view(cx, |_| TestItem::new());
4353 workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
4354 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
4355 assert!(task.await.unwrap());
4356
4357 // When there are dirty untitled items, prompt to save each one. If the user
4358 // cancels any prompt, then abort.
4359 let item2 = window.add_view(cx, |_| TestItem::new().with_dirty(true));
4360 let item3 = window.add_view(cx, |cx| {
4361 TestItem::new()
4362 .with_dirty(true)
4363 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4364 });
4365 workspace.update(cx, |w, cx| {
4366 w.add_item(Box::new(item2.clone()), cx);
4367 w.add_item(Box::new(item3.clone()), cx);
4368 });
4369 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
4370 cx.foreground().run_until_parked();
4371 window.simulate_prompt_answer(2, cx); // cancel
4372 cx.foreground().run_until_parked();
4373 assert!(!window.has_pending_prompt(cx));
4374 assert!(!task.await.unwrap());
4375 }
4376
4377 #[gpui::test]
4378 async fn test_close_pane_items(cx: &mut TestAppContext) {
4379 init_test(cx);
4380
4381 let fs = FakeFs::new(cx.background());
4382
4383 let project = Project::test(fs, None, cx).await;
4384 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
4385 let workspace = window.root(cx);
4386
4387 let item1 = window.add_view(cx, |cx| {
4388 TestItem::new()
4389 .with_dirty(true)
4390 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4391 });
4392 let item2 = window.add_view(cx, |cx| {
4393 TestItem::new()
4394 .with_dirty(true)
4395 .with_conflict(true)
4396 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
4397 });
4398 let item3 = window.add_view(cx, |cx| {
4399 TestItem::new()
4400 .with_dirty(true)
4401 .with_conflict(true)
4402 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
4403 });
4404 let item4 = window.add_view(cx, |cx| {
4405 TestItem::new()
4406 .with_dirty(true)
4407 .with_project_items(&[TestProjectItem::new_untitled(cx)])
4408 });
4409 let pane = workspace.update(cx, |workspace, cx| {
4410 workspace.add_item(Box::new(item1.clone()), cx);
4411 workspace.add_item(Box::new(item2.clone()), cx);
4412 workspace.add_item(Box::new(item3.clone()), cx);
4413 workspace.add_item(Box::new(item4.clone()), cx);
4414 workspace.active_pane().clone()
4415 });
4416
4417 let close_items = pane.update(cx, |pane, cx| {
4418 pane.activate_item(1, true, true, cx);
4419 assert_eq!(pane.active_item().unwrap().id(), item2.id());
4420 let item1_id = item1.id();
4421 let item3_id = item3.id();
4422 let item4_id = item4.id();
4423 pane.close_items(cx, SaveIntent::Close, move |id| {
4424 [item1_id, item3_id, item4_id].contains(&id)
4425 })
4426 });
4427 cx.foreground().run_until_parked();
4428
4429 // There's a prompt to save item 1.
4430 pane.read_with(cx, |pane, _| {
4431 assert_eq!(pane.items_len(), 4);
4432 assert_eq!(pane.active_item().unwrap().id(), item1.id());
4433 });
4434 assert!(window.has_pending_prompt(cx));
4435
4436 // Confirm saving item 1.
4437 window.simulate_prompt_answer(0, cx);
4438 cx.foreground().run_until_parked();
4439
4440 // Item 1 is saved. There's a prompt to save item 3.
4441 pane.read_with(cx, |pane, cx| {
4442 assert_eq!(item1.read(cx).save_count, 1);
4443 assert_eq!(item1.read(cx).save_as_count, 0);
4444 assert_eq!(item1.read(cx).reload_count, 0);
4445 assert_eq!(pane.items_len(), 3);
4446 assert_eq!(pane.active_item().unwrap().id(), item3.id());
4447 });
4448 assert!(window.has_pending_prompt(cx));
4449
4450 // Cancel saving item 3.
4451 window.simulate_prompt_answer(1, cx);
4452 cx.foreground().run_until_parked();
4453
4454 // Item 3 is reloaded. There's a prompt to save item 4.
4455 pane.read_with(cx, |pane, cx| {
4456 assert_eq!(item3.read(cx).save_count, 0);
4457 assert_eq!(item3.read(cx).save_as_count, 0);
4458 assert_eq!(item3.read(cx).reload_count, 1);
4459 assert_eq!(pane.items_len(), 2);
4460 assert_eq!(pane.active_item().unwrap().id(), item4.id());
4461 });
4462 assert!(window.has_pending_prompt(cx));
4463
4464 // Confirm saving item 4.
4465 window.simulate_prompt_answer(0, cx);
4466 cx.foreground().run_until_parked();
4467
4468 // There's a prompt for a path for item 4.
4469 cx.simulate_new_path_selection(|_| Some(Default::default()));
4470 close_items.await.unwrap();
4471
4472 // The requested items are closed.
4473 pane.read_with(cx, |pane, cx| {
4474 assert_eq!(item4.read(cx).save_count, 0);
4475 assert_eq!(item4.read(cx).save_as_count, 1);
4476 assert_eq!(item4.read(cx).reload_count, 0);
4477 assert_eq!(pane.items_len(), 1);
4478 assert_eq!(pane.active_item().unwrap().id(), item2.id());
4479 });
4480 }
4481
4482 #[gpui::test]
4483 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
4484 init_test(cx);
4485
4486 let fs = FakeFs::new(cx.background());
4487
4488 let project = Project::test(fs, [], cx).await;
4489 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
4490 let workspace = window.root(cx);
4491
4492 // Create several workspace items with single project entries, and two
4493 // workspace items with multiple project entries.
4494 let single_entry_items = (0..=4)
4495 .map(|project_entry_id| {
4496 window.add_view(cx, |cx| {
4497 TestItem::new()
4498 .with_dirty(true)
4499 .with_project_items(&[TestProjectItem::new(
4500 project_entry_id,
4501 &format!("{project_entry_id}.txt"),
4502 cx,
4503 )])
4504 })
4505 })
4506 .collect::<Vec<_>>();
4507 let item_2_3 = window.add_view(cx, |cx| {
4508 TestItem::new()
4509 .with_dirty(true)
4510 .with_singleton(false)
4511 .with_project_items(&[
4512 single_entry_items[2].read(cx).project_items[0].clone(),
4513 single_entry_items[3].read(cx).project_items[0].clone(),
4514 ])
4515 });
4516 let item_3_4 = window.add_view(cx, |cx| {
4517 TestItem::new()
4518 .with_dirty(true)
4519 .with_singleton(false)
4520 .with_project_items(&[
4521 single_entry_items[3].read(cx).project_items[0].clone(),
4522 single_entry_items[4].read(cx).project_items[0].clone(),
4523 ])
4524 });
4525
4526 // Create two panes that contain the following project entries:
4527 // left pane:
4528 // multi-entry items: (2, 3)
4529 // single-entry items: 0, 1, 2, 3, 4
4530 // right pane:
4531 // single-entry items: 1
4532 // multi-entry items: (3, 4)
4533 let left_pane = workspace.update(cx, |workspace, cx| {
4534 let left_pane = workspace.active_pane().clone();
4535 workspace.add_item(Box::new(item_2_3.clone()), cx);
4536 for item in single_entry_items {
4537 workspace.add_item(Box::new(item), cx);
4538 }
4539 left_pane.update(cx, |pane, cx| {
4540 pane.activate_item(2, true, true, cx);
4541 });
4542
4543 workspace
4544 .split_and_clone(left_pane.clone(), SplitDirection::Right, cx)
4545 .unwrap();
4546
4547 left_pane
4548 });
4549
4550 //Need to cause an effect flush in order to respect new focus
4551 workspace.update(cx, |workspace, cx| {
4552 workspace.add_item(Box::new(item_3_4.clone()), cx);
4553 cx.focus(&left_pane);
4554 });
4555
4556 // When closing all of the items in the left pane, we should be prompted twice:
4557 // once for project entry 0, and once for project entry 2. After those two
4558 // prompts, the task should complete.
4559
4560 let close = left_pane.update(cx, |pane, cx| {
4561 pane.close_items(cx, SaveIntent::Close, move |_| true)
4562 });
4563 cx.foreground().run_until_parked();
4564 left_pane.read_with(cx, |pane, cx| {
4565 assert_eq!(
4566 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
4567 &[ProjectEntryId::from_proto(0)]
4568 );
4569 });
4570 window.simulate_prompt_answer(0, cx);
4571
4572 cx.foreground().run_until_parked();
4573 left_pane.read_with(cx, |pane, cx| {
4574 assert_eq!(
4575 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
4576 &[ProjectEntryId::from_proto(2)]
4577 );
4578 });
4579 window.simulate_prompt_answer(0, cx);
4580
4581 cx.foreground().run_until_parked();
4582 close.await.unwrap();
4583 left_pane.read_with(cx, |pane, _| {
4584 assert_eq!(pane.items_len(), 0);
4585 });
4586 }
4587
4588 #[gpui::test]
4589 async fn test_autosave(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
4590 init_test(cx);
4591
4592 let fs = FakeFs::new(cx.background());
4593
4594 let project = Project::test(fs, [], cx).await;
4595 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
4596 let workspace = window.root(cx);
4597 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4598
4599 let item = window.add_view(cx, |cx| {
4600 TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4601 });
4602 let item_id = item.id();
4603 workspace.update(cx, |workspace, cx| {
4604 workspace.add_item(Box::new(item.clone()), cx);
4605 });
4606
4607 // Autosave on window change.
4608 item.update(cx, |item, cx| {
4609 cx.update_global(|settings: &mut SettingsStore, cx| {
4610 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4611 settings.autosave = Some(AutosaveSetting::OnWindowChange);
4612 })
4613 });
4614 item.is_dirty = true;
4615 });
4616
4617 // Deactivating the window saves the file.
4618 window.simulate_deactivation(cx);
4619 deterministic.run_until_parked();
4620 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
4621
4622 // Autosave on focus change.
4623 item.update(cx, |item, cx| {
4624 cx.focus_self();
4625 cx.update_global(|settings: &mut SettingsStore, cx| {
4626 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4627 settings.autosave = Some(AutosaveSetting::OnFocusChange);
4628 })
4629 });
4630 item.is_dirty = true;
4631 });
4632
4633 // Blurring the item saves the file.
4634 item.update(cx, |_, cx| cx.blur());
4635 deterministic.run_until_parked();
4636 item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
4637
4638 // Deactivating the window still saves the file.
4639 window.simulate_activation(cx);
4640 item.update(cx, |item, cx| {
4641 cx.focus_self();
4642 item.is_dirty = true;
4643 });
4644 window.simulate_deactivation(cx);
4645
4646 deterministic.run_until_parked();
4647 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
4648
4649 // Autosave after delay.
4650 item.update(cx, |item, cx| {
4651 cx.update_global(|settings: &mut SettingsStore, cx| {
4652 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4653 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
4654 })
4655 });
4656 item.is_dirty = true;
4657 cx.emit(TestItemEvent::Edit);
4658 });
4659
4660 // Delay hasn't fully expired, so the file is still dirty and unsaved.
4661 deterministic.advance_clock(Duration::from_millis(250));
4662 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
4663
4664 // After delay expires, the file is saved.
4665 deterministic.advance_clock(Duration::from_millis(250));
4666 item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
4667
4668 // Autosave on focus change, ensuring closing the tab counts as such.
4669 item.update(cx, |item, cx| {
4670 cx.update_global(|settings: &mut SettingsStore, cx| {
4671 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4672 settings.autosave = Some(AutosaveSetting::OnFocusChange);
4673 })
4674 });
4675 item.is_dirty = true;
4676 });
4677
4678 pane.update(cx, |pane, cx| {
4679 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
4680 })
4681 .await
4682 .unwrap();
4683 assert!(!window.has_pending_prompt(cx));
4684 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
4685
4686 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
4687 workspace.update(cx, |workspace, cx| {
4688 workspace.add_item(Box::new(item.clone()), cx);
4689 });
4690 item.update(cx, |item, cx| {
4691 item.project_items[0].update(cx, |item, _| {
4692 item.entry_id = None;
4693 });
4694 item.is_dirty = true;
4695 cx.blur();
4696 });
4697 deterministic.run_until_parked();
4698 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
4699
4700 // Ensure autosave is prevented for deleted files also when closing the buffer.
4701 let _close_items = pane.update(cx, |pane, cx| {
4702 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
4703 });
4704 deterministic.run_until_parked();
4705 assert!(window.has_pending_prompt(cx));
4706 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
4707 }
4708
4709 #[gpui::test]
4710 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
4711 init_test(cx);
4712
4713 let fs = FakeFs::new(cx.background());
4714
4715 let project = Project::test(fs, [], cx).await;
4716 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
4717 let workspace = window.root(cx);
4718
4719 let item = window.add_view(cx, |cx| {
4720 TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4721 });
4722 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4723 let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone());
4724 let toolbar_notify_count = Rc::new(RefCell::new(0));
4725
4726 workspace.update(cx, |workspace, cx| {
4727 workspace.add_item(Box::new(item.clone()), cx);
4728 let toolbar_notification_count = toolbar_notify_count.clone();
4729 cx.observe(&toolbar, move |_, _, _| {
4730 *toolbar_notification_count.borrow_mut() += 1
4731 })
4732 .detach();
4733 });
4734
4735 pane.read_with(cx, |pane, _| {
4736 assert!(!pane.can_navigate_backward());
4737 assert!(!pane.can_navigate_forward());
4738 });
4739
4740 item.update(cx, |item, cx| {
4741 item.set_state("one".to_string(), cx);
4742 });
4743
4744 // Toolbar must be notified to re-render the navigation buttons
4745 assert_eq!(*toolbar_notify_count.borrow(), 1);
4746
4747 pane.read_with(cx, |pane, _| {
4748 assert!(pane.can_navigate_backward());
4749 assert!(!pane.can_navigate_forward());
4750 });
4751
4752 workspace
4753 .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
4754 .await
4755 .unwrap();
4756
4757 assert_eq!(*toolbar_notify_count.borrow(), 3);
4758 pane.read_with(cx, |pane, _| {
4759 assert!(!pane.can_navigate_backward());
4760 assert!(pane.can_navigate_forward());
4761 });
4762 }
4763
4764 #[gpui::test]
4765 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
4766 init_test(cx);
4767 let fs = FakeFs::new(cx.background());
4768
4769 let project = Project::test(fs, [], cx).await;
4770 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
4771 let workspace = window.root(cx);
4772
4773 let panel = workspace.update(cx, |workspace, cx| {
4774 let panel = cx.add_view(|_| TestPanel::new(DockPosition::Right));
4775 workspace.add_panel(panel.clone(), cx);
4776
4777 workspace
4778 .right_dock()
4779 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
4780
4781 panel
4782 });
4783
4784 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4785 pane.update(cx, |pane, cx| {
4786 let item = cx.add_view(|_| TestItem::new());
4787 pane.add_item(Box::new(item), true, true, None, cx);
4788 });
4789
4790 // Transfer focus from center to panel
4791 workspace.update(cx, |workspace, cx| {
4792 workspace.toggle_panel_focus::<TestPanel>(cx);
4793 });
4794
4795 workspace.read_with(cx, |workspace, cx| {
4796 assert!(workspace.right_dock().read(cx).is_open());
4797 assert!(!panel.is_zoomed(cx));
4798 assert!(panel.has_focus(cx));
4799 });
4800
4801 // Transfer focus from panel to center
4802 workspace.update(cx, |workspace, cx| {
4803 workspace.toggle_panel_focus::<TestPanel>(cx);
4804 });
4805
4806 workspace.read_with(cx, |workspace, cx| {
4807 assert!(workspace.right_dock().read(cx).is_open());
4808 assert!(!panel.is_zoomed(cx));
4809 assert!(!panel.has_focus(cx));
4810 });
4811
4812 // Close the dock
4813 workspace.update(cx, |workspace, cx| {
4814 workspace.toggle_dock(DockPosition::Right, cx);
4815 });
4816
4817 workspace.read_with(cx, |workspace, cx| {
4818 assert!(!workspace.right_dock().read(cx).is_open());
4819 assert!(!panel.is_zoomed(cx));
4820 assert!(!panel.has_focus(cx));
4821 });
4822
4823 // Open the dock
4824 workspace.update(cx, |workspace, cx| {
4825 workspace.toggle_dock(DockPosition::Right, cx);
4826 });
4827
4828 workspace.read_with(cx, |workspace, cx| {
4829 assert!(workspace.right_dock().read(cx).is_open());
4830 assert!(!panel.is_zoomed(cx));
4831 assert!(panel.has_focus(cx));
4832 });
4833
4834 // Focus and zoom panel
4835 panel.update(cx, |panel, cx| {
4836 cx.focus_self();
4837 panel.set_zoomed(true, cx)
4838 });
4839
4840 workspace.read_with(cx, |workspace, cx| {
4841 assert!(workspace.right_dock().read(cx).is_open());
4842 assert!(panel.is_zoomed(cx));
4843 assert!(panel.has_focus(cx));
4844 });
4845
4846 // Transfer focus to the center closes the dock
4847 workspace.update(cx, |workspace, cx| {
4848 workspace.toggle_panel_focus::<TestPanel>(cx);
4849 });
4850
4851 workspace.read_with(cx, |workspace, cx| {
4852 assert!(!workspace.right_dock().read(cx).is_open());
4853 assert!(panel.is_zoomed(cx));
4854 assert!(!panel.has_focus(cx));
4855 });
4856
4857 // Transferring focus back to the panel keeps it zoomed
4858 workspace.update(cx, |workspace, cx| {
4859 workspace.toggle_panel_focus::<TestPanel>(cx);
4860 });
4861
4862 workspace.read_with(cx, |workspace, cx| {
4863 assert!(workspace.right_dock().read(cx).is_open());
4864 assert!(panel.is_zoomed(cx));
4865 assert!(panel.has_focus(cx));
4866 });
4867
4868 // Close the dock while it is zoomed
4869 workspace.update(cx, |workspace, cx| {
4870 workspace.toggle_dock(DockPosition::Right, cx)
4871 });
4872
4873 workspace.read_with(cx, |workspace, cx| {
4874 assert!(!workspace.right_dock().read(cx).is_open());
4875 assert!(panel.is_zoomed(cx));
4876 assert!(workspace.zoomed.is_none());
4877 assert!(!panel.has_focus(cx));
4878 });
4879
4880 // Opening the dock, when it's zoomed, retains focus
4881 workspace.update(cx, |workspace, cx| {
4882 workspace.toggle_dock(DockPosition::Right, cx)
4883 });
4884
4885 workspace.read_with(cx, |workspace, cx| {
4886 assert!(workspace.right_dock().read(cx).is_open());
4887 assert!(panel.is_zoomed(cx));
4888 assert!(workspace.zoomed.is_some());
4889 assert!(panel.has_focus(cx));
4890 });
4891
4892 // Unzoom and close the panel, zoom the active pane.
4893 panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
4894 workspace.update(cx, |workspace, cx| {
4895 workspace.toggle_dock(DockPosition::Right, cx)
4896 });
4897 pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
4898
4899 // Opening a dock unzooms the pane.
4900 workspace.update(cx, |workspace, cx| {
4901 workspace.toggle_dock(DockPosition::Right, cx)
4902 });
4903 workspace.read_with(cx, |workspace, cx| {
4904 let pane = pane.read(cx);
4905 assert!(!pane.is_zoomed());
4906 assert!(!pane.has_focus());
4907 assert!(workspace.right_dock().read(cx).is_open());
4908 assert!(workspace.zoomed.is_none());
4909 });
4910 }
4911
4912 #[gpui::test]
4913 async fn test_panels(cx: &mut gpui::TestAppContext) {
4914 init_test(cx);
4915 let fs = FakeFs::new(cx.background());
4916
4917 let project = Project::test(fs, [], cx).await;
4918 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
4919 let workspace = window.root(cx);
4920
4921 let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
4922 // Add panel_1 on the left, panel_2 on the right.
4923 let panel_1 = cx.add_view(|_| TestPanel::new(DockPosition::Left));
4924 workspace.add_panel(panel_1.clone(), cx);
4925 workspace
4926 .left_dock()
4927 .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
4928 let panel_2 = cx.add_view(|_| TestPanel::new(DockPosition::Right));
4929 workspace.add_panel(panel_2.clone(), cx);
4930 workspace
4931 .right_dock()
4932 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
4933
4934 let left_dock = workspace.left_dock();
4935 assert_eq!(
4936 left_dock.read(cx).visible_panel().unwrap().id(),
4937 panel_1.id()
4938 );
4939 assert_eq!(
4940 left_dock.read(cx).active_panel_size(cx).unwrap(),
4941 panel_1.size(cx)
4942 );
4943
4944 left_dock.update(cx, |left_dock, cx| {
4945 left_dock.resize_active_panel(Some(1337.), cx)
4946 });
4947 assert_eq!(
4948 workspace
4949 .right_dock()
4950 .read(cx)
4951 .visible_panel()
4952 .unwrap()
4953 .id(),
4954 panel_2.id()
4955 );
4956
4957 (panel_1, panel_2)
4958 });
4959
4960 // Move panel_1 to the right
4961 panel_1.update(cx, |panel_1, cx| {
4962 panel_1.set_position(DockPosition::Right, cx)
4963 });
4964
4965 workspace.update(cx, |workspace, cx| {
4966 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
4967 // Since it was the only panel on the left, the left dock should now be closed.
4968 assert!(!workspace.left_dock().read(cx).is_open());
4969 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
4970 let right_dock = workspace.right_dock();
4971 assert_eq!(
4972 right_dock.read(cx).visible_panel().unwrap().id(),
4973 panel_1.id()
4974 );
4975 assert_eq!(right_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
4976
4977 // Now we move panel_2Β to the left
4978 panel_2.set_position(DockPosition::Left, cx);
4979 });
4980
4981 workspace.update(cx, |workspace, cx| {
4982 // Since panel_2 was not visible on the right, we don't open the left dock.
4983 assert!(!workspace.left_dock().read(cx).is_open());
4984 // And the right dock is unaffected in it's displaying of panel_1
4985 assert!(workspace.right_dock().read(cx).is_open());
4986 assert_eq!(
4987 workspace
4988 .right_dock()
4989 .read(cx)
4990 .visible_panel()
4991 .unwrap()
4992 .id(),
4993 panel_1.id()
4994 );
4995 });
4996
4997 // Move panel_1 back to the left
4998 panel_1.update(cx, |panel_1, cx| {
4999 panel_1.set_position(DockPosition::Left, cx)
5000 });
5001
5002 workspace.update(cx, |workspace, cx| {
5003 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
5004 let left_dock = workspace.left_dock();
5005 assert!(left_dock.read(cx).is_open());
5006 assert_eq!(
5007 left_dock.read(cx).visible_panel().unwrap().id(),
5008 panel_1.id()
5009 );
5010 assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
5011 // And right the dock should be closed as it no longer has any panels.
5012 assert!(!workspace.right_dock().read(cx).is_open());
5013
5014 // Now we move panel_1 to the bottom
5015 panel_1.set_position(DockPosition::Bottom, cx);
5016 });
5017
5018 workspace.update(cx, |workspace, cx| {
5019 // Since panel_1 was visible on the left, we close the left dock.
5020 assert!(!workspace.left_dock().read(cx).is_open());
5021 // The bottom dock is sized based on the panel's default size,
5022 // since the panel orientation changed from vertical to horizontal.
5023 let bottom_dock = workspace.bottom_dock();
5024 assert_eq!(
5025 bottom_dock.read(cx).active_panel_size(cx).unwrap(),
5026 panel_1.size(cx),
5027 );
5028 // Close bottom dock and move panel_1 back to the left.
5029 bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
5030 panel_1.set_position(DockPosition::Left, cx);
5031 });
5032
5033 // Emit activated event on panel 1
5034 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Activated));
5035
5036 // Now the left dock is open and panel_1 is active and focused.
5037 workspace.read_with(cx, |workspace, cx| {
5038 let left_dock = workspace.left_dock();
5039 assert!(left_dock.read(cx).is_open());
5040 assert_eq!(
5041 left_dock.read(cx).visible_panel().unwrap().id(),
5042 panel_1.id()
5043 );
5044 assert!(panel_1.is_focused(cx));
5045 });
5046
5047 // Emit closed event on panel 2, which is not active
5048 panel_2.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed));
5049
5050 // Wo don't close the left dock, because panel_2 wasn't the active panel
5051 workspace.read_with(cx, |workspace, cx| {
5052 let left_dock = workspace.left_dock();
5053 assert!(left_dock.read(cx).is_open());
5054 assert_eq!(
5055 left_dock.read(cx).visible_panel().unwrap().id(),
5056 panel_1.id()
5057 );
5058 });
5059
5060 // Emitting a ZoomIn event shows the panel as zoomed.
5061 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomIn));
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::Left));
5065 });
5066
5067 // Move panel to another dock while it is zoomed
5068 panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
5069 workspace.read_with(cx, |workspace, _| {
5070 assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
5071 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5072 });
5073
5074 // If focus is transferred to another view that's not a panel or another pane, we still show
5075 // the panel as zoomed.
5076 let focus_receiver = window.add_view(cx, |_| EmptyView);
5077 focus_receiver.update(cx, |_, cx| cx.focus_self());
5078 workspace.read_with(cx, |workspace, _| {
5079 assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
5080 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5081 });
5082
5083 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
5084 workspace.update(cx, |_, cx| cx.focus_self());
5085 workspace.read_with(cx, |workspace, _| {
5086 assert_eq!(workspace.zoomed, None);
5087 assert_eq!(workspace.zoomed_position, None);
5088 });
5089
5090 // If focus is transferred again to another view that's not a panel or a pane, we won't
5091 // show the panel as zoomed because it wasn't zoomed before.
5092 focus_receiver.update(cx, |_, cx| cx.focus_self());
5093 workspace.read_with(cx, |workspace, _| {
5094 assert_eq!(workspace.zoomed, None);
5095 assert_eq!(workspace.zoomed_position, None);
5096 });
5097
5098 // When focus is transferred back to the panel, it is zoomed again.
5099 panel_1.update(cx, |_, cx| cx.focus_self());
5100 workspace.read_with(cx, |workspace, _| {
5101 assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
5102 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5103 });
5104
5105 // Emitting a ZoomOut event unzooms the panel.
5106 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomOut));
5107 workspace.read_with(cx, |workspace, _| {
5108 assert_eq!(workspace.zoomed, None);
5109 assert_eq!(workspace.zoomed_position, None);
5110 });
5111
5112 // Emit closed event on panel 1, which is active
5113 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed));
5114
5115 // Now the left dock is closed, because panel_1 was the active panel
5116 workspace.read_with(cx, |workspace, cx| {
5117 let right_dock = workspace.right_dock();
5118 assert!(!right_dock.read(cx).is_open());
5119 });
5120 }
5121
5122 pub fn init_test(cx: &mut TestAppContext) {
5123 cx.foreground().forbid_parking();
5124 cx.update(|cx| {
5125 cx.set_global(SettingsStore::test(cx));
5126 theme::init((), cx);
5127 language::init(cx);
5128 crate::init_settings(cx);
5129 Project::init_settings(cx);
5130 });
5131 }
5132}