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