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