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