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