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