1pub mod dock;
2pub mod item;
3pub mod notifications;
4pub mod pane;
5pub mod pane_group;
6mod persistence;
7pub mod searchable;
8pub mod shared_screen;
9mod status_bar;
10mod toolbar;
11mod workspace_settings;
12
13use anyhow::{anyhow, Context, Result};
14use call::ActiveCall;
15use client::{
16 proto::{self, PeerId},
17 Client, TypedEnvelope, UserStore,
18};
19use collections::{hash_map, HashMap, HashSet};
20use drag_and_drop::DragAndDrop;
21use futures::{
22 channel::{mpsc, oneshot},
23 future::try_join_all,
24 FutureExt, StreamExt,
25};
26use gpui::{
27 actions,
28 elements::*,
29 geometry::{
30 rect::RectF,
31 vector::{vec2f, Vector2F},
32 },
33 impl_actions,
34 platform::{
35 CursorStyle, ModifiersChangedEvent, MouseButton, PathPromptOptions, Platform, PromptLevel,
36 WindowBounds, WindowOptions,
37 },
38 AnyModelHandle, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity,
39 ModelContext, ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle,
40 WeakViewHandle, WindowContext, WindowHandle,
41};
42use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem};
43use itertools::Itertools;
44use language::{LanguageRegistry, Rope};
45use std::{
46 any::TypeId,
47 borrow::Cow,
48 cmp, env,
49 future::Future,
50 path::{Path, PathBuf},
51 rc::Rc,
52 str,
53 sync::{atomic::AtomicUsize, Arc},
54 time::Duration,
55};
56
57use crate::{
58 notifications::{simple_message_notification::MessageNotification, NotificationTracker},
59 persistence::model::{
60 DockData, DockStructure, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
61 },
62};
63use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle};
64use lazy_static::lazy_static;
65use notifications::{NotificationHandle, NotifyResultExt};
66pub use pane::*;
67pub use pane_group::*;
68use persistence::{model::SerializedItem, DB};
69pub use persistence::{
70 model::{ItemId, WorkspaceLocation},
71 WorkspaceDb, DB as WORKSPACE_DB,
72};
73use postage::prelude::Stream;
74use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
75use serde::Deserialize;
76use shared_screen::SharedScreen;
77use status_bar::StatusBar;
78pub use status_bar::StatusItemView;
79use theme::{Theme, ThemeSettings};
80pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
81use util::ResultExt;
82pub use workspace_settings::{AutosaveSetting, GitGutterSetting, WorkspaceSettings};
83
84lazy_static! {
85 static ref ZED_WINDOW_SIZE: Option<Vector2F> = env::var("ZED_WINDOW_SIZE")
86 .ok()
87 .as_deref()
88 .and_then(parse_pixel_position_env_var);
89 static ref ZED_WINDOW_POSITION: Option<Vector2F> = env::var("ZED_WINDOW_POSITION")
90 .ok()
91 .as_deref()
92 .and_then(parse_pixel_position_env_var);
93}
94
95pub trait Modal: View {
96 fn has_focus(&self) -> bool;
97 fn dismiss_on_event(event: &Self::Event) -> bool;
98}
99
100trait ModalHandle {
101 fn as_any(&self) -> &AnyViewHandle;
102 fn has_focus(&self, cx: &WindowContext) -> bool;
103}
104
105impl<T: Modal> ModalHandle for ViewHandle<T> {
106 fn as_any(&self) -> &AnyViewHandle {
107 self
108 }
109
110 fn has_focus(&self, cx: &WindowContext) -> bool {
111 self.read(cx).has_focus()
112 }
113}
114
115#[derive(Clone, PartialEq)]
116pub struct RemoveWorktreeFromProject(pub WorktreeId);
117
118actions!(
119 workspace,
120 [
121 Open,
122 NewFile,
123 NewWindow,
124 CloseWindow,
125 CloseInactiveTabsAndPanes,
126 AddFolderToProject,
127 Unfollow,
128 SaveAs,
129 ReloadActiveItem,
130 ActivatePreviousPane,
131 ActivateNextPane,
132 FollowNextCollaborator,
133 NewTerminal,
134 NewCenterTerminal,
135 ToggleTerminalFocus,
136 NewSearch,
137 Feedback,
138 Restart,
139 Welcome,
140 ToggleZoom,
141 ToggleLeftDock,
142 ToggleRightDock,
143 ToggleBottomDock,
144 CloseAllDocks,
145 ]
146);
147
148#[derive(Clone, PartialEq)]
149pub struct OpenPaths {
150 pub paths: Vec<PathBuf>,
151}
152
153#[derive(Clone, Deserialize, PartialEq)]
154pub struct ActivatePane(pub usize);
155
156#[derive(Clone, Deserialize, PartialEq)]
157pub struct ActivatePaneInDirection(pub SplitDirection);
158
159#[derive(Clone, Deserialize, PartialEq)]
160pub struct SwapPaneInDirection(pub SplitDirection);
161
162#[derive(Clone, Deserialize, PartialEq)]
163pub struct NewFileInDirection(pub SplitDirection);
164
165#[derive(Clone, PartialEq, Debug, Deserialize)]
166#[serde(rename_all = "camelCase")]
167pub struct SaveAll {
168 pub save_intent: Option<SaveIntent>,
169}
170
171#[derive(Clone, PartialEq, Debug, Deserialize)]
172#[serde(rename_all = "camelCase")]
173pub struct Save {
174 pub save_intent: Option<SaveIntent>,
175}
176
177#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
178#[serde(rename_all = "camelCase")]
179pub struct CloseAllItemsAndPanes {
180 pub save_intent: Option<SaveIntent>,
181}
182
183#[derive(Deserialize)]
184pub struct Toast {
185 id: usize,
186 msg: Cow<'static, str>,
187 #[serde(skip)]
188 on_click: Option<(Cow<'static, str>, Arc<dyn Fn(&mut WindowContext)>)>,
189}
190
191impl Toast {
192 pub fn new<I: Into<Cow<'static, str>>>(id: usize, msg: I) -> Self {
193 Toast {
194 id,
195 msg: msg.into(),
196 on_click: None,
197 }
198 }
199
200 pub fn on_click<F, M>(mut self, message: M, on_click: F) -> Self
201 where
202 M: Into<Cow<'static, str>>,
203 F: Fn(&mut WindowContext) + 'static,
204 {
205 self.on_click = Some((message.into(), Arc::new(on_click)));
206 self
207 }
208}
209
210impl PartialEq for Toast {
211 fn eq(&self, other: &Self) -> bool {
212 self.id == other.id
213 && self.msg == other.msg
214 && self.on_click.is_some() == other.on_click.is_some()
215 }
216}
217
218impl Clone for Toast {
219 fn clone(&self) -> Self {
220 Toast {
221 id: self.id,
222 msg: self.msg.to_owned(),
223 on_click: self.on_click.clone(),
224 }
225 }
226}
227
228#[derive(Clone, Deserialize, PartialEq)]
229pub struct OpenTerminal {
230 pub working_directory: PathBuf,
231}
232
233impl_actions!(
234 workspace,
235 [
236 ActivatePane,
237 ActivatePaneInDirection,
238 SwapPaneInDirection,
239 NewFileInDirection,
240 Toast,
241 OpenTerminal,
242 SaveAll,
243 Save,
244 CloseAllItemsAndPanes,
245 ]
246);
247
248pub type WorkspaceId = i64;
249
250pub fn init_settings(cx: &mut AppContext) {
251 settings::register::<WorkspaceSettings>(cx);
252 settings::register::<item::ItemSettings>(cx);
253}
254
255pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
256 init_settings(cx);
257 pane::init(cx);
258 notifications::init(cx);
259
260 cx.add_global_action({
261 let app_state = Arc::downgrade(&app_state);
262 move |_: &Open, cx: &mut AppContext| {
263 let mut paths = cx.prompt_for_paths(PathPromptOptions {
264 files: true,
265 directories: true,
266 multiple: true,
267 });
268
269 if let Some(app_state) = app_state.upgrade() {
270 cx.spawn(move |mut cx| async move {
271 if let Some(paths) = paths.recv().await.flatten() {
272 cx.update(|cx| {
273 open_paths(&paths, &app_state, None, cx).detach_and_log_err(cx)
274 });
275 }
276 })
277 .detach();
278 }
279 }
280 });
281 cx.add_async_action(Workspace::open);
282
283 cx.add_async_action(Workspace::follow_next_collaborator);
284 cx.add_async_action(Workspace::close);
285 cx.add_async_action(Workspace::close_inactive_items_and_panes);
286 cx.add_async_action(Workspace::close_all_items_and_panes);
287 cx.add_global_action(Workspace::close_global);
288 cx.add_global_action(restart);
289 cx.add_async_action(Workspace::save_all);
290 cx.add_action(Workspace::add_folder_to_project);
291 cx.add_action(
292 |workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext<Workspace>| {
293 let pane = workspace.active_pane().clone();
294 workspace.unfollow(&pane, cx);
295 },
296 );
297 cx.add_action(
298 |workspace: &mut Workspace, action: &Save, cx: &mut ViewContext<Workspace>| {
299 workspace
300 .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx)
301 .detach_and_log_err(cx);
302 },
303 );
304 cx.add_action(
305 |workspace: &mut Workspace, _: &SaveAs, cx: &mut ViewContext<Workspace>| {
306 workspace
307 .save_active_item(SaveIntent::SaveAs, cx)
308 .detach_and_log_err(cx);
309 },
310 );
311 cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| {
312 workspace.activate_previous_pane(cx)
313 });
314 cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| {
315 workspace.activate_next_pane(cx)
316 });
317
318 cx.add_action(
319 |workspace: &mut Workspace, action: &ActivatePaneInDirection, cx| {
320 workspace.activate_pane_in_direction(action.0, cx)
321 },
322 );
323
324 cx.add_action(
325 |workspace: &mut Workspace, action: &SwapPaneInDirection, cx| {
326 workspace.swap_pane_in_direction(action.0, cx)
327 },
328 );
329
330 cx.add_action(|workspace: &mut Workspace, _: &ToggleLeftDock, cx| {
331 workspace.toggle_dock(DockPosition::Left, cx);
332 });
333 cx.add_action(|workspace: &mut Workspace, _: &ToggleRightDock, cx| {
334 workspace.toggle_dock(DockPosition::Right, cx);
335 });
336 cx.add_action(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| {
337 workspace.toggle_dock(DockPosition::Bottom, cx);
338 });
339 cx.add_action(|workspace: &mut Workspace, _: &CloseAllDocks, cx| {
340 workspace.close_all_docks(cx);
341 });
342 cx.add_action(Workspace::activate_pane_at_index);
343 cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
344 workspace.reopen_closed_item(cx).detach();
345 });
346 cx.add_action(|workspace: &mut Workspace, _: &GoBack, cx| {
347 workspace
348 .go_back(workspace.active_pane().downgrade(), cx)
349 .detach();
350 });
351 cx.add_action(|workspace: &mut Workspace, _: &GoForward, cx| {
352 workspace
353 .go_forward(workspace.active_pane().downgrade(), cx)
354 .detach();
355 });
356
357 cx.add_action(|_: &mut Workspace, _: &install_cli::Install, cx| {
358 cx.spawn(|workspace, mut cx| async move {
359 let err = install_cli::install_cli(&cx)
360 .await
361 .context("Failed to create CLI symlink");
362
363 workspace.update(&mut cx, |workspace, cx| {
364 if matches!(err, Err(_)) {
365 err.notify_err(workspace, cx);
366 } else {
367 workspace.show_notification(1, cx, |cx| {
368 cx.add_view(|_| {
369 MessageNotification::new("Successfully installed the `zed` binary")
370 })
371 });
372 }
373 })
374 })
375 .detach();
376 });
377}
378
379type ProjectItemBuilders = HashMap<
380 TypeId,
381 fn(ModelHandle<Project>, AnyModelHandle, &mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
382>;
383pub fn register_project_item<I: ProjectItem>(cx: &mut AppContext) {
384 cx.update_default_global(|builders: &mut ProjectItemBuilders, _| {
385 builders.insert(TypeId::of::<I::Item>(), |project, model, cx| {
386 let item = model.downcast::<I::Item>().unwrap();
387 Box::new(cx.add_view(|cx| I::for_project_item(project, item, cx)))
388 });
389 });
390}
391
392type FollowableItemBuilder = fn(
393 ViewHandle<Pane>,
394 ViewHandle<Workspace>,
395 ViewId,
396 &mut Option<proto::view::Variant>,
397 &mut AppContext,
398) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>;
399type FollowableItemBuilders = HashMap<
400 TypeId,
401 (
402 FollowableItemBuilder,
403 fn(&AnyViewHandle) -> Box<dyn FollowableItemHandle>,
404 ),
405>;
406pub fn register_followable_item<I: FollowableItem>(cx: &mut AppContext) {
407 cx.update_default_global(|builders: &mut FollowableItemBuilders, _| {
408 builders.insert(
409 TypeId::of::<I>(),
410 (
411 |pane, workspace, id, state, cx| {
412 I::from_state_proto(pane, workspace, id, state, cx).map(|task| {
413 cx.foreground()
414 .spawn(async move { Ok(Box::new(task.await?) as Box<_>) })
415 })
416 },
417 |this| Box::new(this.clone().downcast::<I>().unwrap()),
418 ),
419 );
420 });
421}
422
423type ItemDeserializers = HashMap<
424 Arc<str>,
425 fn(
426 ModelHandle<Project>,
427 WeakViewHandle<Workspace>,
428 WorkspaceId,
429 ItemId,
430 &mut ViewContext<Pane>,
431 ) -> Task<Result<Box<dyn ItemHandle>>>,
432>;
433pub fn register_deserializable_item<I: Item>(cx: &mut AppContext) {
434 cx.update_default_global(|deserializers: &mut ItemDeserializers, _cx| {
435 if let Some(serialized_item_kind) = I::serialized_item_kind() {
436 deserializers.insert(
437 Arc::from(serialized_item_kind),
438 |project, workspace, workspace_id, item_id, cx| {
439 let task = I::deserialize(project, workspace, workspace_id, item_id, cx);
440 cx.foreground()
441 .spawn(async { Ok(Box::new(task.await?) as Box<_>) })
442 },
443 );
444 }
445 });
446}
447
448pub struct AppState {
449 pub languages: Arc<LanguageRegistry>,
450 pub client: Arc<Client>,
451 pub user_store: ModelHandle<UserStore>,
452 pub workspace_store: ModelHandle<WorkspaceStore>,
453 pub fs: Arc<dyn fs::Fs>,
454 pub build_window_options:
455 fn(Option<WindowBounds>, Option<uuid::Uuid>, &dyn Platform) -> WindowOptions<'static>,
456 pub initialize_workspace:
457 fn(WeakViewHandle<Workspace>, bool, Arc<AppState>, AsyncAppContext) -> Task<Result<()>>,
458 pub background_actions: BackgroundActions,
459}
460
461pub struct WorkspaceStore {
462 workspaces: HashSet<WeakViewHandle<Workspace>>,
463 followers: Vec<Follower>,
464 client: Arc<Client>,
465 _subscriptions: Vec<client::Subscription>,
466}
467
468#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
469struct Follower {
470 project_id: Option<u64>,
471 peer_id: PeerId,
472}
473
474impl AppState {
475 #[cfg(any(test, feature = "test-support"))]
476 pub fn test(cx: &mut AppContext) -> Arc<Self> {
477 use settings::SettingsStore;
478
479 if !cx.has_global::<SettingsStore>() {
480 cx.set_global(SettingsStore::test(cx));
481 }
482
483 let fs = fs::FakeFs::new(cx.background().clone());
484 let languages = Arc::new(LanguageRegistry::test());
485 let http_client = util::http::FakeHttpClient::with_404_response();
486 let client = Client::new(http_client.clone(), cx);
487 let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
488 let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx));
489
490 theme::init((), cx);
491 client::init(&client, cx);
492 crate::init_settings(cx);
493
494 Arc::new(Self {
495 client,
496 fs,
497 languages,
498 user_store,
499 // channel_store,
500 workspace_store,
501 initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
502 build_window_options: |_, _, _| Default::default(),
503 background_actions: || &[],
504 })
505 }
506}
507
508struct DelayedDebouncedEditAction {
509 task: Option<Task<()>>,
510 cancel_channel: Option<oneshot::Sender<()>>,
511}
512
513impl DelayedDebouncedEditAction {
514 fn new() -> DelayedDebouncedEditAction {
515 DelayedDebouncedEditAction {
516 task: None,
517 cancel_channel: None,
518 }
519 }
520
521 fn fire_new<F>(&mut self, delay: Duration, cx: &mut ViewContext<Workspace>, func: F)
522 where
523 F: 'static + FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> Task<Result<()>>,
524 {
525 if let Some(channel) = self.cancel_channel.take() {
526 _ = channel.send(());
527 }
528
529 let (sender, mut receiver) = oneshot::channel::<()>();
530 self.cancel_channel = Some(sender);
531
532 let previous_task = self.task.take();
533 self.task = Some(cx.spawn(|workspace, mut cx| async move {
534 let mut timer = cx.background().timer(delay).fuse();
535 if let Some(previous_task) = previous_task {
536 previous_task.await;
537 }
538
539 futures::select_biased! {
540 _ = receiver => return,
541 _ = timer => {}
542 }
543
544 if let Some(result) = workspace
545 .update(&mut cx, |workspace, cx| (func)(workspace, cx))
546 .log_err()
547 {
548 result.await.log_err();
549 }
550 }));
551 }
552}
553
554pub enum Event {
555 PaneAdded(ViewHandle<Pane>),
556 ContactRequestedJoin(u64),
557}
558
559pub struct Workspace {
560 weak_self: WeakViewHandle<Self>,
561 modal: Option<ActiveModal>,
562 zoomed: Option<AnyWeakViewHandle>,
563 zoomed_position: Option<DockPosition>,
564 center: PaneGroup,
565 left_dock: ViewHandle<Dock>,
566 bottom_dock: ViewHandle<Dock>,
567 right_dock: ViewHandle<Dock>,
568 panes: Vec<ViewHandle<Pane>>,
569 panes_by_item: HashMap<usize, WeakViewHandle<Pane>>,
570 active_pane: ViewHandle<Pane>,
571 last_active_center_pane: Option<WeakViewHandle<Pane>>,
572 last_active_view_id: Option<proto::ViewId>,
573 status_bar: ViewHandle<StatusBar>,
574 titlebar_item: Option<AnyViewHandle>,
575 notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
576 project: ModelHandle<Project>,
577 follower_states: 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
4142#[allow(clippy::type_complexity)]
4143pub fn open_paths(
4144 abs_paths: &[PathBuf],
4145 app_state: &Arc<AppState>,
4146 requesting_window: Option<WindowHandle<Workspace>>,
4147 cx: &mut AppContext,
4148) -> Task<
4149 Result<(
4150 WeakViewHandle<Workspace>,
4151 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
4152 )>,
4153> {
4154 let app_state = app_state.clone();
4155 let abs_paths = abs_paths.to_vec();
4156 cx.spawn(|mut cx| async move {
4157 // Open paths in existing workspace if possible
4158 let existing = activate_workspace_for_project(&mut cx, |project, cx| {
4159 project.contains_paths(&abs_paths, cx)
4160 });
4161
4162 if let Some(existing) = existing {
4163 Ok((
4164 existing.clone(),
4165 existing
4166 .update(&mut cx, |workspace, cx| {
4167 workspace.open_paths(abs_paths, true, cx)
4168 })?
4169 .await,
4170 ))
4171 } else {
4172 Ok(cx
4173 .update(|cx| {
4174 Workspace::new_local(abs_paths, app_state.clone(), requesting_window, cx)
4175 })
4176 .await)
4177 }
4178 })
4179}
4180
4181pub fn open_new(
4182 app_state: &Arc<AppState>,
4183 cx: &mut AppContext,
4184 init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static,
4185) -> Task<()> {
4186 let task = Workspace::new_local(Vec::new(), app_state.clone(), None, cx);
4187 cx.spawn(|mut cx| async move {
4188 let (workspace, opened_paths) = task.await;
4189
4190 workspace
4191 .update(&mut cx, |workspace, cx| {
4192 if opened_paths.is_empty() {
4193 init(workspace, cx)
4194 }
4195 })
4196 .log_err();
4197 })
4198}
4199
4200pub fn create_and_open_local_file(
4201 path: &'static Path,
4202 cx: &mut ViewContext<Workspace>,
4203 default_content: impl 'static + Send + FnOnce() -> Rope,
4204) -> Task<Result<Box<dyn ItemHandle>>> {
4205 cx.spawn(|workspace, mut cx| async move {
4206 let fs = workspace.read_with(&cx, |workspace, _| workspace.app_state().fs.clone())?;
4207 if !fs.is_file(path).await {
4208 fs.create_file(path, Default::default()).await?;
4209 fs.save(path, &default_content(), Default::default())
4210 .await?;
4211 }
4212
4213 let mut items = workspace
4214 .update(&mut cx, |workspace, cx| {
4215 workspace.with_local_workspace(cx, |workspace, cx| {
4216 workspace.open_paths(vec![path.to_path_buf()], false, cx)
4217 })
4218 })?
4219 .await?
4220 .await;
4221
4222 let item = items.pop().flatten();
4223 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
4224 })
4225}
4226
4227pub fn join_remote_project(
4228 project_id: u64,
4229 follow_user_id: u64,
4230 app_state: Arc<AppState>,
4231 cx: &mut AppContext,
4232) -> Task<Result<()>> {
4233 cx.spawn(|mut cx| async move {
4234 let windows = cx.windows();
4235 let existing_workspace = windows.into_iter().find_map(|window| {
4236 window.downcast::<Workspace>().and_then(|window| {
4237 window
4238 .read_root_with(&cx, |workspace, cx| {
4239 if workspace.project().read(cx).remote_id() == Some(project_id) {
4240 Some(cx.handle().downgrade())
4241 } else {
4242 None
4243 }
4244 })
4245 .unwrap_or(None)
4246 })
4247 });
4248
4249 let workspace = if let Some(existing_workspace) = existing_workspace {
4250 existing_workspace
4251 } else {
4252 let active_call = cx.read(ActiveCall::global);
4253 let room = active_call
4254 .read_with(&cx, |call, _| call.room().cloned())
4255 .ok_or_else(|| anyhow!("not in a call"))?;
4256 let project = room
4257 .update(&mut cx, |room, cx| {
4258 room.join_project(
4259 project_id,
4260 app_state.languages.clone(),
4261 app_state.fs.clone(),
4262 cx,
4263 )
4264 })
4265 .await?;
4266
4267 let window_bounds_override = window_bounds_env_override(&cx);
4268 let window = cx.add_window(
4269 (app_state.build_window_options)(
4270 window_bounds_override,
4271 None,
4272 cx.platform().as_ref(),
4273 ),
4274 |cx| Workspace::new(0, project, app_state.clone(), cx),
4275 );
4276 let workspace = window.root(&cx).unwrap();
4277 (app_state.initialize_workspace)(
4278 workspace.downgrade(),
4279 false,
4280 app_state.clone(),
4281 cx.clone(),
4282 )
4283 .await
4284 .log_err();
4285
4286 workspace.downgrade()
4287 };
4288
4289 workspace.window().activate(&mut cx);
4290 cx.platform().activate(true);
4291
4292 workspace.update(&mut cx, |workspace, cx| {
4293 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
4294 let follow_peer_id = room
4295 .read(cx)
4296 .remote_participants()
4297 .iter()
4298 .find(|(_, participant)| participant.user.id == follow_user_id)
4299 .map(|(_, p)| p.peer_id)
4300 .or_else(|| {
4301 // If we couldn't follow the given user, follow the host instead.
4302 let collaborator = workspace
4303 .project()
4304 .read(cx)
4305 .collaborators()
4306 .values()
4307 .find(|collaborator| collaborator.replica_id == 0)?;
4308 Some(collaborator.peer_id)
4309 });
4310
4311 if let Some(follow_peer_id) = follow_peer_id {
4312 workspace
4313 .follow(follow_peer_id, cx)
4314 .map(|follow| follow.detach_and_log_err(cx));
4315 }
4316 }
4317 })?;
4318
4319 anyhow::Ok(())
4320 })
4321}
4322
4323pub fn restart(_: &Restart, cx: &mut AppContext) {
4324 let should_confirm = settings::get::<WorkspaceSettings>(cx).confirm_quit;
4325 cx.spawn(|mut cx| async move {
4326 let mut workspace_windows = cx
4327 .windows()
4328 .into_iter()
4329 .filter_map(|window| window.downcast::<Workspace>())
4330 .collect::<Vec<_>>();
4331
4332 // If multiple windows have unsaved changes, and need a save prompt,
4333 // prompt in the active window before switching to a different window.
4334 workspace_windows.sort_by_key(|window| window.is_active(&cx) == Some(false));
4335
4336 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
4337 let answer = window.prompt(
4338 PromptLevel::Info,
4339 "Are you sure you want to restart?",
4340 &["Restart", "Cancel"],
4341 &mut cx,
4342 );
4343
4344 if let Some(mut answer) = answer {
4345 let answer = answer.next().await;
4346 if answer != Some(0) {
4347 return Ok(());
4348 }
4349 }
4350 }
4351
4352 // If the user cancels any save prompt, then keep the app open.
4353 for window in workspace_windows {
4354 if let Some(should_close) = window.update_root(&mut cx, |workspace, cx| {
4355 workspace.prepare_to_close(true, cx)
4356 }) {
4357 if !should_close.await? {
4358 return Ok(());
4359 }
4360 }
4361 }
4362 cx.platform().restart();
4363 anyhow::Ok(())
4364 })
4365 .detach_and_log_err(cx);
4366}
4367
4368fn parse_pixel_position_env_var(value: &str) -> Option<Vector2F> {
4369 let mut parts = value.split(',');
4370 let width: usize = parts.next()?.parse().ok()?;
4371 let height: usize = parts.next()?.parse().ok()?;
4372 Some(vec2f(width as f32, height as f32))
4373}
4374
4375#[cfg(test)]
4376mod tests {
4377 use super::*;
4378 use crate::{
4379 dock::test::{TestPanel, TestPanelEvent},
4380 item::test::{TestItem, TestItemEvent, TestProjectItem},
4381 };
4382 use fs::FakeFs;
4383 use gpui::{executor::Deterministic, test::EmptyView, TestAppContext};
4384 use project::{Project, ProjectEntryId};
4385 use serde_json::json;
4386 use settings::SettingsStore;
4387 use std::{cell::RefCell, rc::Rc};
4388
4389 #[gpui::test]
4390 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
4391 init_test(cx);
4392
4393 let fs = FakeFs::new(cx.background());
4394 let project = Project::test(fs, [], cx).await;
4395 let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
4396 let workspace = window.root(cx);
4397
4398 // Adding an item with no ambiguity renders the tab without detail.
4399 let item1 = window.add_view(cx, |_| {
4400 let mut item = TestItem::new();
4401 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
4402 item
4403 });
4404 workspace.update(cx, |workspace, cx| {
4405 workspace.add_item(Box::new(item1.clone()), cx);
4406 });
4407 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), None));
4408
4409 // Adding an item that creates ambiguity increases the level of detail on
4410 // both tabs.
4411 let item2 = window.add_view(cx, |_| {
4412 let mut item = TestItem::new();
4413 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
4414 item
4415 });
4416 workspace.update(cx, |workspace, cx| {
4417 workspace.add_item(Box::new(item2.clone()), cx);
4418 });
4419 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4420 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4421
4422 // Adding an item that creates ambiguity increases the level of detail only
4423 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
4424 // we stop at the highest detail available.
4425 let item3 = window.add_view(cx, |_| {
4426 let mut item = TestItem::new();
4427 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
4428 item
4429 });
4430 workspace.update(cx, |workspace, cx| {
4431 workspace.add_item(Box::new(item3.clone()), cx);
4432 });
4433 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4434 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
4435 item3.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
4436 }
4437
4438 #[gpui::test]
4439 async fn test_tracking_active_path(cx: &mut TestAppContext) {
4440 init_test(cx);
4441
4442 let fs = FakeFs::new(cx.background());
4443 fs.insert_tree(
4444 "/root1",
4445 json!({
4446 "one.txt": "",
4447 "two.txt": "",
4448 }),
4449 )
4450 .await;
4451 fs.insert_tree(
4452 "/root2",
4453 json!({
4454 "three.txt": "",
4455 }),
4456 )
4457 .await;
4458
4459 let project = Project::test(fs, ["root1".as_ref()], cx).await;
4460 let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
4461 let workspace = window.root(cx);
4462 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4463 let worktree_id = project.read_with(cx, |project, cx| {
4464 project.worktrees(cx).next().unwrap().read(cx).id()
4465 });
4466
4467 let item1 = window.add_view(cx, |cx| {
4468 TestItem::new().with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
4469 });
4470 let item2 = window.add_view(cx, |cx| {
4471 TestItem::new().with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
4472 });
4473
4474 // Add an item to an empty pane
4475 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx));
4476 project.read_with(cx, |project, cx| {
4477 assert_eq!(
4478 project.active_entry(),
4479 project
4480 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
4481 .map(|e| e.id)
4482 );
4483 });
4484 assert_eq!(window.current_title(cx).as_deref(), Some("one.txt β root1"));
4485
4486 // Add a second item to a non-empty pane
4487 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
4488 assert_eq!(window.current_title(cx).as_deref(), Some("two.txt β root1"));
4489 project.read_with(cx, |project, cx| {
4490 assert_eq!(
4491 project.active_entry(),
4492 project
4493 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
4494 .map(|e| e.id)
4495 );
4496 });
4497
4498 // Close the active item
4499 pane.update(cx, |pane, cx| {
4500 pane.close_active_item(&Default::default(), cx).unwrap()
4501 })
4502 .await
4503 .unwrap();
4504 assert_eq!(window.current_title(cx).as_deref(), Some("one.txt β root1"));
4505 project.read_with(cx, |project, cx| {
4506 assert_eq!(
4507 project.active_entry(),
4508 project
4509 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
4510 .map(|e| e.id)
4511 );
4512 });
4513
4514 // Add a project folder
4515 project
4516 .update(cx, |project, cx| {
4517 project.find_or_create_local_worktree("/root2", true, cx)
4518 })
4519 .await
4520 .unwrap();
4521 assert_eq!(
4522 window.current_title(cx).as_deref(),
4523 Some("one.txt β root1, root2")
4524 );
4525
4526 // Remove a project folder
4527 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
4528 assert_eq!(window.current_title(cx).as_deref(), Some("one.txt β root2"));
4529 }
4530
4531 #[gpui::test]
4532 async fn test_close_window(cx: &mut TestAppContext) {
4533 init_test(cx);
4534
4535 let fs = FakeFs::new(cx.background());
4536 fs.insert_tree("/root", json!({ "one": "" })).await;
4537
4538 let project = Project::test(fs, ["root".as_ref()], cx).await;
4539 let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
4540 let workspace = window.root(cx);
4541
4542 // When there are no dirty items, there's nothing to do.
4543 let item1 = window.add_view(cx, |_| TestItem::new());
4544 workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
4545 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
4546 assert!(task.await.unwrap());
4547
4548 // When there are dirty untitled items, prompt to save each one. If the user
4549 // cancels any prompt, then abort.
4550 let item2 = window.add_view(cx, |_| TestItem::new().with_dirty(true));
4551 let item3 = window.add_view(cx, |cx| {
4552 TestItem::new()
4553 .with_dirty(true)
4554 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4555 });
4556 workspace.update(cx, |w, cx| {
4557 w.add_item(Box::new(item2.clone()), cx);
4558 w.add_item(Box::new(item3.clone()), cx);
4559 });
4560 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
4561 cx.foreground().run_until_parked();
4562 window.simulate_prompt_answer(2, cx); // cancel save all
4563 cx.foreground().run_until_parked();
4564 window.simulate_prompt_answer(2, cx); // cancel save all
4565 cx.foreground().run_until_parked();
4566 assert!(!window.has_pending_prompt(cx));
4567 assert!(!task.await.unwrap());
4568 }
4569
4570 #[gpui::test]
4571 async fn test_close_pane_items(cx: &mut TestAppContext) {
4572 init_test(cx);
4573
4574 let fs = FakeFs::new(cx.background());
4575
4576 let project = Project::test(fs, None, cx).await;
4577 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
4578 let workspace = window.root(cx);
4579
4580 let item1 = window.add_view(cx, |cx| {
4581 TestItem::new()
4582 .with_dirty(true)
4583 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4584 });
4585 let item2 = window.add_view(cx, |cx| {
4586 TestItem::new()
4587 .with_dirty(true)
4588 .with_conflict(true)
4589 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
4590 });
4591 let item3 = window.add_view(cx, |cx| {
4592 TestItem::new()
4593 .with_dirty(true)
4594 .with_conflict(true)
4595 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
4596 });
4597 let item4 = window.add_view(cx, |cx| {
4598 TestItem::new()
4599 .with_dirty(true)
4600 .with_project_items(&[TestProjectItem::new_untitled(cx)])
4601 });
4602 let pane = workspace.update(cx, |workspace, cx| {
4603 workspace.add_item(Box::new(item1.clone()), cx);
4604 workspace.add_item(Box::new(item2.clone()), cx);
4605 workspace.add_item(Box::new(item3.clone()), cx);
4606 workspace.add_item(Box::new(item4.clone()), cx);
4607 workspace.active_pane().clone()
4608 });
4609
4610 let close_items = pane.update(cx, |pane, cx| {
4611 pane.activate_item(1, true, true, cx);
4612 assert_eq!(pane.active_item().unwrap().id(), item2.id());
4613 let item1_id = item1.id();
4614 let item3_id = item3.id();
4615 let item4_id = item4.id();
4616 pane.close_items(cx, SaveIntent::Close, move |id| {
4617 [item1_id, item3_id, item4_id].contains(&id)
4618 })
4619 });
4620 cx.foreground().run_until_parked();
4621
4622 assert!(window.has_pending_prompt(cx));
4623 // Ignore "Save all" prompt
4624 window.simulate_prompt_answer(2, cx);
4625 cx.foreground().run_until_parked();
4626 // There's a prompt to save item 1.
4627 pane.read_with(cx, |pane, _| {
4628 assert_eq!(pane.items_len(), 4);
4629 assert_eq!(pane.active_item().unwrap().id(), item1.id());
4630 });
4631 // Confirm saving item 1.
4632 window.simulate_prompt_answer(0, cx);
4633 cx.foreground().run_until_parked();
4634
4635 // Item 1 is saved. There's a prompt to save item 3.
4636 pane.read_with(cx, |pane, cx| {
4637 assert_eq!(item1.read(cx).save_count, 1);
4638 assert_eq!(item1.read(cx).save_as_count, 0);
4639 assert_eq!(item1.read(cx).reload_count, 0);
4640 assert_eq!(pane.items_len(), 3);
4641 assert_eq!(pane.active_item().unwrap().id(), item3.id());
4642 });
4643 assert!(window.has_pending_prompt(cx));
4644
4645 // Cancel saving item 3.
4646 window.simulate_prompt_answer(1, cx);
4647 cx.foreground().run_until_parked();
4648
4649 // Item 3 is reloaded. There's a prompt to save item 4.
4650 pane.read_with(cx, |pane, cx| {
4651 assert_eq!(item3.read(cx).save_count, 0);
4652 assert_eq!(item3.read(cx).save_as_count, 0);
4653 assert_eq!(item3.read(cx).reload_count, 1);
4654 assert_eq!(pane.items_len(), 2);
4655 assert_eq!(pane.active_item().unwrap().id(), item4.id());
4656 });
4657 assert!(window.has_pending_prompt(cx));
4658
4659 // Confirm saving item 4.
4660 window.simulate_prompt_answer(0, cx);
4661 cx.foreground().run_until_parked();
4662
4663 // There's a prompt for a path for item 4.
4664 cx.simulate_new_path_selection(|_| Some(Default::default()));
4665 close_items.await.unwrap();
4666
4667 // The requested items are closed.
4668 pane.read_with(cx, |pane, cx| {
4669 assert_eq!(item4.read(cx).save_count, 0);
4670 assert_eq!(item4.read(cx).save_as_count, 1);
4671 assert_eq!(item4.read(cx).reload_count, 0);
4672 assert_eq!(pane.items_len(), 1);
4673 assert_eq!(pane.active_item().unwrap().id(), item2.id());
4674 });
4675 }
4676
4677 #[gpui::test]
4678 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
4679 init_test(cx);
4680
4681 let fs = FakeFs::new(cx.background());
4682
4683 let project = Project::test(fs, [], cx).await;
4684 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
4685 let workspace = window.root(cx);
4686
4687 // Create several workspace items with single project entries, and two
4688 // workspace items with multiple project entries.
4689 let single_entry_items = (0..=4)
4690 .map(|project_entry_id| {
4691 window.add_view(cx, |cx| {
4692 TestItem::new()
4693 .with_dirty(true)
4694 .with_project_items(&[TestProjectItem::new(
4695 project_entry_id,
4696 &format!("{project_entry_id}.txt"),
4697 cx,
4698 )])
4699 })
4700 })
4701 .collect::<Vec<_>>();
4702 let item_2_3 = window.add_view(cx, |cx| {
4703 TestItem::new()
4704 .with_dirty(true)
4705 .with_singleton(false)
4706 .with_project_items(&[
4707 single_entry_items[2].read(cx).project_items[0].clone(),
4708 single_entry_items[3].read(cx).project_items[0].clone(),
4709 ])
4710 });
4711 let item_3_4 = window.add_view(cx, |cx| {
4712 TestItem::new()
4713 .with_dirty(true)
4714 .with_singleton(false)
4715 .with_project_items(&[
4716 single_entry_items[3].read(cx).project_items[0].clone(),
4717 single_entry_items[4].read(cx).project_items[0].clone(),
4718 ])
4719 });
4720
4721 // Create two panes that contain the following project entries:
4722 // left pane:
4723 // multi-entry items: (2, 3)
4724 // single-entry items: 0, 1, 2, 3, 4
4725 // right pane:
4726 // single-entry items: 1
4727 // multi-entry items: (3, 4)
4728 let left_pane = workspace.update(cx, |workspace, cx| {
4729 let left_pane = workspace.active_pane().clone();
4730 workspace.add_item(Box::new(item_2_3.clone()), cx);
4731 for item in single_entry_items {
4732 workspace.add_item(Box::new(item), cx);
4733 }
4734 left_pane.update(cx, |pane, cx| {
4735 pane.activate_item(2, true, true, cx);
4736 });
4737
4738 workspace
4739 .split_and_clone(left_pane.clone(), SplitDirection::Right, cx)
4740 .unwrap();
4741
4742 left_pane
4743 });
4744
4745 //Need to cause an effect flush in order to respect new focus
4746 workspace.update(cx, |workspace, cx| {
4747 workspace.add_item(Box::new(item_3_4.clone()), cx);
4748 cx.focus(&left_pane);
4749 });
4750
4751 // When closing all of the items in the left pane, we should be prompted twice:
4752 // once for project entry 0, and once for project entry 2. After those two
4753 // prompts, the task should complete.
4754
4755 let close = left_pane.update(cx, |pane, cx| {
4756 pane.close_items(cx, SaveIntent::Close, move |_| true)
4757 });
4758 cx.foreground().run_until_parked();
4759 // Discard "Save all" prompt
4760 window.simulate_prompt_answer(2, cx);
4761
4762 cx.foreground().run_until_parked();
4763 left_pane.read_with(cx, |pane, cx| {
4764 assert_eq!(
4765 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
4766 &[ProjectEntryId::from_proto(0)]
4767 );
4768 });
4769 window.simulate_prompt_answer(0, cx);
4770
4771 cx.foreground().run_until_parked();
4772 left_pane.read_with(cx, |pane, cx| {
4773 assert_eq!(
4774 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
4775 &[ProjectEntryId::from_proto(2)]
4776 );
4777 });
4778 window.simulate_prompt_answer(0, cx);
4779
4780 cx.foreground().run_until_parked();
4781 close.await.unwrap();
4782 left_pane.read_with(cx, |pane, _| {
4783 assert_eq!(pane.items_len(), 0);
4784 });
4785 }
4786
4787 #[gpui::test]
4788 async fn test_autosave(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
4789 init_test(cx);
4790
4791 let fs = FakeFs::new(cx.background());
4792
4793 let project = Project::test(fs, [], cx).await;
4794 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
4795 let workspace = window.root(cx);
4796 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4797
4798 let item = window.add_view(cx, |cx| {
4799 TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4800 });
4801 let item_id = item.id();
4802 workspace.update(cx, |workspace, cx| {
4803 workspace.add_item(Box::new(item.clone()), cx);
4804 });
4805
4806 // Autosave on window change.
4807 item.update(cx, |item, cx| {
4808 cx.update_global(|settings: &mut SettingsStore, cx| {
4809 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4810 settings.autosave = Some(AutosaveSetting::OnWindowChange);
4811 })
4812 });
4813 item.is_dirty = true;
4814 });
4815
4816 // Deactivating the window saves the file.
4817 window.simulate_deactivation(cx);
4818 deterministic.run_until_parked();
4819 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
4820
4821 // Autosave on focus change.
4822 item.update(cx, |item, cx| {
4823 cx.focus_self();
4824 cx.update_global(|settings: &mut SettingsStore, cx| {
4825 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4826 settings.autosave = Some(AutosaveSetting::OnFocusChange);
4827 })
4828 });
4829 item.is_dirty = true;
4830 });
4831
4832 // Blurring the item saves the file.
4833 item.update(cx, |_, cx| cx.blur());
4834 deterministic.run_until_parked();
4835 item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
4836
4837 // Deactivating the window still saves the file.
4838 window.simulate_activation(cx);
4839 item.update(cx, |item, cx| {
4840 cx.focus_self();
4841 item.is_dirty = true;
4842 });
4843 window.simulate_deactivation(cx);
4844
4845 deterministic.run_until_parked();
4846 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
4847
4848 // Autosave after delay.
4849 item.update(cx, |item, cx| {
4850 cx.update_global(|settings: &mut SettingsStore, cx| {
4851 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4852 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
4853 })
4854 });
4855 item.is_dirty = true;
4856 cx.emit(TestItemEvent::Edit);
4857 });
4858
4859 // Delay hasn't fully expired, so the file is still dirty and unsaved.
4860 deterministic.advance_clock(Duration::from_millis(250));
4861 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
4862
4863 // After delay expires, the file is saved.
4864 deterministic.advance_clock(Duration::from_millis(250));
4865 item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
4866
4867 // Autosave on focus change, ensuring closing the tab counts as such.
4868 item.update(cx, |item, cx| {
4869 cx.update_global(|settings: &mut SettingsStore, cx| {
4870 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4871 settings.autosave = Some(AutosaveSetting::OnFocusChange);
4872 })
4873 });
4874 item.is_dirty = true;
4875 });
4876
4877 pane.update(cx, |pane, cx| {
4878 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
4879 })
4880 .await
4881 .unwrap();
4882 assert!(!window.has_pending_prompt(cx));
4883 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
4884
4885 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
4886 workspace.update(cx, |workspace, cx| {
4887 workspace.add_item(Box::new(item.clone()), cx);
4888 });
4889 item.update(cx, |item, cx| {
4890 item.project_items[0].update(cx, |item, _| {
4891 item.entry_id = None;
4892 });
4893 item.is_dirty = true;
4894 cx.blur();
4895 });
4896 deterministic.run_until_parked();
4897 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
4898
4899 // Ensure autosave is prevented for deleted files also when closing the buffer.
4900 let _close_items = pane.update(cx, |pane, cx| {
4901 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
4902 });
4903 deterministic.run_until_parked();
4904 assert!(window.has_pending_prompt(cx));
4905 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
4906 }
4907
4908 #[gpui::test]
4909 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
4910 init_test(cx);
4911
4912 let fs = FakeFs::new(cx.background());
4913
4914 let project = Project::test(fs, [], cx).await;
4915 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
4916 let workspace = window.root(cx);
4917
4918 let item = window.add_view(cx, |cx| {
4919 TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4920 });
4921 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4922 let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone());
4923 let toolbar_notify_count = Rc::new(RefCell::new(0));
4924
4925 workspace.update(cx, |workspace, cx| {
4926 workspace.add_item(Box::new(item.clone()), cx);
4927 let toolbar_notification_count = toolbar_notify_count.clone();
4928 cx.observe(&toolbar, move |_, _, _| {
4929 *toolbar_notification_count.borrow_mut() += 1
4930 })
4931 .detach();
4932 });
4933
4934 pane.read_with(cx, |pane, _| {
4935 assert!(!pane.can_navigate_backward());
4936 assert!(!pane.can_navigate_forward());
4937 });
4938
4939 item.update(cx, |item, cx| {
4940 item.set_state("one".to_string(), cx);
4941 });
4942
4943 // Toolbar must be notified to re-render the navigation buttons
4944 assert_eq!(*toolbar_notify_count.borrow(), 1);
4945
4946 pane.read_with(cx, |pane, _| {
4947 assert!(pane.can_navigate_backward());
4948 assert!(!pane.can_navigate_forward());
4949 });
4950
4951 workspace
4952 .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
4953 .await
4954 .unwrap();
4955
4956 assert_eq!(*toolbar_notify_count.borrow(), 3);
4957 pane.read_with(cx, |pane, _| {
4958 assert!(!pane.can_navigate_backward());
4959 assert!(pane.can_navigate_forward());
4960 });
4961 }
4962
4963 #[gpui::test]
4964 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
4965 init_test(cx);
4966 let fs = FakeFs::new(cx.background());
4967
4968 let project = Project::test(fs, [], cx).await;
4969 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
4970 let workspace = window.root(cx);
4971
4972 let panel = workspace.update(cx, |workspace, cx| {
4973 let panel = cx.add_view(|_| TestPanel::new(DockPosition::Right));
4974 workspace.add_panel(panel.clone(), cx);
4975
4976 workspace
4977 .right_dock()
4978 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
4979
4980 panel
4981 });
4982
4983 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4984 pane.update(cx, |pane, cx| {
4985 let item = cx.add_view(|_| TestItem::new());
4986 pane.add_item(Box::new(item), true, true, None, cx);
4987 });
4988
4989 // Transfer focus from center to panel
4990 workspace.update(cx, |workspace, cx| {
4991 workspace.toggle_panel_focus::<TestPanel>(cx);
4992 });
4993
4994 workspace.read_with(cx, |workspace, cx| {
4995 assert!(workspace.right_dock().read(cx).is_open());
4996 assert!(!panel.is_zoomed(cx));
4997 assert!(panel.has_focus(cx));
4998 });
4999
5000 // Transfer focus from panel to center
5001 workspace.update(cx, |workspace, cx| {
5002 workspace.toggle_panel_focus::<TestPanel>(cx);
5003 });
5004
5005 workspace.read_with(cx, |workspace, cx| {
5006 assert!(workspace.right_dock().read(cx).is_open());
5007 assert!(!panel.is_zoomed(cx));
5008 assert!(!panel.has_focus(cx));
5009 });
5010
5011 // Close the dock
5012 workspace.update(cx, |workspace, cx| {
5013 workspace.toggle_dock(DockPosition::Right, cx);
5014 });
5015
5016 workspace.read_with(cx, |workspace, cx| {
5017 assert!(!workspace.right_dock().read(cx).is_open());
5018 assert!(!panel.is_zoomed(cx));
5019 assert!(!panel.has_focus(cx));
5020 });
5021
5022 // Open the dock
5023 workspace.update(cx, |workspace, cx| {
5024 workspace.toggle_dock(DockPosition::Right, cx);
5025 });
5026
5027 workspace.read_with(cx, |workspace, cx| {
5028 assert!(workspace.right_dock().read(cx).is_open());
5029 assert!(!panel.is_zoomed(cx));
5030 assert!(panel.has_focus(cx));
5031 });
5032
5033 // Focus and zoom panel
5034 panel.update(cx, |panel, cx| {
5035 cx.focus_self();
5036 panel.set_zoomed(true, cx)
5037 });
5038
5039 workspace.read_with(cx, |workspace, cx| {
5040 assert!(workspace.right_dock().read(cx).is_open());
5041 assert!(panel.is_zoomed(cx));
5042 assert!(panel.has_focus(cx));
5043 });
5044
5045 // Transfer focus to the center closes the dock
5046 workspace.update(cx, |workspace, cx| {
5047 workspace.toggle_panel_focus::<TestPanel>(cx);
5048 });
5049
5050 workspace.read_with(cx, |workspace, cx| {
5051 assert!(!workspace.right_dock().read(cx).is_open());
5052 assert!(panel.is_zoomed(cx));
5053 assert!(!panel.has_focus(cx));
5054 });
5055
5056 // Transferring focus back to the panel keeps it zoomed
5057 workspace.update(cx, |workspace, cx| {
5058 workspace.toggle_panel_focus::<TestPanel>(cx);
5059 });
5060
5061 workspace.read_with(cx, |workspace, cx| {
5062 assert!(workspace.right_dock().read(cx).is_open());
5063 assert!(panel.is_zoomed(cx));
5064 assert!(panel.has_focus(cx));
5065 });
5066
5067 // Close the dock while it is zoomed
5068 workspace.update(cx, |workspace, cx| {
5069 workspace.toggle_dock(DockPosition::Right, cx)
5070 });
5071
5072 workspace.read_with(cx, |workspace, cx| {
5073 assert!(!workspace.right_dock().read(cx).is_open());
5074 assert!(panel.is_zoomed(cx));
5075 assert!(workspace.zoomed.is_none());
5076 assert!(!panel.has_focus(cx));
5077 });
5078
5079 // Opening the dock, when it's zoomed, retains focus
5080 workspace.update(cx, |workspace, cx| {
5081 workspace.toggle_dock(DockPosition::Right, cx)
5082 });
5083
5084 workspace.read_with(cx, |workspace, cx| {
5085 assert!(workspace.right_dock().read(cx).is_open());
5086 assert!(panel.is_zoomed(cx));
5087 assert!(workspace.zoomed.is_some());
5088 assert!(panel.has_focus(cx));
5089 });
5090
5091 // Unzoom and close the panel, zoom the active pane.
5092 panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
5093 workspace.update(cx, |workspace, cx| {
5094 workspace.toggle_dock(DockPosition::Right, cx)
5095 });
5096 pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
5097
5098 // Opening a dock unzooms the pane.
5099 workspace.update(cx, |workspace, cx| {
5100 workspace.toggle_dock(DockPosition::Right, cx)
5101 });
5102 workspace.read_with(cx, |workspace, cx| {
5103 let pane = pane.read(cx);
5104 assert!(!pane.is_zoomed());
5105 assert!(!pane.has_focus());
5106 assert!(workspace.right_dock().read(cx).is_open());
5107 assert!(workspace.zoomed.is_none());
5108 });
5109 }
5110
5111 #[gpui::test]
5112 async fn test_panels(cx: &mut gpui::TestAppContext) {
5113 init_test(cx);
5114 let fs = FakeFs::new(cx.background());
5115
5116 let project = Project::test(fs, [], cx).await;
5117 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
5118 let workspace = window.root(cx);
5119
5120 let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
5121 // Add panel_1 on the left, panel_2 on the right.
5122 let panel_1 = cx.add_view(|_| TestPanel::new(DockPosition::Left));
5123 workspace.add_panel(panel_1.clone(), cx);
5124 workspace
5125 .left_dock()
5126 .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
5127 let panel_2 = cx.add_view(|_| TestPanel::new(DockPosition::Right));
5128 workspace.add_panel(panel_2.clone(), cx);
5129 workspace
5130 .right_dock()
5131 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
5132
5133 let left_dock = workspace.left_dock();
5134 assert_eq!(
5135 left_dock.read(cx).visible_panel().unwrap().id(),
5136 panel_1.id()
5137 );
5138 assert_eq!(
5139 left_dock.read(cx).active_panel_size(cx).unwrap(),
5140 panel_1.size(cx)
5141 );
5142
5143 left_dock.update(cx, |left_dock, cx| {
5144 left_dock.resize_active_panel(Some(1337.), cx)
5145 });
5146 assert_eq!(
5147 workspace
5148 .right_dock()
5149 .read(cx)
5150 .visible_panel()
5151 .unwrap()
5152 .id(),
5153 panel_2.id()
5154 );
5155
5156 (panel_1, panel_2)
5157 });
5158
5159 // Move panel_1 to the right
5160 panel_1.update(cx, |panel_1, cx| {
5161 panel_1.set_position(DockPosition::Right, cx)
5162 });
5163
5164 workspace.update(cx, |workspace, cx| {
5165 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
5166 // Since it was the only panel on the left, the left dock should now be closed.
5167 assert!(!workspace.left_dock().read(cx).is_open());
5168 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
5169 let right_dock = workspace.right_dock();
5170 assert_eq!(
5171 right_dock.read(cx).visible_panel().unwrap().id(),
5172 panel_1.id()
5173 );
5174 assert_eq!(right_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
5175
5176 // Now we move panel_2Β to the left
5177 panel_2.set_position(DockPosition::Left, cx);
5178 });
5179
5180 workspace.update(cx, |workspace, cx| {
5181 // Since panel_2 was not visible on the right, we don't open the left dock.
5182 assert!(!workspace.left_dock().read(cx).is_open());
5183 // And the right dock is unaffected in it's displaying of panel_1
5184 assert!(workspace.right_dock().read(cx).is_open());
5185 assert_eq!(
5186 workspace
5187 .right_dock()
5188 .read(cx)
5189 .visible_panel()
5190 .unwrap()
5191 .id(),
5192 panel_1.id()
5193 );
5194 });
5195
5196 // Move panel_1 back to the left
5197 panel_1.update(cx, |panel_1, cx| {
5198 panel_1.set_position(DockPosition::Left, cx)
5199 });
5200
5201 workspace.update(cx, |workspace, cx| {
5202 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
5203 let left_dock = workspace.left_dock();
5204 assert!(left_dock.read(cx).is_open());
5205 assert_eq!(
5206 left_dock.read(cx).visible_panel().unwrap().id(),
5207 panel_1.id()
5208 );
5209 assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
5210 // And right the dock should be closed as it no longer has any panels.
5211 assert!(!workspace.right_dock().read(cx).is_open());
5212
5213 // Now we move panel_1 to the bottom
5214 panel_1.set_position(DockPosition::Bottom, cx);
5215 });
5216
5217 workspace.update(cx, |workspace, cx| {
5218 // Since panel_1 was visible on the left, we close the left dock.
5219 assert!(!workspace.left_dock().read(cx).is_open());
5220 // The bottom dock is sized based on the panel's default size,
5221 // since the panel orientation changed from vertical to horizontal.
5222 let bottom_dock = workspace.bottom_dock();
5223 assert_eq!(
5224 bottom_dock.read(cx).active_panel_size(cx).unwrap(),
5225 panel_1.size(cx),
5226 );
5227 // Close bottom dock and move panel_1 back to the left.
5228 bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
5229 panel_1.set_position(DockPosition::Left, cx);
5230 });
5231
5232 // Emit activated event on panel 1
5233 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Activated));
5234
5235 // Now the left dock is open and panel_1 is active and focused.
5236 workspace.read_with(cx, |workspace, cx| {
5237 let left_dock = workspace.left_dock();
5238 assert!(left_dock.read(cx).is_open());
5239 assert_eq!(
5240 left_dock.read(cx).visible_panel().unwrap().id(),
5241 panel_1.id()
5242 );
5243 assert!(panel_1.is_focused(cx));
5244 });
5245
5246 // Emit closed event on panel 2, which is not active
5247 panel_2.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed));
5248
5249 // Wo don't close the left dock, because panel_2 wasn't the active panel
5250 workspace.read_with(cx, |workspace, cx| {
5251 let left_dock = workspace.left_dock();
5252 assert!(left_dock.read(cx).is_open());
5253 assert_eq!(
5254 left_dock.read(cx).visible_panel().unwrap().id(),
5255 panel_1.id()
5256 );
5257 });
5258
5259 // Emitting a ZoomIn event shows the panel as zoomed.
5260 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomIn));
5261 workspace.read_with(cx, |workspace, _| {
5262 assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
5263 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
5264 });
5265
5266 // Move panel to another dock while it is zoomed
5267 panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
5268 workspace.read_with(cx, |workspace, _| {
5269 assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
5270 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5271 });
5272
5273 // If focus is transferred to another view that's not a panel or another pane, we still show
5274 // the panel as zoomed.
5275 let focus_receiver = window.add_view(cx, |_| EmptyView);
5276 focus_receiver.update(cx, |_, cx| cx.focus_self());
5277 workspace.read_with(cx, |workspace, _| {
5278 assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
5279 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5280 });
5281
5282 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
5283 workspace.update(cx, |_, cx| cx.focus_self());
5284 workspace.read_with(cx, |workspace, _| {
5285 assert_eq!(workspace.zoomed, None);
5286 assert_eq!(workspace.zoomed_position, None);
5287 });
5288
5289 // If focus is transferred again to another view that's not a panel or a pane, we won't
5290 // show the panel as zoomed because it wasn't zoomed before.
5291 focus_receiver.update(cx, |_, cx| cx.focus_self());
5292 workspace.read_with(cx, |workspace, _| {
5293 assert_eq!(workspace.zoomed, None);
5294 assert_eq!(workspace.zoomed_position, None);
5295 });
5296
5297 // When focus is transferred back to the panel, it is zoomed again.
5298 panel_1.update(cx, |_, cx| cx.focus_self());
5299 workspace.read_with(cx, |workspace, _| {
5300 assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
5301 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5302 });
5303
5304 // Emitting a ZoomOut event unzooms the panel.
5305 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomOut));
5306 workspace.read_with(cx, |workspace, _| {
5307 assert_eq!(workspace.zoomed, None);
5308 assert_eq!(workspace.zoomed_position, None);
5309 });
5310
5311 // Emit closed event on panel 1, which is active
5312 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed));
5313
5314 // Now the left dock is closed, because panel_1 was the active panel
5315 workspace.read_with(cx, |workspace, cx| {
5316 let right_dock = workspace.right_dock();
5317 assert!(!right_dock.read(cx).is_open());
5318 });
5319 }
5320
5321 pub fn init_test(cx: &mut TestAppContext) {
5322 cx.foreground().forbid_parking();
5323 cx.update(|cx| {
5324 cx.set_global(SettingsStore::test(cx));
5325 theme::init((), cx);
5326 language::init(cx);
5327 crate::init_settings(cx);
5328 Project::init_settings(cx);
5329 });
5330 }
5331}