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