1pub mod dock;
2pub mod item;
3pub mod notifications;
4pub mod pane;
5pub mod pane_group;
6mod persistence;
7pub mod searchable;
8pub mod shared_screen;
9mod status_bar;
10mod toolbar;
11mod workspace_settings;
12
13use anyhow::{anyhow, Context, Result};
14use call::ActiveCall;
15use channel::ChannelStore;
16use client::{
17 proto::{self, PeerId},
18 Client, TypedEnvelope, UserStore,
19};
20use collections::{hash_map, HashMap, HashSet};
21use drag_and_drop::DragAndDrop;
22use futures::{
23 channel::{mpsc, oneshot},
24 future::try_join_all,
25 FutureExt, StreamExt,
26};
27use gpui::{
28 actions,
29 elements::*,
30 geometry::{
31 rect::RectF,
32 vector::{vec2f, Vector2F},
33 },
34 impl_actions,
35 platform::{
36 CursorStyle, ModifiersChangedEvent, MouseButton, PathPromptOptions, Platform, PromptLevel,
37 WindowBounds, WindowOptions,
38 },
39 AnyModelHandle, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity,
40 ModelContext, ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle,
41 WeakViewHandle, WindowContext, WindowHandle,
42};
43use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem};
44use itertools::Itertools;
45use language::{LanguageRegistry, Rope};
46use std::{
47 any::TypeId,
48 borrow::Cow,
49 cmp, env,
50 future::Future,
51 path::{Path, PathBuf},
52 rc::Rc,
53 str,
54 sync::{atomic::AtomicUsize, Arc},
55 time::Duration,
56};
57
58use crate::{
59 notifications::{simple_message_notification::MessageNotification, NotificationTracker},
60 persistence::model::{
61 DockData, DockStructure, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
62 },
63};
64use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle};
65use lazy_static::lazy_static;
66use notifications::{NotificationHandle, NotifyResultExt};
67pub use pane::*;
68pub use pane_group::*;
69use persistence::{model::SerializedItem, DB};
70pub use persistence::{
71 model::{ItemId, WorkspaceLocation},
72 WorkspaceDb, DB as WORKSPACE_DB,
73};
74use postage::prelude::Stream;
75use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
76use serde::Deserialize;
77use shared_screen::SharedScreen;
78use status_bar::StatusBar;
79pub use status_bar::StatusItemView;
80use theme::{Theme, ThemeSettings};
81pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
82use util::{async_iife, ResultExt};
83pub use workspace_settings::{AutosaveSetting, GitGutterSetting, WorkspaceSettings};
84
85lazy_static! {
86 static ref ZED_WINDOW_SIZE: Option<Vector2F> = env::var("ZED_WINDOW_SIZE")
87 .ok()
88 .as_deref()
89 .and_then(parse_pixel_position_env_var);
90 static ref ZED_WINDOW_POSITION: Option<Vector2F> = env::var("ZED_WINDOW_POSITION")
91 .ok()
92 .as_deref()
93 .and_then(parse_pixel_position_env_var);
94}
95
96pub trait Modal: View {
97 fn has_focus(&self) -> bool;
98 fn dismiss_on_event(event: &Self::Event) -> bool;
99}
100
101trait ModalHandle {
102 fn as_any(&self) -> &AnyViewHandle;
103 fn has_focus(&self, cx: &WindowContext) -> bool;
104}
105
106impl<T: Modal> ModalHandle for ViewHandle<T> {
107 fn as_any(&self) -> &AnyViewHandle {
108 self
109 }
110
111 fn has_focus(&self, cx: &WindowContext) -> bool {
112 self.read(cx).has_focus()
113 }
114}
115
116#[derive(Clone, PartialEq)]
117pub struct RemoveWorktreeFromProject(pub WorktreeId);
118
119actions!(
120 workspace,
121 [
122 Open,
123 NewFile,
124 NewWindow,
125 CloseWindow,
126 CloseInactiveTabsAndPanes,
127 AddFolderToProject,
128 Unfollow,
129 SaveAs,
130 ReloadActiveItem,
131 ActivatePreviousPane,
132 ActivateNextPane,
133 FollowNextCollaborator,
134 NewTerminal,
135 NewCenterTerminal,
136 ToggleTerminalFocus,
137 NewSearch,
138 Feedback,
139 Restart,
140 Welcome,
141 ToggleZoom,
142 ToggleLeftDock,
143 ToggleRightDock,
144 ToggleBottomDock,
145 CloseAllDocks,
146 ]
147);
148
149#[derive(Clone, PartialEq)]
150pub struct OpenPaths {
151 pub paths: Vec<PathBuf>,
152}
153
154#[derive(Clone, Deserialize, PartialEq)]
155pub struct ActivatePane(pub usize);
156
157#[derive(Clone, Deserialize, PartialEq)]
158pub struct ActivatePaneInDirection(pub SplitDirection);
159
160#[derive(Clone, Deserialize, PartialEq)]
161pub struct SwapPaneInDirection(pub SplitDirection);
162
163#[derive(Clone, Deserialize, PartialEq)]
164pub struct NewFileInDirection(pub SplitDirection);
165
166#[derive(Clone, PartialEq, Debug, Deserialize)]
167#[serde(rename_all = "camelCase")]
168pub struct SaveAll {
169 pub save_intent: Option<SaveIntent>,
170}
171
172#[derive(Clone, PartialEq, Debug, Deserialize)]
173#[serde(rename_all = "camelCase")]
174pub struct Save {
175 pub save_intent: Option<SaveIntent>,
176}
177
178#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
179#[serde(rename_all = "camelCase")]
180pub struct CloseAllItemsAndPanes {
181 pub save_intent: Option<SaveIntent>,
182}
183
184#[derive(Deserialize)]
185pub struct Toast {
186 id: usize,
187 msg: Cow<'static, str>,
188 #[serde(skip)]
189 on_click: Option<(Cow<'static, str>, Arc<dyn Fn(&mut WindowContext)>)>,
190}
191
192impl Toast {
193 pub fn new<I: Into<Cow<'static, str>>>(id: usize, msg: I) -> Self {
194 Toast {
195 id,
196 msg: msg.into(),
197 on_click: None,
198 }
199 }
200
201 pub fn on_click<F, M>(mut self, message: M, on_click: F) -> Self
202 where
203 M: Into<Cow<'static, str>>,
204 F: Fn(&mut WindowContext) + 'static,
205 {
206 self.on_click = Some((message.into(), Arc::new(on_click)));
207 self
208 }
209}
210
211impl PartialEq for Toast {
212 fn eq(&self, other: &Self) -> bool {
213 self.id == other.id
214 && self.msg == other.msg
215 && self.on_click.is_some() == other.on_click.is_some()
216 }
217}
218
219impl Clone for Toast {
220 fn clone(&self) -> Self {
221 Toast {
222 id: self.id,
223 msg: self.msg.to_owned(),
224 on_click: self.on_click.clone(),
225 }
226 }
227}
228
229#[derive(Clone, Deserialize, PartialEq)]
230pub struct OpenTerminal {
231 pub working_directory: PathBuf,
232}
233
234impl_actions!(
235 workspace,
236 [
237 ActivatePane,
238 ActivatePaneInDirection,
239 SwapPaneInDirection,
240 NewFileInDirection,
241 Toast,
242 OpenTerminal,
243 SaveAll,
244 Save,
245 CloseAllItemsAndPanes,
246 ]
247);
248
249pub type WorkspaceId = i64;
250
251pub fn init_settings(cx: &mut AppContext) {
252 settings::register::<WorkspaceSettings>(cx);
253 settings::register::<item::ItemSettings>(cx);
254}
255
256pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
257 init_settings(cx);
258 pane::init(cx);
259 notifications::init(cx);
260
261 cx.add_global_action({
262 let app_state = Arc::downgrade(&app_state);
263 move |_: &Open, cx: &mut AppContext| {
264 let mut paths = cx.prompt_for_paths(PathPromptOptions {
265 files: true,
266 directories: true,
267 multiple: true,
268 });
269
270 if let Some(app_state) = app_state.upgrade() {
271 cx.spawn(move |mut cx| async move {
272 if let Some(paths) = paths.recv().await.flatten() {
273 cx.update(|cx| {
274 open_paths(&paths, &app_state, None, cx).detach_and_log_err(cx)
275 });
276 }
277 })
278 .detach();
279 }
280 }
281 });
282 cx.add_async_action(Workspace::open);
283
284 cx.add_async_action(Workspace::follow_next_collaborator);
285 cx.add_async_action(Workspace::close);
286 cx.add_async_action(Workspace::close_inactive_items_and_panes);
287 cx.add_async_action(Workspace::close_all_items_and_panes);
288 cx.add_global_action(Workspace::close_global);
289 cx.add_global_action(restart);
290 cx.add_async_action(Workspace::save_all);
291 cx.add_action(Workspace::add_folder_to_project);
292 cx.add_action(
293 |workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext<Workspace>| {
294 let pane = workspace.active_pane().clone();
295 workspace.unfollow(&pane, cx);
296 },
297 );
298 cx.add_action(
299 |workspace: &mut Workspace, action: &Save, cx: &mut ViewContext<Workspace>| {
300 workspace
301 .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx)
302 .detach_and_log_err(cx);
303 },
304 );
305 cx.add_action(
306 |workspace: &mut Workspace, _: &SaveAs, cx: &mut ViewContext<Workspace>| {
307 workspace
308 .save_active_item(SaveIntent::SaveAs, cx)
309 .detach_and_log_err(cx);
310 },
311 );
312 cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| {
313 workspace.activate_previous_pane(cx)
314 });
315 cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| {
316 workspace.activate_next_pane(cx)
317 });
318
319 cx.add_action(
320 |workspace: &mut Workspace, action: &ActivatePaneInDirection, cx| {
321 workspace.activate_pane_in_direction(action.0, cx)
322 },
323 );
324
325 cx.add_action(
326 |workspace: &mut Workspace, action: &SwapPaneInDirection, cx| {
327 workspace.swap_pane_in_direction(action.0, cx)
328 },
329 );
330
331 cx.add_action(|workspace: &mut Workspace, _: &ToggleLeftDock, cx| {
332 workspace.toggle_dock(DockPosition::Left, cx);
333 });
334 cx.add_action(|workspace: &mut Workspace, _: &ToggleRightDock, cx| {
335 workspace.toggle_dock(DockPosition::Right, cx);
336 });
337 cx.add_action(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| {
338 workspace.toggle_dock(DockPosition::Bottom, cx);
339 });
340 cx.add_action(|workspace: &mut Workspace, _: &CloseAllDocks, cx| {
341 workspace.close_all_docks(cx);
342 });
343 cx.add_action(Workspace::activate_pane_at_index);
344 cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
345 workspace.reopen_closed_item(cx).detach();
346 });
347 cx.add_action(|workspace: &mut Workspace, _: &GoBack, cx| {
348 workspace
349 .go_back(workspace.active_pane().downgrade(), cx)
350 .detach();
351 });
352 cx.add_action(|workspace: &mut Workspace, _: &GoForward, cx| {
353 workspace
354 .go_forward(workspace.active_pane().downgrade(), cx)
355 .detach();
356 });
357
358 cx.add_action(|_: &mut Workspace, _: &install_cli::Install, cx| {
359 cx.spawn(|workspace, mut cx| async move {
360 let err = install_cli::install_cli(&cx)
361 .await
362 .context("Failed to create CLI symlink");
363
364 workspace.update(&mut cx, |workspace, cx| {
365 if matches!(err, Err(_)) {
366 err.notify_err(workspace, cx);
367 } else {
368 workspace.show_notification(1, cx, |cx| {
369 cx.add_view(|_| {
370 MessageNotification::new("Successfully installed the `zed` binary")
371 })
372 });
373 }
374 })
375 })
376 .detach();
377 });
378}
379
380type ProjectItemBuilders = HashMap<
381 TypeId,
382 fn(ModelHandle<Project>, AnyModelHandle, &mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
383>;
384pub fn register_project_item<I: ProjectItem>(cx: &mut AppContext) {
385 cx.update_default_global(|builders: &mut ProjectItemBuilders, _| {
386 builders.insert(TypeId::of::<I::Item>(), |project, model, cx| {
387 let item = model.downcast::<I::Item>().unwrap();
388 Box::new(cx.add_view(|cx| I::for_project_item(project, item, cx)))
389 });
390 });
391}
392
393type FollowableItemBuilder = fn(
394 ViewHandle<Pane>,
395 ViewHandle<Workspace>,
396 ViewId,
397 &mut Option<proto::view::Variant>,
398 &mut AppContext,
399) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>;
400type FollowableItemBuilders = HashMap<
401 TypeId,
402 (
403 FollowableItemBuilder,
404 fn(&AnyViewHandle) -> Box<dyn FollowableItemHandle>,
405 ),
406>;
407pub fn register_followable_item<I: FollowableItem>(cx: &mut AppContext) {
408 cx.update_default_global(|builders: &mut FollowableItemBuilders, _| {
409 builders.insert(
410 TypeId::of::<I>(),
411 (
412 |pane, workspace, id, state, cx| {
413 I::from_state_proto(pane, workspace, id, state, cx).map(|task| {
414 cx.foreground()
415 .spawn(async move { Ok(Box::new(task.await?) as Box<_>) })
416 })
417 },
418 |this| Box::new(this.clone().downcast::<I>().unwrap()),
419 ),
420 );
421 });
422}
423
424type ItemDeserializers = HashMap<
425 Arc<str>,
426 fn(
427 ModelHandle<Project>,
428 WeakViewHandle<Workspace>,
429 WorkspaceId,
430 ItemId,
431 &mut ViewContext<Pane>,
432 ) -> Task<Result<Box<dyn ItemHandle>>>,
433>;
434pub fn register_deserializable_item<I: Item>(cx: &mut AppContext) {
435 cx.update_default_global(|deserializers: &mut ItemDeserializers, _cx| {
436 if let Some(serialized_item_kind) = I::serialized_item_kind() {
437 deserializers.insert(
438 Arc::from(serialized_item_kind),
439 |project, workspace, workspace_id, item_id, cx| {
440 let task = I::deserialize(project, workspace, workspace_id, item_id, cx);
441 cx.foreground()
442 .spawn(async { Ok(Box::new(task.await?) as Box<_>) })
443 },
444 );
445 }
446 });
447}
448
449pub struct AppState {
450 pub languages: Arc<LanguageRegistry>,
451 pub client: Arc<Client>,
452 pub user_store: ModelHandle<UserStore>,
453 pub channel_store: ModelHandle<ChannelStore>,
454 pub workspace_store: ModelHandle<WorkspaceStore>,
455 pub fs: Arc<dyn fs::Fs>,
456 pub build_window_options:
457 fn(Option<WindowBounds>, Option<uuid::Uuid>, &dyn Platform) -> WindowOptions<'static>,
458 pub initialize_workspace:
459 fn(WeakViewHandle<Workspace>, bool, Arc<AppState>, AsyncAppContext) -> Task<Result<()>>,
460 pub background_actions: BackgroundActions,
461}
462
463pub struct WorkspaceStore {
464 workspaces: HashSet<WeakViewHandle<Workspace>>,
465 followers: Vec<Follower>,
466 client: Arc<Client>,
467 _subscriptions: Vec<client::Subscription>,
468}
469
470#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
471struct Follower {
472 project_id: Option<u64>,
473 peer_id: PeerId,
474}
475
476impl AppState {
477 #[cfg(any(test, feature = "test-support"))]
478 pub fn test(cx: &mut AppContext) -> Arc<Self> {
479 use settings::SettingsStore;
480
481 if !cx.has_global::<SettingsStore>() {
482 cx.set_global(SettingsStore::test(cx));
483 }
484
485 let fs = fs::FakeFs::new(cx.background().clone());
486 let languages = Arc::new(LanguageRegistry::test());
487 let http_client = util::http::FakeHttpClient::with_404_response();
488 let client = Client::new(http_client.clone(), cx);
489 let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
490 let channel_store =
491 cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
492 let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx));
493
494 theme::init((), cx);
495 client::init(&client, cx);
496 crate::init_settings(cx);
497
498 Arc::new(Self {
499 client,
500 fs,
501 languages,
502 user_store,
503 channel_store,
504 workspace_store,
505 initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
506 build_window_options: |_, _, _| Default::default(),
507 background_actions: || &[],
508 })
509 }
510}
511
512struct DelayedDebouncedEditAction {
513 task: Option<Task<()>>,
514 cancel_channel: Option<oneshot::Sender<()>>,
515}
516
517impl DelayedDebouncedEditAction {
518 fn new() -> DelayedDebouncedEditAction {
519 DelayedDebouncedEditAction {
520 task: None,
521 cancel_channel: None,
522 }
523 }
524
525 fn fire_new<F>(&mut self, delay: Duration, cx: &mut ViewContext<Workspace>, func: F)
526 where
527 F: 'static + FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> Task<Result<()>>,
528 {
529 if let Some(channel) = self.cancel_channel.take() {
530 _ = channel.send(());
531 }
532
533 let (sender, mut receiver) = oneshot::channel::<()>();
534 self.cancel_channel = Some(sender);
535
536 let previous_task = self.task.take();
537 self.task = Some(cx.spawn(|workspace, mut cx| async move {
538 let mut timer = cx.background().timer(delay).fuse();
539 if let Some(previous_task) = previous_task {
540 previous_task.await;
541 }
542
543 futures::select_biased! {
544 _ = receiver => return,
545 _ = timer => {}
546 }
547
548 if let Some(result) = workspace
549 .update(&mut cx, |workspace, cx| (func)(workspace, cx))
550 .log_err()
551 {
552 result.await.log_err();
553 }
554 }));
555 }
556}
557
558pub enum Event {
559 PaneAdded(ViewHandle<Pane>),
560 ContactRequestedJoin(u64),
561}
562
563pub struct Workspace {
564 weak_self: WeakViewHandle<Self>,
565 modal: Option<ActiveModal>,
566 zoomed: Option<AnyWeakViewHandle>,
567 zoomed_position: Option<DockPosition>,
568 center: PaneGroup,
569 left_dock: ViewHandle<Dock>,
570 bottom_dock: ViewHandle<Dock>,
571 right_dock: ViewHandle<Dock>,
572 panes: Vec<ViewHandle<Pane>>,
573 panes_by_item: HashMap<usize, WeakViewHandle<Pane>>,
574 active_pane: ViewHandle<Pane>,
575 last_active_center_pane: Option<WeakViewHandle<Pane>>,
576 status_bar: ViewHandle<StatusBar>,
577 titlebar_item: Option<AnyViewHandle>,
578 notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
579 project: ModelHandle<Project>,
580 follower_states_by_leader: FollowerStatesByLeader,
581 last_leaders_by_pane: HashMap<WeakViewHandle<Pane>, PeerId>,
582 window_edited: bool,
583 active_call: Option<(ModelHandle<ActiveCall>, Vec<Subscription>)>,
584 leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
585 database_id: WorkspaceId,
586 app_state: Arc<AppState>,
587 subscriptions: Vec<Subscription>,
588 _apply_leader_updates: Task<Result<()>>,
589 _observe_current_user: Task<Result<()>>,
590 _schedule_serialize: Option<Task<()>>,
591 pane_history_timestamp: Arc<AtomicUsize>,
592}
593
594struct ActiveModal {
595 view: Box<dyn ModalHandle>,
596 previously_focused_view_id: Option<usize>,
597}
598
599#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
600pub struct ViewId {
601 pub creator: PeerId,
602 pub id: u64,
603}
604
605type FollowerStatesByLeader = HashMap<PeerId, HashMap<ViewHandle<Pane>, FollowerState>>;
606
607#[derive(Default)]
608struct FollowerState {
609 active_view_id: Option<ViewId>,
610 items_by_leader_view_id: HashMap<ViewId, Box<dyn FollowableItemHandle>>,
611}
612
613enum WorkspaceBounds {}
614
615impl Workspace {
616 pub fn new(
617 workspace_id: WorkspaceId,
618 project: ModelHandle<Project>,
619 app_state: Arc<AppState>,
620 cx: &mut ViewContext<Self>,
621 ) -> Self {
622 cx.observe(&project, |_, _, cx| cx.notify()).detach();
623 cx.subscribe(&project, move |this, _, event, cx| {
624 match event {
625 project::Event::RemoteIdChanged(remote_id) => {
626 this.update_window_title(cx);
627 this.project_remote_id_changed(*remote_id, 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 let mut this = Workspace {
780 weak_self: weak_handle.clone(),
781 modal: None,
782 zoomed: None,
783 zoomed_position: None,
784 center: PaneGroup::new(center_pane.clone()),
785 panes: vec![center_pane.clone()],
786 panes_by_item: Default::default(),
787 active_pane: center_pane.clone(),
788 last_active_center_pane: Some(center_pane.downgrade()),
789 status_bar,
790 titlebar_item: None,
791 notifications: Default::default(),
792 left_dock,
793 bottom_dock,
794 right_dock,
795 project: project.clone(),
796 follower_states_by_leader: Default::default(),
797 last_leaders_by_pane: Default::default(),
798 window_edited: false,
799 active_call,
800 database_id: workspace_id,
801 app_state,
802 _observe_current_user,
803 _apply_leader_updates,
804 _schedule_serialize: None,
805 leader_updates_tx,
806 subscriptions,
807 pane_history_timestamp,
808 };
809 this.project_remote_id_changed(project.read(cx).remote_id(), cx);
810 cx.defer(|this, cx| this.update_window_title(cx));
811 this
812 }
813
814 fn new_local(
815 abs_paths: Vec<PathBuf>,
816 app_state: Arc<AppState>,
817 requesting_window: Option<WindowHandle<Workspace>>,
818 cx: &mut AppContext,
819 ) -> Task<(
820 WeakViewHandle<Workspace>,
821 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
822 )> {
823 let project_handle = Project::local(
824 app_state.client.clone(),
825 app_state.user_store.clone(),
826 app_state.languages.clone(),
827 app_state.fs.clone(),
828 cx,
829 );
830
831 cx.spawn(|mut cx| async move {
832 let serialized_workspace = persistence::DB.workspace_for_roots(&abs_paths.as_slice());
833
834 let paths_to_open = Arc::new(abs_paths);
835
836 // Get project paths for all of the abs_paths
837 let mut worktree_roots: HashSet<Arc<Path>> = Default::default();
838 let mut project_paths: Vec<(PathBuf, Option<ProjectPath>)> =
839 Vec::with_capacity(paths_to_open.len());
840 for path in paths_to_open.iter().cloned() {
841 if let Some((worktree, project_entry)) = cx
842 .update(|cx| {
843 Workspace::project_path_for_path(project_handle.clone(), &path, true, cx)
844 })
845 .await
846 .log_err()
847 {
848 worktree_roots.insert(worktree.read_with(&mut cx, |tree, _| tree.abs_path()));
849 project_paths.push((path, Some(project_entry)));
850 } else {
851 project_paths.push((path, None));
852 }
853 }
854
855 let workspace_id = if let Some(serialized_workspace) = serialized_workspace.as_ref() {
856 serialized_workspace.id
857 } else {
858 DB.next_id().await.unwrap_or(0)
859 };
860
861 let window = if let Some(window) = requesting_window {
862 window.replace_root(&mut cx, |cx| {
863 Workspace::new(workspace_id, project_handle.clone(), app_state.clone(), cx)
864 });
865 window
866 } else {
867 {
868 let window_bounds_override = window_bounds_env_override(&cx);
869 let (bounds, display) = if let Some(bounds) = window_bounds_override {
870 (Some(bounds), None)
871 } else {
872 serialized_workspace
873 .as_ref()
874 .and_then(|serialized_workspace| {
875 let display = serialized_workspace.display?;
876 let mut bounds = serialized_workspace.bounds?;
877
878 // Stored bounds are relative to the containing display.
879 // So convert back to global coordinates if that screen still exists
880 if let WindowBounds::Fixed(mut window_bounds) = bounds {
881 if let Some(screen) = cx.platform().screen_by_id(display) {
882 let screen_bounds = screen.bounds();
883 window_bounds.set_origin_x(
884 window_bounds.origin_x() + screen_bounds.origin_x(),
885 );
886 window_bounds.set_origin_y(
887 window_bounds.origin_y() + screen_bounds.origin_y(),
888 );
889 bounds = WindowBounds::Fixed(window_bounds);
890 } else {
891 // Screen no longer exists. Return none here.
892 return None;
893 }
894 }
895
896 Some((bounds, display))
897 })
898 .unzip()
899 };
900
901 // Use the serialized workspace to construct the new window
902 cx.add_window(
903 (app_state.build_window_options)(bounds, display, cx.platform().as_ref()),
904 |cx| {
905 Workspace::new(
906 workspace_id,
907 project_handle.clone(),
908 app_state.clone(),
909 cx,
910 )
911 },
912 )
913 }
914 };
915
916 // We haven't yielded the main thread since obtaining the window handle,
917 // so the window exists.
918 let workspace = window.root(&cx).unwrap();
919
920 (app_state.initialize_workspace)(
921 workspace.downgrade(),
922 serialized_workspace.is_some(),
923 app_state.clone(),
924 cx.clone(),
925 )
926 .await
927 .log_err();
928
929 window.update(&mut cx, |cx| cx.activate_window());
930
931 let workspace = workspace.downgrade();
932 notify_if_database_failed(&workspace, &mut cx);
933 let opened_items = open_items(
934 serialized_workspace,
935 &workspace,
936 project_paths,
937 app_state,
938 cx,
939 )
940 .await;
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 project_remote_id_changed(&mut self, _project_id: Option<u64>, _cx: &mut ViewContext<Self>) {
2516 // TODO
2517 }
2518
2519 fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
2520 if let Some(states_by_pane) = self.follower_states_by_leader.remove(&peer_id) {
2521 for state in states_by_pane.into_values() {
2522 for item in state.items_by_leader_view_id.into_values() {
2523 item.set_leader_peer_id(None, cx);
2524 }
2525 }
2526 }
2527 cx.notify();
2528 }
2529
2530 pub fn toggle_follow(
2531 &mut self,
2532 leader_id: PeerId,
2533 cx: &mut ViewContext<Self>,
2534 ) -> Option<Task<Result<()>>> {
2535 let pane = self.active_pane().clone();
2536
2537 if let Some(prev_leader_id) = self.unfollow(&pane, cx) {
2538 if leader_id == prev_leader_id {
2539 return None;
2540 }
2541 }
2542
2543 self.last_leaders_by_pane
2544 .insert(pane.downgrade(), leader_id);
2545 self.follower_states_by_leader
2546 .entry(leader_id)
2547 .or_default()
2548 .insert(pane.clone(), Default::default());
2549 cx.notify();
2550
2551 let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
2552 let project_id = self.project.read(cx).remote_id();
2553 let request = self.app_state.client.request(proto::Follow {
2554 room_id,
2555 project_id,
2556 leader_id: Some(leader_id),
2557 });
2558
2559 Some(cx.spawn(|this, mut cx| async move {
2560 let response = request.await?;
2561 this.update(&mut cx, |this, _| {
2562 let state = this
2563 .follower_states_by_leader
2564 .get_mut(&leader_id)
2565 .and_then(|states_by_pane| states_by_pane.get_mut(&pane))
2566 .ok_or_else(|| anyhow!("following interrupted"))?;
2567 state.active_view_id = if let Some(active_view_id) = response.active_view_id {
2568 Some(ViewId::from_proto(active_view_id)?)
2569 } else {
2570 None
2571 };
2572 Ok::<_, anyhow::Error>(())
2573 })??;
2574 Self::add_views_from_leader(
2575 this.clone(),
2576 leader_id,
2577 vec![pane],
2578 response.views,
2579 &mut cx,
2580 )
2581 .await?;
2582 this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx))?;
2583 Ok(())
2584 }))
2585 }
2586
2587 pub fn follow_next_collaborator(
2588 &mut self,
2589 _: &FollowNextCollaborator,
2590 cx: &mut ViewContext<Self>,
2591 ) -> Option<Task<Result<()>>> {
2592 let collaborators = self.project.read(cx).collaborators();
2593 let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
2594 let mut collaborators = collaborators.keys().copied();
2595 for peer_id in collaborators.by_ref() {
2596 if peer_id == leader_id {
2597 break;
2598 }
2599 }
2600 collaborators.next()
2601 } else if let Some(last_leader_id) =
2602 self.last_leaders_by_pane.get(&self.active_pane.downgrade())
2603 {
2604 if collaborators.contains_key(last_leader_id) {
2605 Some(*last_leader_id)
2606 } else {
2607 None
2608 }
2609 } else {
2610 None
2611 };
2612
2613 next_leader_id
2614 .or_else(|| collaborators.keys().copied().next())
2615 .and_then(|leader_id| self.toggle_follow(leader_id, cx))
2616 }
2617
2618 pub fn unfollow(
2619 &mut self,
2620 pane: &ViewHandle<Pane>,
2621 cx: &mut ViewContext<Self>,
2622 ) -> Option<PeerId> {
2623 for (leader_id, states_by_pane) in &mut self.follower_states_by_leader {
2624 let leader_id = *leader_id;
2625 if let Some(state) = states_by_pane.remove(pane) {
2626 for (_, item) in state.items_by_leader_view_id {
2627 item.set_leader_peer_id(None, cx);
2628 }
2629
2630 if states_by_pane.is_empty() {
2631 self.follower_states_by_leader.remove(&leader_id);
2632 let project_id = self.project.read(cx).remote_id();
2633 let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
2634 self.app_state
2635 .client
2636 .send(proto::Unfollow {
2637 room_id,
2638 project_id,
2639 leader_id: Some(leader_id),
2640 })
2641 .log_err();
2642 }
2643
2644 cx.notify();
2645 return Some(leader_id);
2646 }
2647 }
2648 None
2649 }
2650
2651 pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
2652 self.follower_states_by_leader.contains_key(&peer_id)
2653 }
2654
2655 fn render_titlebar(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
2656 // TODO: There should be a better system in place for this
2657 // (https://github.com/zed-industries/zed/issues/1290)
2658 let is_fullscreen = cx.window_is_fullscreen();
2659 let container_theme = if is_fullscreen {
2660 let mut container_theme = theme.titlebar.container;
2661 container_theme.padding.left = container_theme.padding.right;
2662 container_theme
2663 } else {
2664 theme.titlebar.container
2665 };
2666
2667 enum TitleBar {}
2668 MouseEventHandler::new::<TitleBar, _>(0, cx, |_, cx| {
2669 Stack::new()
2670 .with_children(
2671 self.titlebar_item
2672 .as_ref()
2673 .map(|item| ChildView::new(item, cx)),
2674 )
2675 .contained()
2676 .with_style(container_theme)
2677 })
2678 .on_click(MouseButton::Left, |event, _, cx| {
2679 if event.click_count == 2 {
2680 cx.zoom_window();
2681 }
2682 })
2683 .constrained()
2684 .with_height(theme.titlebar.height)
2685 .into_any_named("titlebar")
2686 }
2687
2688 fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
2689 let active_entry = self.active_project_path(cx);
2690 self.project
2691 .update(cx, |project, cx| project.set_active_path(active_entry, cx));
2692 self.update_window_title(cx);
2693 }
2694
2695 fn update_window_title(&mut self, cx: &mut ViewContext<Self>) {
2696 let project = self.project().read(cx);
2697 let mut title = String::new();
2698
2699 if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
2700 let filename = path
2701 .path
2702 .file_name()
2703 .map(|s| s.to_string_lossy())
2704 .or_else(|| {
2705 Some(Cow::Borrowed(
2706 project
2707 .worktree_for_id(path.worktree_id, cx)?
2708 .read(cx)
2709 .root_name(),
2710 ))
2711 });
2712
2713 if let Some(filename) = filename {
2714 title.push_str(filename.as_ref());
2715 title.push_str(" β ");
2716 }
2717 }
2718
2719 for (i, name) in project.worktree_root_names(cx).enumerate() {
2720 if i > 0 {
2721 title.push_str(", ");
2722 }
2723 title.push_str(name);
2724 }
2725
2726 if title.is_empty() {
2727 title = "empty project".to_string();
2728 }
2729
2730 if project.is_remote() {
2731 title.push_str(" β");
2732 } else if project.is_shared() {
2733 title.push_str(" β");
2734 }
2735
2736 cx.set_window_title(&title);
2737 }
2738
2739 fn update_window_edited(&mut self, cx: &mut ViewContext<Self>) {
2740 let is_edited = !self.project.read(cx).is_read_only()
2741 && self
2742 .items(cx)
2743 .any(|item| item.has_conflict(cx) || item.is_dirty(cx));
2744 if is_edited != self.window_edited {
2745 self.window_edited = is_edited;
2746 cx.set_window_edited(self.window_edited)
2747 }
2748 }
2749
2750 fn render_disconnected_overlay(
2751 &self,
2752 cx: &mut ViewContext<Workspace>,
2753 ) -> Option<AnyElement<Workspace>> {
2754 if self.project.read(cx).is_read_only() {
2755 enum DisconnectedOverlay {}
2756 Some(
2757 MouseEventHandler::new::<DisconnectedOverlay, _>(0, cx, |_, cx| {
2758 let theme = &theme::current(cx);
2759 Label::new(
2760 "Your connection to the remote project has been lost.",
2761 theme.workspace.disconnected_overlay.text.clone(),
2762 )
2763 .aligned()
2764 .contained()
2765 .with_style(theme.workspace.disconnected_overlay.container)
2766 })
2767 .with_cursor_style(CursorStyle::Arrow)
2768 .capture_all()
2769 .into_any_named("disconnected overlay"),
2770 )
2771 } else {
2772 None
2773 }
2774 }
2775
2776 fn render_notifications(
2777 &self,
2778 theme: &theme::Workspace,
2779 cx: &AppContext,
2780 ) -> Option<AnyElement<Workspace>> {
2781 if self.notifications.is_empty() {
2782 None
2783 } else {
2784 Some(
2785 Flex::column()
2786 .with_children(self.notifications.iter().map(|(_, _, notification)| {
2787 ChildView::new(notification.as_any(), cx)
2788 .contained()
2789 .with_style(theme.notification)
2790 }))
2791 .constrained()
2792 .with_width(theme.notifications.width)
2793 .contained()
2794 .with_style(theme.notifications.container)
2795 .aligned()
2796 .bottom()
2797 .right()
2798 .into_any(),
2799 )
2800 }
2801 }
2802
2803 // RPC handlers
2804
2805 fn handle_follow(&mut self, cx: &mut ViewContext<Self>) -> proto::FollowResponse {
2806 let client = &self.app_state.client;
2807
2808 let active_view_id = self.active_item(cx).and_then(|i| {
2809 Some(
2810 i.to_followable_item_handle(cx)?
2811 .remote_id(client, cx)?
2812 .to_proto(),
2813 )
2814 });
2815
2816 cx.notify();
2817
2818 proto::FollowResponse {
2819 active_view_id,
2820 views: self
2821 .panes()
2822 .iter()
2823 .flat_map(|pane| {
2824 let leader_id = self.leader_for_pane(pane);
2825 pane.read(cx).items().filter_map({
2826 let cx = &cx;
2827 move |item| {
2828 let item = item.to_followable_item_handle(cx)?;
2829 let id = item.remote_id(client, cx)?.to_proto();
2830 let variant = item.to_state_proto(cx)?;
2831 Some(proto::View {
2832 id: Some(id),
2833 leader_id,
2834 variant: Some(variant),
2835 })
2836 }
2837 })
2838 })
2839 .collect(),
2840 }
2841 }
2842
2843 fn handle_update_followers(
2844 &mut self,
2845 leader_id: PeerId,
2846 message: proto::UpdateFollowers,
2847 _cx: &mut ViewContext<Self>,
2848 ) {
2849 self.leader_updates_tx
2850 .unbounded_send((leader_id, message))
2851 .ok();
2852 }
2853
2854 async fn process_leader_update(
2855 this: &WeakViewHandle<Self>,
2856 leader_id: PeerId,
2857 update: proto::UpdateFollowers,
2858 cx: &mut AsyncAppContext,
2859 ) -> Result<()> {
2860 match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
2861 proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
2862 this.update(cx, |this, _| {
2863 if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
2864 for state in state.values_mut() {
2865 state.active_view_id =
2866 if let Some(active_view_id) = update_active_view.id.clone() {
2867 Some(ViewId::from_proto(active_view_id)?)
2868 } else {
2869 None
2870 };
2871 }
2872 }
2873 anyhow::Ok(())
2874 })??;
2875 }
2876 proto::update_followers::Variant::UpdateView(update_view) => {
2877 let variant = update_view
2878 .variant
2879 .ok_or_else(|| anyhow!("missing update view variant"))?;
2880 let id = update_view
2881 .id
2882 .ok_or_else(|| anyhow!("missing update view id"))?;
2883 let mut tasks = Vec::new();
2884 this.update(cx, |this, cx| {
2885 let project = this.project.clone();
2886 if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
2887 for state in state.values_mut() {
2888 let view_id = ViewId::from_proto(id.clone())?;
2889 if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
2890 tasks.push(item.apply_update_proto(&project, variant.clone(), cx));
2891 }
2892 }
2893 }
2894 anyhow::Ok(())
2895 })??;
2896 try_join_all(tasks).await.log_err();
2897 }
2898 proto::update_followers::Variant::CreateView(view) => {
2899 let panes = this.read_with(cx, |this, _| {
2900 this.follower_states_by_leader
2901 .get(&leader_id)
2902 .into_iter()
2903 .flat_map(|states_by_pane| states_by_pane.keys())
2904 .cloned()
2905 .collect()
2906 })?;
2907 Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], cx).await?;
2908 }
2909 }
2910 this.update(cx, |this, cx| this.leader_updated(leader_id, cx))?;
2911 Ok(())
2912 }
2913
2914 async fn add_views_from_leader(
2915 this: WeakViewHandle<Self>,
2916 leader_id: PeerId,
2917 panes: Vec<ViewHandle<Pane>>,
2918 views: Vec<proto::View>,
2919 cx: &mut AsyncAppContext,
2920 ) -> Result<()> {
2921 let this = this
2922 .upgrade(cx)
2923 .ok_or_else(|| anyhow!("workspace dropped"))?;
2924
2925 let item_builders = cx.update(|cx| {
2926 cx.default_global::<FollowableItemBuilders>()
2927 .values()
2928 .map(|b| b.0)
2929 .collect::<Vec<_>>()
2930 });
2931
2932 let mut item_tasks_by_pane = HashMap::default();
2933 for pane in panes {
2934 let mut item_tasks = Vec::new();
2935 let mut leader_view_ids = Vec::new();
2936 for view in &views {
2937 let Some(id) = &view.id else { continue };
2938 let id = ViewId::from_proto(id.clone())?;
2939 let mut variant = view.variant.clone();
2940 if variant.is_none() {
2941 Err(anyhow!("missing view variant"))?;
2942 }
2943 for build_item in &item_builders {
2944 let task = cx
2945 .update(|cx| build_item(pane.clone(), this.clone(), id, &mut variant, cx));
2946 if let Some(task) = task {
2947 item_tasks.push(task);
2948 leader_view_ids.push(id);
2949 break;
2950 } else {
2951 assert!(variant.is_some());
2952 }
2953 }
2954 }
2955
2956 item_tasks_by_pane.insert(pane, (item_tasks, leader_view_ids));
2957 }
2958
2959 for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane {
2960 let items = futures::future::try_join_all(item_tasks).await?;
2961 this.update(cx, |this, cx| {
2962 let state = this
2963 .follower_states_by_leader
2964 .get_mut(&leader_id)?
2965 .get_mut(&pane)?;
2966
2967 for (id, item) in leader_view_ids.into_iter().zip(items) {
2968 item.set_leader_peer_id(Some(leader_id), cx);
2969 state.items_by_leader_view_id.insert(id, item);
2970 }
2971
2972 Some(())
2973 });
2974 }
2975 Ok(())
2976 }
2977
2978 fn update_active_view_for_followers(&self, cx: &AppContext) {
2979 if self.active_pane.read(cx).has_focus() {
2980 self.update_followers(
2981 proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView {
2982 id: self.active_item(cx).and_then(|item| {
2983 item.to_followable_item_handle(cx)?
2984 .remote_id(&self.app_state.client, cx)
2985 .map(|id| id.to_proto())
2986 }),
2987 leader_id: self.leader_for_pane(&self.active_pane),
2988 }),
2989 cx,
2990 );
2991 } else {
2992 self.update_followers(
2993 proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView {
2994 id: None,
2995 leader_id: None,
2996 }),
2997 cx,
2998 );
2999 }
3000 }
3001
3002 fn update_followers(
3003 &self,
3004 update: proto::update_followers::Variant,
3005 cx: &AppContext,
3006 ) -> Option<()> {
3007 self.app_state().workspace_store.read_with(cx, |store, cx| {
3008 store.update_followers(self.project.read(cx).remote_id(), update, cx)
3009 })
3010 }
3011
3012 pub fn leader_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<PeerId> {
3013 self.follower_states_by_leader
3014 .iter()
3015 .find_map(|(leader_id, state)| {
3016 if state.contains_key(pane) {
3017 Some(*leader_id)
3018 } else {
3019 None
3020 }
3021 })
3022 }
3023
3024 fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
3025 cx.notify();
3026
3027 let call = self.active_call()?;
3028 let room = call.read(cx).room()?.read(cx);
3029 let participant = room.remote_participant_for_peer_id(leader_id)?;
3030 let mut items_to_activate = Vec::new();
3031
3032 let leader_in_this_app;
3033 let leader_in_this_project;
3034 match participant.location {
3035 call::ParticipantLocation::SharedProject { project_id } => {
3036 leader_in_this_app = true;
3037 leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
3038 }
3039 call::ParticipantLocation::UnsharedProject => {
3040 leader_in_this_app = true;
3041 leader_in_this_project = false;
3042 }
3043 call::ParticipantLocation::External => {
3044 leader_in_this_app = false;
3045 leader_in_this_project = false;
3046 }
3047 };
3048
3049 for (pane, state) in self.follower_states_by_leader.get(&leader_id)? {
3050 if leader_in_this_app {
3051 let item = state
3052 .active_view_id
3053 .and_then(|id| state.items_by_leader_view_id.get(&id));
3054 if let Some(item) = item {
3055 if leader_in_this_project || !item.is_project_item(cx) {
3056 items_to_activate.push((pane.clone(), item.boxed_clone()));
3057 }
3058 continue;
3059 }
3060 }
3061 if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
3062 items_to_activate.push((pane.clone(), Box::new(shared_screen)));
3063 }
3064 }
3065
3066 for (pane, item) in items_to_activate {
3067 let pane_was_focused = pane.read(cx).has_focus();
3068 if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) {
3069 pane.update(cx, |pane, cx| pane.activate_item(index, false, false, cx));
3070 } else {
3071 pane.update(cx, |pane, cx| {
3072 pane.add_item(item.boxed_clone(), false, false, None, cx)
3073 });
3074 }
3075
3076 if pane_was_focused {
3077 pane.update(cx, |pane, cx| pane.focus_active_item(cx));
3078 }
3079 }
3080
3081 None
3082 }
3083
3084 fn shared_screen_for_peer(
3085 &self,
3086 peer_id: PeerId,
3087 pane: &ViewHandle<Pane>,
3088 cx: &mut ViewContext<Self>,
3089 ) -> Option<ViewHandle<SharedScreen>> {
3090 let call = self.active_call()?;
3091 let room = call.read(cx).room()?.read(cx);
3092 let participant = room.remote_participant_for_peer_id(peer_id)?;
3093 let track = participant.video_tracks.values().next()?.clone();
3094 let user = participant.user.clone();
3095
3096 for item in pane.read(cx).items_of_type::<SharedScreen>() {
3097 if item.read(cx).peer_id == peer_id {
3098 return Some(item);
3099 }
3100 }
3101
3102 Some(cx.add_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx)))
3103 }
3104
3105 pub fn on_window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
3106 if active {
3107 cx.background()
3108 .spawn(persistence::DB.update_timestamp(self.database_id()))
3109 .detach();
3110 } else {
3111 for pane in &self.panes {
3112 pane.update(cx, |pane, cx| {
3113 if let Some(item) = pane.active_item() {
3114 item.workspace_deactivated(cx);
3115 }
3116 if matches!(
3117 settings::get::<WorkspaceSettings>(cx).autosave,
3118 AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
3119 ) {
3120 for item in pane.items() {
3121 Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
3122 .detach_and_log_err(cx);
3123 }
3124 }
3125 });
3126 }
3127 }
3128 }
3129
3130 fn active_call(&self) -> Option<&ModelHandle<ActiveCall>> {
3131 self.active_call.as_ref().map(|(call, _)| call)
3132 }
3133
3134 fn on_active_call_event(
3135 &mut self,
3136 _: ModelHandle<ActiveCall>,
3137 event: &call::room::Event,
3138 cx: &mut ViewContext<Self>,
3139 ) {
3140 match event {
3141 call::room::Event::ParticipantLocationChanged { participant_id }
3142 | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
3143 self.leader_updated(*participant_id, cx);
3144 }
3145 _ => {}
3146 }
3147 }
3148
3149 pub fn database_id(&self) -> WorkspaceId {
3150 self.database_id
3151 }
3152
3153 fn location(&self, cx: &AppContext) -> Option<WorkspaceLocation> {
3154 let project = self.project().read(cx);
3155
3156 if project.is_local() {
3157 Some(
3158 project
3159 .visible_worktrees(cx)
3160 .map(|worktree| worktree.read(cx).abs_path())
3161 .collect::<Vec<_>>()
3162 .into(),
3163 )
3164 } else {
3165 None
3166 }
3167 }
3168
3169 fn remove_panes(&mut self, member: Member, cx: &mut ViewContext<Workspace>) {
3170 match member {
3171 Member::Axis(PaneAxis { members, .. }) => {
3172 for child in members.iter() {
3173 self.remove_panes(child.clone(), cx)
3174 }
3175 }
3176 Member::Pane(pane) => {
3177 self.force_remove_pane(&pane, cx);
3178 }
3179 }
3180 }
3181
3182 fn force_remove_pane(&mut self, pane: &ViewHandle<Pane>, cx: &mut ViewContext<Workspace>) {
3183 self.panes.retain(|p| p != pane);
3184 cx.focus(self.panes.last().unwrap());
3185 if self.last_active_center_pane == Some(pane.downgrade()) {
3186 self.last_active_center_pane = None;
3187 }
3188 cx.notify();
3189 }
3190
3191 fn schedule_serialize(&mut self, cx: &mut ViewContext<Self>) {
3192 self._schedule_serialize = Some(cx.spawn(|this, cx| async move {
3193 cx.background().timer(Duration::from_millis(100)).await;
3194 this.read_with(&cx, |this, cx| this.serialize_workspace(cx))
3195 .ok();
3196 }));
3197 }
3198
3199 fn serialize_workspace(&self, cx: &ViewContext<Self>) {
3200 fn serialize_pane_handle(
3201 pane_handle: &ViewHandle<Pane>,
3202 cx: &AppContext,
3203 ) -> SerializedPane {
3204 let (items, active) = {
3205 let pane = pane_handle.read(cx);
3206 let active_item_id = pane.active_item().map(|item| item.id());
3207 (
3208 pane.items()
3209 .filter_map(|item_handle| {
3210 Some(SerializedItem {
3211 kind: Arc::from(item_handle.serialized_item_kind()?),
3212 item_id: item_handle.id(),
3213 active: Some(item_handle.id()) == active_item_id,
3214 })
3215 })
3216 .collect::<Vec<_>>(),
3217 pane.has_focus(),
3218 )
3219 };
3220
3221 SerializedPane::new(items, active)
3222 }
3223
3224 fn build_serialized_pane_group(
3225 pane_group: &Member,
3226 cx: &AppContext,
3227 ) -> SerializedPaneGroup {
3228 match pane_group {
3229 Member::Axis(PaneAxis {
3230 axis,
3231 members,
3232 flexes,
3233 bounding_boxes: _,
3234 }) => SerializedPaneGroup::Group {
3235 axis: *axis,
3236 children: members
3237 .iter()
3238 .map(|member| build_serialized_pane_group(member, cx))
3239 .collect::<Vec<_>>(),
3240 flexes: Some(flexes.borrow().clone()),
3241 },
3242 Member::Pane(pane_handle) => {
3243 SerializedPaneGroup::Pane(serialize_pane_handle(&pane_handle, cx))
3244 }
3245 }
3246 }
3247
3248 fn build_serialized_docks(this: &Workspace, cx: &ViewContext<Workspace>) -> DockStructure {
3249 let left_dock = this.left_dock.read(cx);
3250 let left_visible = left_dock.is_open();
3251 let left_active_panel = left_dock.visible_panel().and_then(|panel| {
3252 Some(
3253 cx.view_ui_name(panel.as_any().window(), panel.id())?
3254 .to_string(),
3255 )
3256 });
3257 let left_dock_zoom = left_dock
3258 .visible_panel()
3259 .map(|panel| panel.is_zoomed(cx))
3260 .unwrap_or(false);
3261
3262 let right_dock = this.right_dock.read(cx);
3263 let right_visible = right_dock.is_open();
3264 let right_active_panel = right_dock.visible_panel().and_then(|panel| {
3265 Some(
3266 cx.view_ui_name(panel.as_any().window(), panel.id())?
3267 .to_string(),
3268 )
3269 });
3270 let right_dock_zoom = right_dock
3271 .visible_panel()
3272 .map(|panel| panel.is_zoomed(cx))
3273 .unwrap_or(false);
3274
3275 let bottom_dock = this.bottom_dock.read(cx);
3276 let bottom_visible = bottom_dock.is_open();
3277 let bottom_active_panel = bottom_dock.visible_panel().and_then(|panel| {
3278 Some(
3279 cx.view_ui_name(panel.as_any().window(), panel.id())?
3280 .to_string(),
3281 )
3282 });
3283 let bottom_dock_zoom = bottom_dock
3284 .visible_panel()
3285 .map(|panel| panel.is_zoomed(cx))
3286 .unwrap_or(false);
3287
3288 DockStructure {
3289 left: DockData {
3290 visible: left_visible,
3291 active_panel: left_active_panel,
3292 zoom: left_dock_zoom,
3293 },
3294 right: DockData {
3295 visible: right_visible,
3296 active_panel: right_active_panel,
3297 zoom: right_dock_zoom,
3298 },
3299 bottom: DockData {
3300 visible: bottom_visible,
3301 active_panel: bottom_active_panel,
3302 zoom: bottom_dock_zoom,
3303 },
3304 }
3305 }
3306
3307 if let Some(location) = self.location(cx) {
3308 // Load bearing special case:
3309 // - with_local_workspace() relies on this to not have other stuff open
3310 // when you open your log
3311 if !location.paths().is_empty() {
3312 let center_group = build_serialized_pane_group(&self.center.root, cx);
3313 let docks = build_serialized_docks(self, cx);
3314
3315 let serialized_workspace = SerializedWorkspace {
3316 id: self.database_id,
3317 location,
3318 center_group,
3319 bounds: Default::default(),
3320 display: Default::default(),
3321 docks,
3322 };
3323
3324 cx.background()
3325 .spawn(persistence::DB.save_workspace(serialized_workspace))
3326 .detach();
3327 }
3328 }
3329 }
3330
3331 pub(crate) fn load_workspace(
3332 workspace: WeakViewHandle<Workspace>,
3333 serialized_workspace: SerializedWorkspace,
3334 paths_to_open: Vec<Option<ProjectPath>>,
3335 cx: &mut AppContext,
3336 ) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
3337 cx.spawn(|mut cx| async move {
3338 let result = async_iife! {{
3339 let (project, old_center_pane) =
3340 workspace.read_with(&cx, |workspace, _| {
3341 (
3342 workspace.project().clone(),
3343 workspace.last_active_center_pane.clone(),
3344 )
3345 })?;
3346
3347 let mut center_items = None;
3348 let mut center_group = None;
3349 // Traverse the splits tree and add to things
3350 if let Some((group, active_pane, items)) = serialized_workspace
3351 .center_group
3352 .deserialize(&project, serialized_workspace.id, &workspace, &mut cx)
3353 .await {
3354 center_items = Some(items);
3355 center_group = Some((group, active_pane))
3356 }
3357
3358 let resulting_list = cx.read(|cx| {
3359 let mut opened_items = center_items
3360 .unwrap_or_default()
3361 .into_iter()
3362 .filter_map(|item| {
3363 let item = item?;
3364 let project_path = item.project_path(cx)?;
3365 Some((project_path, item))
3366 })
3367 .collect::<HashMap<_, _>>();
3368
3369 paths_to_open
3370 .into_iter()
3371 .map(|path_to_open| {
3372 path_to_open.map(|path_to_open| {
3373 Ok(opened_items.remove(&path_to_open))
3374 })
3375 .transpose()
3376 .map(|item| item.flatten())
3377 .transpose()
3378 })
3379 .collect::<Vec<_>>()
3380 });
3381
3382 // Remove old panes from workspace panes list
3383 workspace.update(&mut cx, |workspace, cx| {
3384 if let Some((center_group, active_pane)) = center_group {
3385 workspace.remove_panes(workspace.center.root.clone(), cx);
3386
3387 // Swap workspace center group
3388 workspace.center = PaneGroup::with_root(center_group);
3389
3390 // Change the focus to the workspace first so that we retrigger focus in on the pane.
3391 cx.focus_self();
3392
3393 if let Some(active_pane) = active_pane {
3394 cx.focus(&active_pane);
3395 } else {
3396 cx.focus(workspace.panes.last().unwrap());
3397 }
3398 } else {
3399 let old_center_handle = old_center_pane.and_then(|weak| weak.upgrade(cx));
3400 if let Some(old_center_handle) = old_center_handle {
3401 cx.focus(&old_center_handle)
3402 } else {
3403 cx.focus_self()
3404 }
3405 }
3406
3407 let docks = serialized_workspace.docks;
3408 workspace.left_dock.update(cx, |dock, cx| {
3409 dock.set_open(docks.left.visible, cx);
3410 if let Some(active_panel) = docks.left.active_panel {
3411 if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
3412 dock.activate_panel(ix, cx);
3413 }
3414 }
3415 dock.active_panel()
3416 .map(|panel| {
3417 panel.set_zoomed(docks.left.zoom, cx)
3418 });
3419 if docks.left.visible && docks.left.zoom {
3420 cx.focus_self()
3421 }
3422 });
3423 // TODO: I think the bug is that setting zoom or active undoes the bottom zoom or something
3424 workspace.right_dock.update(cx, |dock, cx| {
3425 dock.set_open(docks.right.visible, cx);
3426 if let Some(active_panel) = docks.right.active_panel {
3427 if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
3428 dock.activate_panel(ix, cx);
3429
3430 }
3431 }
3432 dock.active_panel()
3433 .map(|panel| {
3434 panel.set_zoomed(docks.right.zoom, cx)
3435 });
3436
3437 if docks.right.visible && docks.right.zoom {
3438 cx.focus_self()
3439 }
3440 });
3441 workspace.bottom_dock.update(cx, |dock, cx| {
3442 dock.set_open(docks.bottom.visible, cx);
3443 if let Some(active_panel) = docks.bottom.active_panel {
3444 if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
3445 dock.activate_panel(ix, cx);
3446 }
3447 }
3448
3449 dock.active_panel()
3450 .map(|panel| {
3451 panel.set_zoomed(docks.bottom.zoom, cx)
3452 });
3453
3454 if docks.bottom.visible && docks.bottom.zoom {
3455 cx.focus_self()
3456 }
3457 });
3458
3459
3460 cx.notify();
3461 })?;
3462
3463 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
3464 workspace.read_with(&cx, |workspace, cx| workspace.serialize_workspace(cx))?;
3465
3466 Ok::<_, anyhow::Error>(resulting_list)
3467 }};
3468
3469 result.await.unwrap_or_default()
3470 })
3471 }
3472
3473 #[cfg(any(test, feature = "test-support"))]
3474 pub fn test_new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
3475 let client = project.read(cx).client();
3476 let user_store = project.read(cx).user_store();
3477
3478 let channel_store =
3479 cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
3480 let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx));
3481 let app_state = Arc::new(AppState {
3482 languages: project.read(cx).languages().clone(),
3483 workspace_store,
3484 client,
3485 user_store,
3486 channel_store,
3487 fs: project.read(cx).fs().clone(),
3488 build_window_options: |_, _, _| Default::default(),
3489 initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
3490 background_actions: || &[],
3491 });
3492 Self::new(0, project, app_state, cx)
3493 }
3494
3495 fn render_dock(&self, position: DockPosition, cx: &WindowContext) -> Option<AnyElement<Self>> {
3496 let dock = match position {
3497 DockPosition::Left => &self.left_dock,
3498 DockPosition::Right => &self.right_dock,
3499 DockPosition::Bottom => &self.bottom_dock,
3500 };
3501 let active_panel = dock.read(cx).visible_panel()?;
3502 let element = if Some(active_panel.id()) == self.zoomed.as_ref().map(|zoomed| zoomed.id()) {
3503 dock.read(cx).render_placeholder(cx)
3504 } else {
3505 ChildView::new(dock, cx).into_any()
3506 };
3507
3508 Some(
3509 element
3510 .constrained()
3511 .dynamically(move |constraint, _, cx| match position {
3512 DockPosition::Left | DockPosition::Right => SizeConstraint::new(
3513 Vector2F::new(20., constraint.min.y()),
3514 Vector2F::new(cx.window_size().x() * 0.8, constraint.max.y()),
3515 ),
3516 DockPosition::Bottom => SizeConstraint::new(
3517 Vector2F::new(constraint.min.x(), 20.),
3518 Vector2F::new(constraint.max.x(), cx.window_size().y() * 0.8),
3519 ),
3520 })
3521 .into_any(),
3522 )
3523 }
3524}
3525
3526fn window_bounds_env_override(cx: &AsyncAppContext) -> Option<WindowBounds> {
3527 ZED_WINDOW_POSITION
3528 .zip(*ZED_WINDOW_SIZE)
3529 .map(|(position, size)| {
3530 WindowBounds::Fixed(RectF::new(
3531 cx.platform().screens()[0].bounds().origin() + position,
3532 size,
3533 ))
3534 })
3535}
3536
3537async fn open_items(
3538 serialized_workspace: Option<SerializedWorkspace>,
3539 workspace: &WeakViewHandle<Workspace>,
3540 mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
3541 app_state: Arc<AppState>,
3542 mut cx: AsyncAppContext,
3543) -> Vec<Option<anyhow::Result<Box<dyn ItemHandle>>>> {
3544 let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
3545
3546 if let Some(serialized_workspace) = serialized_workspace {
3547 let workspace = workspace.clone();
3548 let restored_items = cx
3549 .update(|cx| {
3550 Workspace::load_workspace(
3551 workspace,
3552 serialized_workspace,
3553 project_paths_to_open
3554 .iter()
3555 .map(|(_, project_path)| project_path)
3556 .cloned()
3557 .collect(),
3558 cx,
3559 )
3560 })
3561 .await;
3562
3563 let restored_project_paths = cx.read(|cx| {
3564 restored_items
3565 .iter()
3566 .filter_map(|item| item.as_ref()?.as_ref().ok()?.project_path(cx))
3567 .collect::<HashSet<_>>()
3568 });
3569
3570 opened_items = restored_items;
3571 project_paths_to_open
3572 .iter_mut()
3573 .for_each(|(_, project_path)| {
3574 if let Some(project_path_to_open) = project_path {
3575 if restored_project_paths.contains(project_path_to_open) {
3576 *project_path = None;
3577 }
3578 }
3579 });
3580 } else {
3581 for _ in 0..project_paths_to_open.len() {
3582 opened_items.push(None);
3583 }
3584 }
3585 assert!(opened_items.len() == project_paths_to_open.len());
3586
3587 let tasks =
3588 project_paths_to_open
3589 .into_iter()
3590 .enumerate()
3591 .map(|(i, (abs_path, project_path))| {
3592 let workspace = workspace.clone();
3593 cx.spawn(|mut cx| {
3594 let fs = app_state.fs.clone();
3595 async move {
3596 let file_project_path = project_path?;
3597 if fs.is_file(&abs_path).await {
3598 Some((
3599 i,
3600 workspace
3601 .update(&mut cx, |workspace, cx| {
3602 workspace.open_path(file_project_path, None, true, cx)
3603 })
3604 .log_err()?
3605 .await,
3606 ))
3607 } else {
3608 None
3609 }
3610 }
3611 })
3612 });
3613
3614 for maybe_opened_path in futures::future::join_all(tasks.into_iter())
3615 .await
3616 .into_iter()
3617 {
3618 if let Some((i, path_open_result)) = maybe_opened_path {
3619 opened_items[i] = Some(path_open_result);
3620 }
3621 }
3622
3623 opened_items
3624}
3625
3626fn notify_of_new_dock(workspace: &WeakViewHandle<Workspace>, cx: &mut AsyncAppContext) {
3627 const NEW_PANEL_BLOG_POST: &str = "https://zed.dev/blog/new-panel-system";
3628 const NEW_DOCK_HINT_KEY: &str = "show_new_dock_key";
3629 const MESSAGE_ID: usize = 2;
3630
3631 if workspace
3632 .read_with(cx, |workspace, cx| {
3633 workspace.has_shown_notification_once::<MessageNotification>(MESSAGE_ID, cx)
3634 })
3635 .unwrap_or(false)
3636 {
3637 return;
3638 }
3639
3640 if db::kvp::KEY_VALUE_STORE
3641 .read_kvp(NEW_DOCK_HINT_KEY)
3642 .ok()
3643 .flatten()
3644 .is_some()
3645 {
3646 if !workspace
3647 .read_with(cx, |workspace, cx| {
3648 workspace.has_shown_notification_once::<MessageNotification>(MESSAGE_ID, cx)
3649 })
3650 .unwrap_or(false)
3651 {
3652 cx.update(|cx| {
3653 cx.update_global::<NotificationTracker, _, _>(|tracker, _| {
3654 let entry = tracker
3655 .entry(TypeId::of::<MessageNotification>())
3656 .or_default();
3657 if !entry.contains(&MESSAGE_ID) {
3658 entry.push(MESSAGE_ID);
3659 }
3660 });
3661 });
3662 }
3663
3664 return;
3665 }
3666
3667 cx.spawn(|_| async move {
3668 db::kvp::KEY_VALUE_STORE
3669 .write_kvp(NEW_DOCK_HINT_KEY.to_string(), "seen".to_string())
3670 .await
3671 .ok();
3672 })
3673 .detach();
3674
3675 workspace
3676 .update(cx, |workspace, cx| {
3677 workspace.show_notification_once(2, cx, |cx| {
3678 cx.add_view(|_| {
3679 MessageNotification::new_element(|text, _| {
3680 Text::new(
3681 "Looking for the dock? Try ctrl-`!\nshift-escape now zooms your pane.",
3682 text,
3683 )
3684 .with_custom_runs(vec![26..32, 34..46], |_, bounds, cx| {
3685 let code_span_background_color = settings::get::<ThemeSettings>(cx)
3686 .theme
3687 .editor
3688 .document_highlight_read_background;
3689
3690 cx.scene().push_quad(gpui::Quad {
3691 bounds,
3692 background: Some(code_span_background_color),
3693 border: Default::default(),
3694 corner_radii: (2.0).into(),
3695 })
3696 })
3697 .into_any()
3698 })
3699 .with_click_message("Read more about the new panel system")
3700 .on_click(|cx| cx.platform().open_url(NEW_PANEL_BLOG_POST))
3701 })
3702 })
3703 })
3704 .ok();
3705}
3706
3707fn notify_if_database_failed(workspace: &WeakViewHandle<Workspace>, cx: &mut AsyncAppContext) {
3708 const REPORT_ISSUE_URL: &str ="https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";
3709
3710 workspace
3711 .update(cx, |workspace, cx| {
3712 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
3713 workspace.show_notification_once(0, cx, |cx| {
3714 cx.add_view(|_| {
3715 MessageNotification::new("Failed to load the database file.")
3716 .with_click_message("Click to let us know about this error")
3717 .on_click(|cx| cx.platform().open_url(REPORT_ISSUE_URL))
3718 })
3719 });
3720 }
3721 })
3722 .log_err();
3723}
3724
3725impl Entity for Workspace {
3726 type Event = Event;
3727
3728 fn release(&mut self, cx: &mut AppContext) {
3729 self.app_state.workspace_store.update(cx, |store, _| {
3730 store.workspaces.remove(&self.weak_self);
3731 })
3732 }
3733}
3734
3735impl View for Workspace {
3736 fn ui_name() -> &'static str {
3737 "Workspace"
3738 }
3739
3740 fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
3741 let theme = theme::current(cx).clone();
3742 Stack::new()
3743 .with_child(
3744 Flex::column()
3745 .with_child(self.render_titlebar(&theme, cx))
3746 .with_child(
3747 Stack::new()
3748 .with_child({
3749 let project = self.project.clone();
3750 Flex::row()
3751 .with_children(self.render_dock(DockPosition::Left, cx))
3752 .with_child(
3753 Flex::column()
3754 .with_child(
3755 FlexItem::new(
3756 self.center.render(
3757 &project,
3758 &theme,
3759 &self.follower_states_by_leader,
3760 self.active_call(),
3761 self.active_pane(),
3762 self.zoomed
3763 .as_ref()
3764 .and_then(|zoomed| zoomed.upgrade(cx))
3765 .as_ref(),
3766 &self.app_state,
3767 cx,
3768 ),
3769 )
3770 .flex(1., true),
3771 )
3772 .with_children(
3773 self.render_dock(DockPosition::Bottom, cx),
3774 )
3775 .flex(1., true),
3776 )
3777 .with_children(self.render_dock(DockPosition::Right, cx))
3778 })
3779 .with_child(Overlay::new(
3780 Stack::new()
3781 .with_children(self.zoomed.as_ref().and_then(|zoomed| {
3782 enum ZoomBackground {}
3783 let zoomed = zoomed.upgrade(cx)?;
3784
3785 let mut foreground_style =
3786 theme.workspace.zoomed_pane_foreground;
3787 if let Some(zoomed_dock_position) = self.zoomed_position {
3788 foreground_style =
3789 theme.workspace.zoomed_panel_foreground;
3790 let margin = foreground_style.margin.top;
3791 let border = foreground_style.border.top;
3792
3793 // Only include a margin and border on the opposite side.
3794 foreground_style.margin.top = 0.;
3795 foreground_style.margin.left = 0.;
3796 foreground_style.margin.bottom = 0.;
3797 foreground_style.margin.right = 0.;
3798 foreground_style.border.top = false;
3799 foreground_style.border.left = false;
3800 foreground_style.border.bottom = false;
3801 foreground_style.border.right = false;
3802 match zoomed_dock_position {
3803 DockPosition::Left => {
3804 foreground_style.margin.right = margin;
3805 foreground_style.border.right = border;
3806 }
3807 DockPosition::Right => {
3808 foreground_style.margin.left = margin;
3809 foreground_style.border.left = border;
3810 }
3811 DockPosition::Bottom => {
3812 foreground_style.margin.top = margin;
3813 foreground_style.border.top = border;
3814 }
3815 }
3816 }
3817
3818 Some(
3819 ChildView::new(&zoomed, cx)
3820 .contained()
3821 .with_style(foreground_style)
3822 .aligned()
3823 .contained()
3824 .with_style(theme.workspace.zoomed_background)
3825 .mouse::<ZoomBackground>(0)
3826 .capture_all()
3827 .on_down(
3828 MouseButton::Left,
3829 |_, this: &mut Self, cx| {
3830 this.zoom_out(cx);
3831 },
3832 ),
3833 )
3834 }))
3835 .with_children(self.modal.as_ref().map(|modal| {
3836 // Prevent clicks within the modal from falling
3837 // through to the rest of the workspace.
3838 enum ModalBackground {}
3839 MouseEventHandler::new::<ModalBackground, _>(
3840 0,
3841 cx,
3842 |_, cx| ChildView::new(modal.view.as_any(), cx),
3843 )
3844 .on_click(MouseButton::Left, |_, _, _| {})
3845 .contained()
3846 .with_style(theme.workspace.modal)
3847 .aligned()
3848 .top()
3849 }))
3850 .with_children(self.render_notifications(&theme.workspace, cx)),
3851 ))
3852 .provide_resize_bounds::<WorkspaceBounds>()
3853 .flex(1.0, true),
3854 )
3855 .with_child(ChildView::new(&self.status_bar, cx))
3856 .contained()
3857 .with_background_color(theme.workspace.background),
3858 )
3859 .with_children(DragAndDrop::render(cx))
3860 .with_children(self.render_disconnected_overlay(cx))
3861 .into_any_named("workspace")
3862 }
3863
3864 fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
3865 if cx.is_self_focused() {
3866 cx.focus(&self.active_pane);
3867 }
3868 }
3869
3870 fn modifiers_changed(&mut self, e: &ModifiersChangedEvent, cx: &mut ViewContext<Self>) -> bool {
3871 DragAndDrop::<Workspace>::update_modifiers(e.modifiers, cx)
3872 }
3873}
3874
3875impl WorkspaceStore {
3876 pub fn new(client: Arc<Client>, cx: &mut ModelContext<Self>) -> Self {
3877 Self {
3878 workspaces: Default::default(),
3879 followers: Default::default(),
3880 _subscriptions: vec![
3881 client.add_request_handler(cx.handle(), Self::handle_follow),
3882 client.add_message_handler(cx.handle(), Self::handle_unfollow),
3883 client.add_message_handler(cx.handle(), Self::handle_update_from_leader),
3884 ],
3885 client,
3886 }
3887 }
3888
3889 pub fn update_followers(
3890 &self,
3891 project_id: Option<u64>,
3892 update: proto::update_followers::Variant,
3893 cx: &AppContext,
3894 ) -> Option<()> {
3895 if !cx.has_global::<ModelHandle<ActiveCall>>() {
3896 return None;
3897 }
3898
3899 let room_id = ActiveCall::global(cx).read(cx).room()?.read(cx).id();
3900 let follower_ids: Vec<_> = self
3901 .followers
3902 .iter()
3903 .filter_map(|follower| {
3904 (follower.project_id == project_id).then_some(follower.peer_id.into())
3905 })
3906 .collect();
3907 if follower_ids.is_empty() {
3908 return None;
3909 }
3910 self.client
3911 .send(proto::UpdateFollowers {
3912 room_id,
3913 project_id,
3914 follower_ids,
3915 variant: Some(update),
3916 })
3917 .log_err()
3918 }
3919
3920 async fn handle_follow(
3921 this: ModelHandle<Self>,
3922 envelope: TypedEnvelope<proto::Follow>,
3923 _: Arc<Client>,
3924 mut cx: AsyncAppContext,
3925 ) -> Result<proto::FollowResponse> {
3926 this.update(&mut cx, |this, cx| {
3927 let follower = Follower {
3928 project_id: envelope.payload.project_id,
3929 peer_id: envelope.original_sender_id()?,
3930 };
3931 let active_project_id = ActiveCall::global(cx)
3932 .read(cx)
3933 .location()
3934 .as_ref()
3935 .and_then(|project| project.upgrade(cx)?.read(cx).remote_id());
3936
3937 let mut response = proto::FollowResponse::default();
3938 for workspace in &this.workspaces {
3939 let Some(workspace) = workspace.upgrade(cx) else {
3940 continue;
3941 };
3942
3943 workspace.update(cx.as_mut(), |workspace, cx| {
3944 let project_id = workspace.project.read(cx).remote_id();
3945 if follower.project_id != project_id && follower.project_id.is_some() {
3946 return;
3947 }
3948
3949 let handler_response = workspace.handle_follow(cx);
3950 if response.views.is_empty() {
3951 response.views = handler_response.views;
3952 } else {
3953 response.views.extend_from_slice(&handler_response.views);
3954 }
3955
3956 if let Some(active_view_id) = handler_response.active_view_id.clone() {
3957 if response.active_view_id.is_none() || project_id == active_project_id {
3958 response.active_view_id = Some(active_view_id);
3959 }
3960 }
3961 });
3962 }
3963
3964 if let Err(ix) = this.followers.binary_search(&follower) {
3965 this.followers.insert(ix, follower);
3966 }
3967
3968 Ok(response)
3969 })
3970 }
3971
3972 async fn handle_unfollow(
3973 this: ModelHandle<Self>,
3974 envelope: TypedEnvelope<proto::Unfollow>,
3975 _: Arc<Client>,
3976 mut cx: AsyncAppContext,
3977 ) -> Result<()> {
3978 this.update(&mut cx, |this, _| {
3979 let follower = Follower {
3980 project_id: envelope.payload.project_id,
3981 peer_id: envelope.original_sender_id()?,
3982 };
3983 if let Ok(ix) = this.followers.binary_search(&follower) {
3984 this.followers.remove(ix);
3985 }
3986 Ok(())
3987 })
3988 }
3989
3990 async fn handle_update_from_leader(
3991 this: ModelHandle<Self>,
3992 envelope: TypedEnvelope<proto::UpdateFollowers>,
3993 _: Arc<Client>,
3994 mut cx: AsyncAppContext,
3995 ) -> Result<()> {
3996 let leader_id = envelope.original_sender_id()?;
3997 let update = envelope.payload;
3998 this.update(&mut cx, |this, cx| {
3999 for workspace in &this.workspaces {
4000 let Some(workspace) = workspace.upgrade(cx) else {
4001 continue;
4002 };
4003 workspace.update(cx.as_mut(), |workspace, cx| {
4004 let project_id = workspace.project.read(cx).remote_id();
4005 if update.project_id != project_id && update.project_id.is_some() {
4006 return;
4007 }
4008 workspace.handle_update_followers(leader_id, update.clone(), cx);
4009 });
4010 }
4011 Ok(())
4012 })
4013 }
4014}
4015
4016impl Entity for WorkspaceStore {
4017 type Event = ();
4018}
4019
4020impl ViewId {
4021 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
4022 Ok(Self {
4023 creator: message
4024 .creator
4025 .ok_or_else(|| anyhow!("creator is missing"))?,
4026 id: message.id,
4027 })
4028 }
4029
4030 pub(crate) fn to_proto(&self) -> proto::ViewId {
4031 proto::ViewId {
4032 creator: Some(self.creator),
4033 id: self.id,
4034 }
4035 }
4036}
4037
4038pub trait WorkspaceHandle {
4039 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
4040}
4041
4042impl WorkspaceHandle for ViewHandle<Workspace> {
4043 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
4044 self.read(cx)
4045 .worktrees(cx)
4046 .flat_map(|worktree| {
4047 let worktree_id = worktree.read(cx).id();
4048 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
4049 worktree_id,
4050 path: f.path.clone(),
4051 })
4052 })
4053 .collect::<Vec<_>>()
4054 }
4055}
4056
4057impl std::fmt::Debug for OpenPaths {
4058 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4059 f.debug_struct("OpenPaths")
4060 .field("paths", &self.paths)
4061 .finish()
4062 }
4063}
4064
4065pub struct WorkspaceCreated(pub WeakViewHandle<Workspace>);
4066
4067pub fn activate_workspace_for_project(
4068 cx: &mut AsyncAppContext,
4069 predicate: impl Fn(&mut Project, &mut ModelContext<Project>) -> bool,
4070) -> Option<WeakViewHandle<Workspace>> {
4071 for window in cx.windows() {
4072 let handle = window
4073 .update(cx, |cx| {
4074 if let Some(workspace_handle) = cx.root_view().clone().downcast::<Workspace>() {
4075 let project = workspace_handle.read(cx).project.clone();
4076 if project.update(cx, &predicate) {
4077 cx.activate_window();
4078 return Some(workspace_handle.clone());
4079 }
4080 }
4081 None
4082 })
4083 .flatten();
4084
4085 if let Some(handle) = handle {
4086 return Some(handle.downgrade());
4087 }
4088 }
4089 None
4090}
4091
4092pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
4093 DB.last_workspace().await.log_err().flatten()
4094}
4095
4096#[allow(clippy::type_complexity)]
4097pub fn open_paths(
4098 abs_paths: &[PathBuf],
4099 app_state: &Arc<AppState>,
4100 requesting_window: Option<WindowHandle<Workspace>>,
4101 cx: &mut AppContext,
4102) -> Task<
4103 Result<(
4104 WeakViewHandle<Workspace>,
4105 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
4106 )>,
4107> {
4108 let app_state = app_state.clone();
4109 let abs_paths = abs_paths.to_vec();
4110 cx.spawn(|mut cx| async move {
4111 // Open paths in existing workspace if possible
4112 let existing = activate_workspace_for_project(&mut cx, |project, cx| {
4113 project.contains_paths(&abs_paths, cx)
4114 });
4115
4116 if let Some(existing) = existing {
4117 Ok((
4118 existing.clone(),
4119 existing
4120 .update(&mut cx, |workspace, cx| {
4121 workspace.open_paths(abs_paths, true, cx)
4122 })?
4123 .await,
4124 ))
4125 } else {
4126 Ok(cx
4127 .update(|cx| {
4128 Workspace::new_local(abs_paths, app_state.clone(), requesting_window, cx)
4129 })
4130 .await)
4131 }
4132 })
4133}
4134
4135pub fn open_new(
4136 app_state: &Arc<AppState>,
4137 cx: &mut AppContext,
4138 init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static,
4139) -> Task<()> {
4140 let task = Workspace::new_local(Vec::new(), app_state.clone(), None, cx);
4141 cx.spawn(|mut cx| async move {
4142 let (workspace, opened_paths) = task.await;
4143
4144 workspace
4145 .update(&mut cx, |workspace, cx| {
4146 if opened_paths.is_empty() {
4147 init(workspace, cx)
4148 }
4149 })
4150 .log_err();
4151 })
4152}
4153
4154pub fn create_and_open_local_file(
4155 path: &'static Path,
4156 cx: &mut ViewContext<Workspace>,
4157 default_content: impl 'static + Send + FnOnce() -> Rope,
4158) -> Task<Result<Box<dyn ItemHandle>>> {
4159 cx.spawn(|workspace, mut cx| async move {
4160 let fs = workspace.read_with(&cx, |workspace, _| workspace.app_state().fs.clone())?;
4161 if !fs.is_file(path).await {
4162 fs.create_file(path, Default::default()).await?;
4163 fs.save(path, &default_content(), Default::default())
4164 .await?;
4165 }
4166
4167 let mut items = workspace
4168 .update(&mut cx, |workspace, cx| {
4169 workspace.with_local_workspace(cx, |workspace, cx| {
4170 workspace.open_paths(vec![path.to_path_buf()], false, cx)
4171 })
4172 })?
4173 .await?
4174 .await;
4175
4176 let item = items.pop().flatten();
4177 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
4178 })
4179}
4180
4181pub fn join_remote_project(
4182 project_id: u64,
4183 follow_user_id: u64,
4184 app_state: Arc<AppState>,
4185 cx: &mut AppContext,
4186) -> Task<Result<()>> {
4187 cx.spawn(|mut cx| async move {
4188 let existing_workspace = cx
4189 .windows()
4190 .into_iter()
4191 .find_map(|window| {
4192 window.downcast::<Workspace>().and_then(|window| {
4193 window.read_root_with(&cx, |workspace, cx| {
4194 if workspace.project().read(cx).remote_id() == Some(project_id) {
4195 Some(cx.handle().downgrade())
4196 } else {
4197 None
4198 }
4199 })
4200 })
4201 })
4202 .flatten();
4203
4204 let workspace = if let Some(existing_workspace) = existing_workspace {
4205 existing_workspace
4206 } else {
4207 let active_call = cx.read(ActiveCall::global);
4208 let room = active_call
4209 .read_with(&cx, |call, _| call.room().cloned())
4210 .ok_or_else(|| anyhow!("not in a call"))?;
4211 let project = room
4212 .update(&mut cx, |room, cx| {
4213 room.join_project(
4214 project_id,
4215 app_state.languages.clone(),
4216 app_state.fs.clone(),
4217 cx,
4218 )
4219 })
4220 .await?;
4221
4222 let window_bounds_override = window_bounds_env_override(&cx);
4223 let window = cx.add_window(
4224 (app_state.build_window_options)(
4225 window_bounds_override,
4226 None,
4227 cx.platform().as_ref(),
4228 ),
4229 |cx| Workspace::new(0, project, app_state.clone(), cx),
4230 );
4231 let workspace = window.root(&cx).unwrap();
4232 (app_state.initialize_workspace)(
4233 workspace.downgrade(),
4234 false,
4235 app_state.clone(),
4236 cx.clone(),
4237 )
4238 .await
4239 .log_err();
4240
4241 workspace.downgrade()
4242 };
4243
4244 workspace.window().activate(&mut cx);
4245 cx.platform().activate(true);
4246
4247 workspace.update(&mut cx, |workspace, cx| {
4248 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
4249 let follow_peer_id = room
4250 .read(cx)
4251 .remote_participants()
4252 .iter()
4253 .find(|(_, participant)| participant.user.id == follow_user_id)
4254 .map(|(_, p)| p.peer_id)
4255 .or_else(|| {
4256 // If we couldn't follow the given user, follow the host instead.
4257 let collaborator = workspace
4258 .project()
4259 .read(cx)
4260 .collaborators()
4261 .values()
4262 .find(|collaborator| collaborator.replica_id == 0)?;
4263 Some(collaborator.peer_id)
4264 });
4265
4266 if let Some(follow_peer_id) = follow_peer_id {
4267 if !workspace.is_being_followed(follow_peer_id) {
4268 workspace
4269 .toggle_follow(follow_peer_id, cx)
4270 .map(|follow| follow.detach_and_log_err(cx));
4271 }
4272 }
4273 }
4274 })?;
4275
4276 anyhow::Ok(())
4277 })
4278}
4279
4280pub fn restart(_: &Restart, cx: &mut AppContext) {
4281 let should_confirm = settings::get::<WorkspaceSettings>(cx).confirm_quit;
4282 cx.spawn(|mut cx| async move {
4283 let mut workspace_windows = cx
4284 .windows()
4285 .into_iter()
4286 .filter_map(|window| window.downcast::<Workspace>())
4287 .collect::<Vec<_>>();
4288
4289 // If multiple windows have unsaved changes, and need a save prompt,
4290 // prompt in the active window before switching to a different window.
4291 workspace_windows.sort_by_key(|window| window.is_active(&cx) == Some(false));
4292
4293 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
4294 let answer = window.prompt(
4295 PromptLevel::Info,
4296 "Are you sure you want to restart?",
4297 &["Restart", "Cancel"],
4298 &mut cx,
4299 );
4300
4301 if let Some(mut answer) = answer {
4302 let answer = answer.next().await;
4303 if answer != Some(0) {
4304 return Ok(());
4305 }
4306 }
4307 }
4308
4309 // If the user cancels any save prompt, then keep the app open.
4310 for window in workspace_windows {
4311 if let Some(should_close) = window.update_root(&mut cx, |workspace, cx| {
4312 workspace.prepare_to_close(true, cx)
4313 }) {
4314 if !should_close.await? {
4315 return Ok(());
4316 }
4317 }
4318 }
4319 cx.platform().restart();
4320 anyhow::Ok(())
4321 })
4322 .detach_and_log_err(cx);
4323}
4324
4325fn parse_pixel_position_env_var(value: &str) -> Option<Vector2F> {
4326 let mut parts = value.split(',');
4327 let width: usize = parts.next()?.parse().ok()?;
4328 let height: usize = parts.next()?.parse().ok()?;
4329 Some(vec2f(width as f32, height as f32))
4330}
4331
4332#[cfg(test)]
4333mod tests {
4334 use super::*;
4335 use crate::{
4336 dock::test::{TestPanel, TestPanelEvent},
4337 item::test::{TestItem, TestItemEvent, TestProjectItem},
4338 };
4339 use fs::FakeFs;
4340 use gpui::{executor::Deterministic, test::EmptyView, TestAppContext};
4341 use project::{Project, ProjectEntryId};
4342 use serde_json::json;
4343 use settings::SettingsStore;
4344 use std::{cell::RefCell, rc::Rc};
4345
4346 #[gpui::test]
4347 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
4348 init_test(cx);
4349
4350 let fs = FakeFs::new(cx.background());
4351 let project = Project::test(fs, [], cx).await;
4352 let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
4353 let workspace = window.root(cx);
4354
4355 // Adding an item with no ambiguity renders the tab without detail.
4356 let item1 = window.add_view(cx, |_| {
4357 let mut item = TestItem::new();
4358 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
4359 item
4360 });
4361 workspace.update(cx, |workspace, cx| {
4362 workspace.add_item(Box::new(item1.clone()), cx);
4363 });
4364 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), None));
4365
4366 // Adding an item that creates ambiguity increases the level of detail on
4367 // both tabs.
4368 let item2 = window.add_view(cx, |_| {
4369 let mut item = TestItem::new();
4370 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
4371 item
4372 });
4373 workspace.update(cx, |workspace, cx| {
4374 workspace.add_item(Box::new(item2.clone()), cx);
4375 });
4376 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4377 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4378
4379 // Adding an item that creates ambiguity increases the level of detail only
4380 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
4381 // we stop at the highest detail available.
4382 let item3 = window.add_view(cx, |_| {
4383 let mut item = TestItem::new();
4384 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
4385 item
4386 });
4387 workspace.update(cx, |workspace, cx| {
4388 workspace.add_item(Box::new(item3.clone()), cx);
4389 });
4390 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4391 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
4392 item3.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
4393 }
4394
4395 #[gpui::test]
4396 async fn test_tracking_active_path(cx: &mut TestAppContext) {
4397 init_test(cx);
4398
4399 let fs = FakeFs::new(cx.background());
4400 fs.insert_tree(
4401 "/root1",
4402 json!({
4403 "one.txt": "",
4404 "two.txt": "",
4405 }),
4406 )
4407 .await;
4408 fs.insert_tree(
4409 "/root2",
4410 json!({
4411 "three.txt": "",
4412 }),
4413 )
4414 .await;
4415
4416 let project = Project::test(fs, ["root1".as_ref()], cx).await;
4417 let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
4418 let workspace = window.root(cx);
4419 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4420 let worktree_id = project.read_with(cx, |project, cx| {
4421 project.worktrees(cx).next().unwrap().read(cx).id()
4422 });
4423
4424 let item1 = window.add_view(cx, |cx| {
4425 TestItem::new().with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
4426 });
4427 let item2 = window.add_view(cx, |cx| {
4428 TestItem::new().with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
4429 });
4430
4431 // Add an item to an empty pane
4432 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx));
4433 project.read_with(cx, |project, cx| {
4434 assert_eq!(
4435 project.active_entry(),
4436 project
4437 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
4438 .map(|e| e.id)
4439 );
4440 });
4441 assert_eq!(window.current_title(cx).as_deref(), Some("one.txt β root1"));
4442
4443 // Add a second item to a non-empty pane
4444 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
4445 assert_eq!(window.current_title(cx).as_deref(), Some("two.txt β root1"));
4446 project.read_with(cx, |project, cx| {
4447 assert_eq!(
4448 project.active_entry(),
4449 project
4450 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
4451 .map(|e| e.id)
4452 );
4453 });
4454
4455 // Close the active item
4456 pane.update(cx, |pane, cx| {
4457 pane.close_active_item(&Default::default(), cx).unwrap()
4458 })
4459 .await
4460 .unwrap();
4461 assert_eq!(window.current_title(cx).as_deref(), Some("one.txt β root1"));
4462 project.read_with(cx, |project, cx| {
4463 assert_eq!(
4464 project.active_entry(),
4465 project
4466 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
4467 .map(|e| e.id)
4468 );
4469 });
4470
4471 // Add a project folder
4472 project
4473 .update(cx, |project, cx| {
4474 project.find_or_create_local_worktree("/root2", true, cx)
4475 })
4476 .await
4477 .unwrap();
4478 assert_eq!(
4479 window.current_title(cx).as_deref(),
4480 Some("one.txt β root1, root2")
4481 );
4482
4483 // Remove a project folder
4484 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
4485 assert_eq!(window.current_title(cx).as_deref(), Some("one.txt β root2"));
4486 }
4487
4488 #[gpui::test]
4489 async fn test_close_window(cx: &mut TestAppContext) {
4490 init_test(cx);
4491
4492 let fs = FakeFs::new(cx.background());
4493 fs.insert_tree("/root", json!({ "one": "" })).await;
4494
4495 let project = Project::test(fs, ["root".as_ref()], cx).await;
4496 let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
4497 let workspace = window.root(cx);
4498
4499 // When there are no dirty items, there's nothing to do.
4500 let item1 = window.add_view(cx, |_| TestItem::new());
4501 workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
4502 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
4503 assert!(task.await.unwrap());
4504
4505 // When there are dirty untitled items, prompt to save each one. If the user
4506 // cancels any prompt, then abort.
4507 let item2 = window.add_view(cx, |_| TestItem::new().with_dirty(true));
4508 let item3 = window.add_view(cx, |cx| {
4509 TestItem::new()
4510 .with_dirty(true)
4511 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4512 });
4513 workspace.update(cx, |w, cx| {
4514 w.add_item(Box::new(item2.clone()), cx);
4515 w.add_item(Box::new(item3.clone()), cx);
4516 });
4517 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
4518 cx.foreground().run_until_parked();
4519 window.simulate_prompt_answer(2, cx); // cancel save all
4520 cx.foreground().run_until_parked();
4521 window.simulate_prompt_answer(2, cx); // cancel save all
4522 cx.foreground().run_until_parked();
4523 assert!(!window.has_pending_prompt(cx));
4524 assert!(!task.await.unwrap());
4525 }
4526
4527 #[gpui::test]
4528 async fn test_close_pane_items(cx: &mut TestAppContext) {
4529 init_test(cx);
4530
4531 let fs = FakeFs::new(cx.background());
4532
4533 let project = Project::test(fs, None, cx).await;
4534 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
4535 let workspace = window.root(cx);
4536
4537 let item1 = window.add_view(cx, |cx| {
4538 TestItem::new()
4539 .with_dirty(true)
4540 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4541 });
4542 let item2 = window.add_view(cx, |cx| {
4543 TestItem::new()
4544 .with_dirty(true)
4545 .with_conflict(true)
4546 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
4547 });
4548 let item3 = window.add_view(cx, |cx| {
4549 TestItem::new()
4550 .with_dirty(true)
4551 .with_conflict(true)
4552 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
4553 });
4554 let item4 = window.add_view(cx, |cx| {
4555 TestItem::new()
4556 .with_dirty(true)
4557 .with_project_items(&[TestProjectItem::new_untitled(cx)])
4558 });
4559 let pane = workspace.update(cx, |workspace, cx| {
4560 workspace.add_item(Box::new(item1.clone()), cx);
4561 workspace.add_item(Box::new(item2.clone()), cx);
4562 workspace.add_item(Box::new(item3.clone()), cx);
4563 workspace.add_item(Box::new(item4.clone()), cx);
4564 workspace.active_pane().clone()
4565 });
4566
4567 let close_items = pane.update(cx, |pane, cx| {
4568 pane.activate_item(1, true, true, cx);
4569 assert_eq!(pane.active_item().unwrap().id(), item2.id());
4570 let item1_id = item1.id();
4571 let item3_id = item3.id();
4572 let item4_id = item4.id();
4573 pane.close_items(cx, SaveIntent::Close, move |id| {
4574 [item1_id, item3_id, item4_id].contains(&id)
4575 })
4576 });
4577 cx.foreground().run_until_parked();
4578
4579 assert!(window.has_pending_prompt(cx));
4580 // Ignore "Save all" prompt
4581 window.simulate_prompt_answer(2, cx);
4582 cx.foreground().run_until_parked();
4583 // There's a prompt to save item 1.
4584 pane.read_with(cx, |pane, _| {
4585 assert_eq!(pane.items_len(), 4);
4586 assert_eq!(pane.active_item().unwrap().id(), item1.id());
4587 });
4588 // Confirm saving item 1.
4589 window.simulate_prompt_answer(0, cx);
4590 cx.foreground().run_until_parked();
4591
4592 // Item 1 is saved. There's a prompt to save item 3.
4593 pane.read_with(cx, |pane, cx| {
4594 assert_eq!(item1.read(cx).save_count, 1);
4595 assert_eq!(item1.read(cx).save_as_count, 0);
4596 assert_eq!(item1.read(cx).reload_count, 0);
4597 assert_eq!(pane.items_len(), 3);
4598 assert_eq!(pane.active_item().unwrap().id(), item3.id());
4599 });
4600 assert!(window.has_pending_prompt(cx));
4601
4602 // Cancel saving item 3.
4603 window.simulate_prompt_answer(1, cx);
4604 cx.foreground().run_until_parked();
4605
4606 // Item 3 is reloaded. There's a prompt to save item 4.
4607 pane.read_with(cx, |pane, cx| {
4608 assert_eq!(item3.read(cx).save_count, 0);
4609 assert_eq!(item3.read(cx).save_as_count, 0);
4610 assert_eq!(item3.read(cx).reload_count, 1);
4611 assert_eq!(pane.items_len(), 2);
4612 assert_eq!(pane.active_item().unwrap().id(), item4.id());
4613 });
4614 assert!(window.has_pending_prompt(cx));
4615
4616 // Confirm saving item 4.
4617 window.simulate_prompt_answer(0, cx);
4618 cx.foreground().run_until_parked();
4619
4620 // There's a prompt for a path for item 4.
4621 cx.simulate_new_path_selection(|_| Some(Default::default()));
4622 close_items.await.unwrap();
4623
4624 // The requested items are closed.
4625 pane.read_with(cx, |pane, cx| {
4626 assert_eq!(item4.read(cx).save_count, 0);
4627 assert_eq!(item4.read(cx).save_as_count, 1);
4628 assert_eq!(item4.read(cx).reload_count, 0);
4629 assert_eq!(pane.items_len(), 1);
4630 assert_eq!(pane.active_item().unwrap().id(), item2.id());
4631 });
4632 }
4633
4634 #[gpui::test]
4635 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
4636 init_test(cx);
4637
4638 let fs = FakeFs::new(cx.background());
4639
4640 let project = Project::test(fs, [], cx).await;
4641 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
4642 let workspace = window.root(cx);
4643
4644 // Create several workspace items with single project entries, and two
4645 // workspace items with multiple project entries.
4646 let single_entry_items = (0..=4)
4647 .map(|project_entry_id| {
4648 window.add_view(cx, |cx| {
4649 TestItem::new()
4650 .with_dirty(true)
4651 .with_project_items(&[TestProjectItem::new(
4652 project_entry_id,
4653 &format!("{project_entry_id}.txt"),
4654 cx,
4655 )])
4656 })
4657 })
4658 .collect::<Vec<_>>();
4659 let item_2_3 = window.add_view(cx, |cx| {
4660 TestItem::new()
4661 .with_dirty(true)
4662 .with_singleton(false)
4663 .with_project_items(&[
4664 single_entry_items[2].read(cx).project_items[0].clone(),
4665 single_entry_items[3].read(cx).project_items[0].clone(),
4666 ])
4667 });
4668 let item_3_4 = window.add_view(cx, |cx| {
4669 TestItem::new()
4670 .with_dirty(true)
4671 .with_singleton(false)
4672 .with_project_items(&[
4673 single_entry_items[3].read(cx).project_items[0].clone(),
4674 single_entry_items[4].read(cx).project_items[0].clone(),
4675 ])
4676 });
4677
4678 // Create two panes that contain the following project entries:
4679 // left pane:
4680 // multi-entry items: (2, 3)
4681 // single-entry items: 0, 1, 2, 3, 4
4682 // right pane:
4683 // single-entry items: 1
4684 // multi-entry items: (3, 4)
4685 let left_pane = workspace.update(cx, |workspace, cx| {
4686 let left_pane = workspace.active_pane().clone();
4687 workspace.add_item(Box::new(item_2_3.clone()), cx);
4688 for item in single_entry_items {
4689 workspace.add_item(Box::new(item), cx);
4690 }
4691 left_pane.update(cx, |pane, cx| {
4692 pane.activate_item(2, true, true, cx);
4693 });
4694
4695 workspace
4696 .split_and_clone(left_pane.clone(), SplitDirection::Right, cx)
4697 .unwrap();
4698
4699 left_pane
4700 });
4701
4702 //Need to cause an effect flush in order to respect new focus
4703 workspace.update(cx, |workspace, cx| {
4704 workspace.add_item(Box::new(item_3_4.clone()), cx);
4705 cx.focus(&left_pane);
4706 });
4707
4708 // When closing all of the items in the left pane, we should be prompted twice:
4709 // once for project entry 0, and once for project entry 2. After those two
4710 // prompts, the task should complete.
4711
4712 let close = left_pane.update(cx, |pane, cx| {
4713 pane.close_items(cx, SaveIntent::Close, move |_| true)
4714 });
4715 cx.foreground().run_until_parked();
4716 // Discard "Save all" prompt
4717 window.simulate_prompt_answer(2, cx);
4718
4719 cx.foreground().run_until_parked();
4720 left_pane.read_with(cx, |pane, cx| {
4721 assert_eq!(
4722 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
4723 &[ProjectEntryId::from_proto(0)]
4724 );
4725 });
4726 window.simulate_prompt_answer(0, cx);
4727
4728 cx.foreground().run_until_parked();
4729 left_pane.read_with(cx, |pane, cx| {
4730 assert_eq!(
4731 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
4732 &[ProjectEntryId::from_proto(2)]
4733 );
4734 });
4735 window.simulate_prompt_answer(0, cx);
4736
4737 cx.foreground().run_until_parked();
4738 close.await.unwrap();
4739 left_pane.read_with(cx, |pane, _| {
4740 assert_eq!(pane.items_len(), 0);
4741 });
4742 }
4743
4744 #[gpui::test]
4745 async fn test_autosave(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
4746 init_test(cx);
4747
4748 let fs = FakeFs::new(cx.background());
4749
4750 let project = Project::test(fs, [], cx).await;
4751 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
4752 let workspace = window.root(cx);
4753 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4754
4755 let item = window.add_view(cx, |cx| {
4756 TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4757 });
4758 let item_id = item.id();
4759 workspace.update(cx, |workspace, cx| {
4760 workspace.add_item(Box::new(item.clone()), cx);
4761 });
4762
4763 // Autosave on window change.
4764 item.update(cx, |item, cx| {
4765 cx.update_global(|settings: &mut SettingsStore, cx| {
4766 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4767 settings.autosave = Some(AutosaveSetting::OnWindowChange);
4768 })
4769 });
4770 item.is_dirty = true;
4771 });
4772
4773 // Deactivating the window saves the file.
4774 window.simulate_deactivation(cx);
4775 deterministic.run_until_parked();
4776 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
4777
4778 // Autosave on focus change.
4779 item.update(cx, |item, cx| {
4780 cx.focus_self();
4781 cx.update_global(|settings: &mut SettingsStore, cx| {
4782 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4783 settings.autosave = Some(AutosaveSetting::OnFocusChange);
4784 })
4785 });
4786 item.is_dirty = true;
4787 });
4788
4789 // Blurring the item saves the file.
4790 item.update(cx, |_, cx| cx.blur());
4791 deterministic.run_until_parked();
4792 item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
4793
4794 // Deactivating the window still saves the file.
4795 window.simulate_activation(cx);
4796 item.update(cx, |item, cx| {
4797 cx.focus_self();
4798 item.is_dirty = true;
4799 });
4800 window.simulate_deactivation(cx);
4801
4802 deterministic.run_until_parked();
4803 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
4804
4805 // Autosave after delay.
4806 item.update(cx, |item, cx| {
4807 cx.update_global(|settings: &mut SettingsStore, cx| {
4808 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4809 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
4810 })
4811 });
4812 item.is_dirty = true;
4813 cx.emit(TestItemEvent::Edit);
4814 });
4815
4816 // Delay hasn't fully expired, so the file is still dirty and unsaved.
4817 deterministic.advance_clock(Duration::from_millis(250));
4818 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
4819
4820 // After delay expires, the file is saved.
4821 deterministic.advance_clock(Duration::from_millis(250));
4822 item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
4823
4824 // Autosave on focus change, ensuring closing the tab counts as such.
4825 item.update(cx, |item, cx| {
4826 cx.update_global(|settings: &mut SettingsStore, cx| {
4827 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4828 settings.autosave = Some(AutosaveSetting::OnFocusChange);
4829 })
4830 });
4831 item.is_dirty = true;
4832 });
4833
4834 pane.update(cx, |pane, cx| {
4835 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
4836 })
4837 .await
4838 .unwrap();
4839 assert!(!window.has_pending_prompt(cx));
4840 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
4841
4842 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
4843 workspace.update(cx, |workspace, cx| {
4844 workspace.add_item(Box::new(item.clone()), cx);
4845 });
4846 item.update(cx, |item, cx| {
4847 item.project_items[0].update(cx, |item, _| {
4848 item.entry_id = None;
4849 });
4850 item.is_dirty = true;
4851 cx.blur();
4852 });
4853 deterministic.run_until_parked();
4854 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
4855
4856 // Ensure autosave is prevented for deleted files also when closing the buffer.
4857 let _close_items = pane.update(cx, |pane, cx| {
4858 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
4859 });
4860 deterministic.run_until_parked();
4861 assert!(window.has_pending_prompt(cx));
4862 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
4863 }
4864
4865 #[gpui::test]
4866 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
4867 init_test(cx);
4868
4869 let fs = FakeFs::new(cx.background());
4870
4871 let project = Project::test(fs, [], cx).await;
4872 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
4873 let workspace = window.root(cx);
4874
4875 let item = window.add_view(cx, |cx| {
4876 TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4877 });
4878 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4879 let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone());
4880 let toolbar_notify_count = Rc::new(RefCell::new(0));
4881
4882 workspace.update(cx, |workspace, cx| {
4883 workspace.add_item(Box::new(item.clone()), cx);
4884 let toolbar_notification_count = toolbar_notify_count.clone();
4885 cx.observe(&toolbar, move |_, _, _| {
4886 *toolbar_notification_count.borrow_mut() += 1
4887 })
4888 .detach();
4889 });
4890
4891 pane.read_with(cx, |pane, _| {
4892 assert!(!pane.can_navigate_backward());
4893 assert!(!pane.can_navigate_forward());
4894 });
4895
4896 item.update(cx, |item, cx| {
4897 item.set_state("one".to_string(), cx);
4898 });
4899
4900 // Toolbar must be notified to re-render the navigation buttons
4901 assert_eq!(*toolbar_notify_count.borrow(), 1);
4902
4903 pane.read_with(cx, |pane, _| {
4904 assert!(pane.can_navigate_backward());
4905 assert!(!pane.can_navigate_forward());
4906 });
4907
4908 workspace
4909 .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
4910 .await
4911 .unwrap();
4912
4913 assert_eq!(*toolbar_notify_count.borrow(), 3);
4914 pane.read_with(cx, |pane, _| {
4915 assert!(!pane.can_navigate_backward());
4916 assert!(pane.can_navigate_forward());
4917 });
4918 }
4919
4920 #[gpui::test]
4921 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
4922 init_test(cx);
4923 let fs = FakeFs::new(cx.background());
4924
4925 let project = Project::test(fs, [], cx).await;
4926 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
4927 let workspace = window.root(cx);
4928
4929 let panel = workspace.update(cx, |workspace, cx| {
4930 let panel = cx.add_view(|_| TestPanel::new(DockPosition::Right));
4931 workspace.add_panel(panel.clone(), cx);
4932
4933 workspace
4934 .right_dock()
4935 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
4936
4937 panel
4938 });
4939
4940 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4941 pane.update(cx, |pane, cx| {
4942 let item = cx.add_view(|_| TestItem::new());
4943 pane.add_item(Box::new(item), true, true, None, cx);
4944 });
4945
4946 // Transfer focus from center to panel
4947 workspace.update(cx, |workspace, cx| {
4948 workspace.toggle_panel_focus::<TestPanel>(cx);
4949 });
4950
4951 workspace.read_with(cx, |workspace, cx| {
4952 assert!(workspace.right_dock().read(cx).is_open());
4953 assert!(!panel.is_zoomed(cx));
4954 assert!(panel.has_focus(cx));
4955 });
4956
4957 // Transfer focus from panel to center
4958 workspace.update(cx, |workspace, cx| {
4959 workspace.toggle_panel_focus::<TestPanel>(cx);
4960 });
4961
4962 workspace.read_with(cx, |workspace, cx| {
4963 assert!(workspace.right_dock().read(cx).is_open());
4964 assert!(!panel.is_zoomed(cx));
4965 assert!(!panel.has_focus(cx));
4966 });
4967
4968 // Close the dock
4969 workspace.update(cx, |workspace, cx| {
4970 workspace.toggle_dock(DockPosition::Right, cx);
4971 });
4972
4973 workspace.read_with(cx, |workspace, cx| {
4974 assert!(!workspace.right_dock().read(cx).is_open());
4975 assert!(!panel.is_zoomed(cx));
4976 assert!(!panel.has_focus(cx));
4977 });
4978
4979 // Open the dock
4980 workspace.update(cx, |workspace, cx| {
4981 workspace.toggle_dock(DockPosition::Right, cx);
4982 });
4983
4984 workspace.read_with(cx, |workspace, cx| {
4985 assert!(workspace.right_dock().read(cx).is_open());
4986 assert!(!panel.is_zoomed(cx));
4987 assert!(panel.has_focus(cx));
4988 });
4989
4990 // Focus and zoom panel
4991 panel.update(cx, |panel, cx| {
4992 cx.focus_self();
4993 panel.set_zoomed(true, cx)
4994 });
4995
4996 workspace.read_with(cx, |workspace, cx| {
4997 assert!(workspace.right_dock().read(cx).is_open());
4998 assert!(panel.is_zoomed(cx));
4999 assert!(panel.has_focus(cx));
5000 });
5001
5002 // Transfer focus to the center closes the dock
5003 workspace.update(cx, |workspace, cx| {
5004 workspace.toggle_panel_focus::<TestPanel>(cx);
5005 });
5006
5007 workspace.read_with(cx, |workspace, cx| {
5008 assert!(!workspace.right_dock().read(cx).is_open());
5009 assert!(panel.is_zoomed(cx));
5010 assert!(!panel.has_focus(cx));
5011 });
5012
5013 // Transferring focus back to the panel keeps it zoomed
5014 workspace.update(cx, |workspace, cx| {
5015 workspace.toggle_panel_focus::<TestPanel>(cx);
5016 });
5017
5018 workspace.read_with(cx, |workspace, cx| {
5019 assert!(workspace.right_dock().read(cx).is_open());
5020 assert!(panel.is_zoomed(cx));
5021 assert!(panel.has_focus(cx));
5022 });
5023
5024 // Close the dock while it is zoomed
5025 workspace.update(cx, |workspace, cx| {
5026 workspace.toggle_dock(DockPosition::Right, cx)
5027 });
5028
5029 workspace.read_with(cx, |workspace, cx| {
5030 assert!(!workspace.right_dock().read(cx).is_open());
5031 assert!(panel.is_zoomed(cx));
5032 assert!(workspace.zoomed.is_none());
5033 assert!(!panel.has_focus(cx));
5034 });
5035
5036 // Opening the dock, when it's zoomed, retains focus
5037 workspace.update(cx, |workspace, cx| {
5038 workspace.toggle_dock(DockPosition::Right, cx)
5039 });
5040
5041 workspace.read_with(cx, |workspace, cx| {
5042 assert!(workspace.right_dock().read(cx).is_open());
5043 assert!(panel.is_zoomed(cx));
5044 assert!(workspace.zoomed.is_some());
5045 assert!(panel.has_focus(cx));
5046 });
5047
5048 // Unzoom and close the panel, zoom the active pane.
5049 panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
5050 workspace.update(cx, |workspace, cx| {
5051 workspace.toggle_dock(DockPosition::Right, cx)
5052 });
5053 pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
5054
5055 // Opening a dock unzooms the pane.
5056 workspace.update(cx, |workspace, cx| {
5057 workspace.toggle_dock(DockPosition::Right, cx)
5058 });
5059 workspace.read_with(cx, |workspace, cx| {
5060 let pane = pane.read(cx);
5061 assert!(!pane.is_zoomed());
5062 assert!(!pane.has_focus());
5063 assert!(workspace.right_dock().read(cx).is_open());
5064 assert!(workspace.zoomed.is_none());
5065 });
5066 }
5067
5068 #[gpui::test]
5069 async fn test_panels(cx: &mut gpui::TestAppContext) {
5070 init_test(cx);
5071 let fs = FakeFs::new(cx.background());
5072
5073 let project = Project::test(fs, [], cx).await;
5074 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
5075 let workspace = window.root(cx);
5076
5077 let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
5078 // Add panel_1 on the left, panel_2 on the right.
5079 let panel_1 = cx.add_view(|_| TestPanel::new(DockPosition::Left));
5080 workspace.add_panel(panel_1.clone(), cx);
5081 workspace
5082 .left_dock()
5083 .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
5084 let panel_2 = cx.add_view(|_| TestPanel::new(DockPosition::Right));
5085 workspace.add_panel(panel_2.clone(), cx);
5086 workspace
5087 .right_dock()
5088 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
5089
5090 let left_dock = workspace.left_dock();
5091 assert_eq!(
5092 left_dock.read(cx).visible_panel().unwrap().id(),
5093 panel_1.id()
5094 );
5095 assert_eq!(
5096 left_dock.read(cx).active_panel_size(cx).unwrap(),
5097 panel_1.size(cx)
5098 );
5099
5100 left_dock.update(cx, |left_dock, cx| {
5101 left_dock.resize_active_panel(Some(1337.), cx)
5102 });
5103 assert_eq!(
5104 workspace
5105 .right_dock()
5106 .read(cx)
5107 .visible_panel()
5108 .unwrap()
5109 .id(),
5110 panel_2.id()
5111 );
5112
5113 (panel_1, panel_2)
5114 });
5115
5116 // Move panel_1 to the right
5117 panel_1.update(cx, |panel_1, cx| {
5118 panel_1.set_position(DockPosition::Right, cx)
5119 });
5120
5121 workspace.update(cx, |workspace, cx| {
5122 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
5123 // Since it was the only panel on the left, the left dock should now be closed.
5124 assert!(!workspace.left_dock().read(cx).is_open());
5125 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
5126 let right_dock = workspace.right_dock();
5127 assert_eq!(
5128 right_dock.read(cx).visible_panel().unwrap().id(),
5129 panel_1.id()
5130 );
5131 assert_eq!(right_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
5132
5133 // Now we move panel_2Β to the left
5134 panel_2.set_position(DockPosition::Left, cx);
5135 });
5136
5137 workspace.update(cx, |workspace, cx| {
5138 // Since panel_2 was not visible on the right, we don't open the left dock.
5139 assert!(!workspace.left_dock().read(cx).is_open());
5140 // And the right dock is unaffected in it's displaying of panel_1
5141 assert!(workspace.right_dock().read(cx).is_open());
5142 assert_eq!(
5143 workspace
5144 .right_dock()
5145 .read(cx)
5146 .visible_panel()
5147 .unwrap()
5148 .id(),
5149 panel_1.id()
5150 );
5151 });
5152
5153 // Move panel_1 back to the left
5154 panel_1.update(cx, |panel_1, cx| {
5155 panel_1.set_position(DockPosition::Left, cx)
5156 });
5157
5158 workspace.update(cx, |workspace, cx| {
5159 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
5160 let left_dock = workspace.left_dock();
5161 assert!(left_dock.read(cx).is_open());
5162 assert_eq!(
5163 left_dock.read(cx).visible_panel().unwrap().id(),
5164 panel_1.id()
5165 );
5166 assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
5167 // And right the dock should be closed as it no longer has any panels.
5168 assert!(!workspace.right_dock().read(cx).is_open());
5169
5170 // Now we move panel_1 to the bottom
5171 panel_1.set_position(DockPosition::Bottom, cx);
5172 });
5173
5174 workspace.update(cx, |workspace, cx| {
5175 // Since panel_1 was visible on the left, we close the left dock.
5176 assert!(!workspace.left_dock().read(cx).is_open());
5177 // The bottom dock is sized based on the panel's default size,
5178 // since the panel orientation changed from vertical to horizontal.
5179 let bottom_dock = workspace.bottom_dock();
5180 assert_eq!(
5181 bottom_dock.read(cx).active_panel_size(cx).unwrap(),
5182 panel_1.size(cx),
5183 );
5184 // Close bottom dock and move panel_1 back to the left.
5185 bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
5186 panel_1.set_position(DockPosition::Left, cx);
5187 });
5188
5189 // Emit activated event on panel 1
5190 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Activated));
5191
5192 // Now the left dock is open and panel_1 is active and focused.
5193 workspace.read_with(cx, |workspace, cx| {
5194 let left_dock = workspace.left_dock();
5195 assert!(left_dock.read(cx).is_open());
5196 assert_eq!(
5197 left_dock.read(cx).visible_panel().unwrap().id(),
5198 panel_1.id()
5199 );
5200 assert!(panel_1.is_focused(cx));
5201 });
5202
5203 // Emit closed event on panel 2, which is not active
5204 panel_2.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed));
5205
5206 // Wo don't close the left dock, because panel_2 wasn't the active panel
5207 workspace.read_with(cx, |workspace, cx| {
5208 let left_dock = workspace.left_dock();
5209 assert!(left_dock.read(cx).is_open());
5210 assert_eq!(
5211 left_dock.read(cx).visible_panel().unwrap().id(),
5212 panel_1.id()
5213 );
5214 });
5215
5216 // Emitting a ZoomIn event shows the panel as zoomed.
5217 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomIn));
5218 workspace.read_with(cx, |workspace, _| {
5219 assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
5220 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
5221 });
5222
5223 // Move panel to another dock while it is zoomed
5224 panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
5225 workspace.read_with(cx, |workspace, _| {
5226 assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
5227 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5228 });
5229
5230 // If focus is transferred to another view that's not a panel or another pane, we still show
5231 // the panel as zoomed.
5232 let focus_receiver = window.add_view(cx, |_| EmptyView);
5233 focus_receiver.update(cx, |_, cx| cx.focus_self());
5234 workspace.read_with(cx, |workspace, _| {
5235 assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
5236 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5237 });
5238
5239 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
5240 workspace.update(cx, |_, cx| cx.focus_self());
5241 workspace.read_with(cx, |workspace, _| {
5242 assert_eq!(workspace.zoomed, None);
5243 assert_eq!(workspace.zoomed_position, None);
5244 });
5245
5246 // If focus is transferred again to another view that's not a panel or a pane, we won't
5247 // show the panel as zoomed because it wasn't zoomed before.
5248 focus_receiver.update(cx, |_, cx| cx.focus_self());
5249 workspace.read_with(cx, |workspace, _| {
5250 assert_eq!(workspace.zoomed, None);
5251 assert_eq!(workspace.zoomed_position, None);
5252 });
5253
5254 // When focus is transferred back to the panel, it is zoomed again.
5255 panel_1.update(cx, |_, cx| cx.focus_self());
5256 workspace.read_with(cx, |workspace, _| {
5257 assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
5258 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5259 });
5260
5261 // Emitting a ZoomOut event unzooms the panel.
5262 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomOut));
5263 workspace.read_with(cx, |workspace, _| {
5264 assert_eq!(workspace.zoomed, None);
5265 assert_eq!(workspace.zoomed_position, None);
5266 });
5267
5268 // Emit closed event on panel 1, which is active
5269 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed));
5270
5271 // Now the left dock is closed, because panel_1 was the active panel
5272 workspace.read_with(cx, |workspace, cx| {
5273 let right_dock = workspace.right_dock();
5274 assert!(!right_dock.read(cx).is_open());
5275 });
5276 }
5277
5278 pub fn init_test(cx: &mut TestAppContext) {
5279 cx.foreground().forbid_parking();
5280 cx.update(|cx| {
5281 cx.set_global(SettingsStore::test(cx));
5282 theme::init((), cx);
5283 language::init(cx);
5284 crate::init_settings(cx);
5285 Project::init_settings(cx);
5286 });
5287 }
5288}