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