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