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