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, Option<PathBuf>)> {
950 let mut abs_paths_opened: HashMap<PathBuf, HashSet<ProjectPath>> = HashMap::default();
951 let mut history: HashMap<ProjectPath, (Option<PathBuf>, usize)> = HashMap::default();
952 for pane in &self.panes {
953 let pane = pane.read(cx);
954 pane.nav_history()
955 .for_each_entry(cx, |entry, (project_path, fs_path)| {
956 if let Some(fs_path) = &fs_path {
957 abs_paths_opened
958 .entry(fs_path.clone())
959 .or_default()
960 .insert(project_path.clone());
961 }
962 let timestamp = entry.timestamp;
963 match history.entry(project_path) {
964 hash_map::Entry::Occupied(mut entry) => {
965 let (old_fs_path, old_timestamp) = entry.get();
966 if ×tamp > old_timestamp {
967 assert_eq!(&fs_path, old_fs_path, "Inconsistent nav history");
968 entry.insert((fs_path, timestamp));
969 }
970 }
971 hash_map::Entry::Vacant(entry) => {
972 entry.insert((fs_path, timestamp));
973 }
974 }
975 });
976 }
977
978 history
979 .into_iter()
980 .sorted_by_key(|(_, (_, timestamp))| *timestamp)
981 .map(|(project_path, (fs_path, _))| (project_path, fs_path))
982 .rev()
983 .filter(|(history_path, abs_path)| {
984 let latest_project_path_opened = abs_path
985 .as_ref()
986 .and_then(|abs_path| abs_paths_opened.get(abs_path))
987 .and_then(|project_paths| {
988 project_paths
989 .iter()
990 .max_by(|b1, b2| b1.worktree_id.cmp(&b2.worktree_id))
991 });
992
993 match latest_project_path_opened {
994 Some(latest_project_path_opened) => latest_project_path_opened == history_path,
995 None => true,
996 }
997 })
998 .take(limit.unwrap_or(usize::MAX))
999 .collect()
1000 }
1001
1002 pub fn client(&self) -> &Client {
1003 &self.app_state.client
1004 }
1005
1006 pub fn set_titlebar_item(&mut self, item: AnyViewHandle, cx: &mut ViewContext<Self>) {
1007 self.titlebar_item = Some(item);
1008 cx.notify();
1009 }
1010
1011 pub fn titlebar_item(&self) -> Option<AnyViewHandle> {
1012 self.titlebar_item.clone()
1013 }
1014
1015 /// Call the given callback with a workspace whose project is local.
1016 ///
1017 /// If the given workspace has a local project, then it will be passed
1018 /// to the callback. Otherwise, a new empty window will be created.
1019 pub fn with_local_workspace<T, F>(
1020 &mut self,
1021 cx: &mut ViewContext<Self>,
1022 callback: F,
1023 ) -> Task<Result<T>>
1024 where
1025 T: 'static,
1026 F: 'static + FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
1027 {
1028 if self.project.read(cx).is_local() {
1029 Task::Ready(Some(Ok(callback(self, cx))))
1030 } else {
1031 let task = Self::new_local(Vec::new(), self.app_state.clone(), None, cx);
1032 cx.spawn(|_vh, mut cx| async move {
1033 let (workspace, _) = task.await;
1034 workspace.update(&mut cx, callback)
1035 })
1036 }
1037 }
1038
1039 pub fn worktrees<'a>(
1040 &self,
1041 cx: &'a AppContext,
1042 ) -> impl 'a + Iterator<Item = ModelHandle<Worktree>> {
1043 self.project.read(cx).worktrees(cx)
1044 }
1045
1046 pub fn visible_worktrees<'a>(
1047 &self,
1048 cx: &'a AppContext,
1049 ) -> impl 'a + Iterator<Item = ModelHandle<Worktree>> {
1050 self.project.read(cx).visible_worktrees(cx)
1051 }
1052
1053 pub fn worktree_scans_complete(&self, cx: &AppContext) -> impl Future<Output = ()> + 'static {
1054 let futures = self
1055 .worktrees(cx)
1056 .filter_map(|worktree| worktree.read(cx).as_local())
1057 .map(|worktree| worktree.scan_complete())
1058 .collect::<Vec<_>>();
1059 async move {
1060 for future in futures {
1061 future.await;
1062 }
1063 }
1064 }
1065
1066 pub fn close_global(_: &CloseWindow, cx: &mut AppContext) {
1067 cx.spawn(|mut cx| async move {
1068 let id = cx
1069 .window_ids()
1070 .into_iter()
1071 .find(|&id| cx.window_is_active(id));
1072 if let Some(id) = id {
1073 //This can only get called when the window's project connection has been lost
1074 //so we don't need to prompt the user for anything and instead just close the window
1075 cx.remove_window(id);
1076 }
1077 })
1078 .detach();
1079 }
1080
1081 pub fn close(
1082 &mut self,
1083 _: &CloseWindow,
1084 cx: &mut ViewContext<Self>,
1085 ) -> Option<Task<Result<()>>> {
1086 let window_id = cx.window_id();
1087 let prepare = self.prepare_to_close(false, cx);
1088 Some(cx.spawn(|_, mut cx| async move {
1089 if prepare.await? {
1090 cx.remove_window(window_id);
1091 }
1092 Ok(())
1093 }))
1094 }
1095
1096 pub fn prepare_to_close(
1097 &mut self,
1098 quitting: bool,
1099 cx: &mut ViewContext<Self>,
1100 ) -> Task<Result<bool>> {
1101 let active_call = self.active_call().cloned();
1102 let window_id = cx.window_id();
1103
1104 cx.spawn(|this, mut cx| async move {
1105 let workspace_count = cx
1106 .window_ids()
1107 .into_iter()
1108 .filter_map(|window_id| cx.root_view(window_id)?.clone().downcast::<Workspace>())
1109 .count();
1110
1111 if let Some(active_call) = active_call {
1112 if !quitting
1113 && workspace_count == 1
1114 && active_call.read_with(&cx, |call, _| call.room().is_some())
1115 {
1116 let answer = cx.prompt(
1117 window_id,
1118 PromptLevel::Warning,
1119 "Do you want to leave the current call?",
1120 &["Close window and hang up", "Cancel"],
1121 );
1122
1123 if let Some(mut answer) = answer {
1124 if answer.next().await == Some(1) {
1125 return anyhow::Ok(false);
1126 } else {
1127 active_call
1128 .update(&mut cx, |call, cx| call.hang_up(cx))
1129 .await
1130 .log_err();
1131 }
1132 }
1133 }
1134 }
1135
1136 Ok(this
1137 .update(&mut cx, |this, cx| this.save_all_internal(true, cx))?
1138 .await?)
1139 })
1140 }
1141
1142 fn save_all(&mut self, _: &SaveAll, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
1143 let save_all = self.save_all_internal(false, cx);
1144 Some(cx.foreground().spawn(async move {
1145 save_all.await?;
1146 Ok(())
1147 }))
1148 }
1149
1150 fn save_all_internal(
1151 &mut self,
1152 should_prompt_to_save: bool,
1153 cx: &mut ViewContext<Self>,
1154 ) -> Task<Result<bool>> {
1155 if self.project.read(cx).is_read_only() {
1156 return Task::ready(Ok(true));
1157 }
1158
1159 let dirty_items = self
1160 .panes
1161 .iter()
1162 .flat_map(|pane| {
1163 pane.read(cx).items().filter_map(|item| {
1164 if item.is_dirty(cx) {
1165 Some((pane.downgrade(), item.boxed_clone()))
1166 } else {
1167 None
1168 }
1169 })
1170 })
1171 .collect::<Vec<_>>();
1172
1173 let project = self.project.clone();
1174 cx.spawn(|_, mut cx| async move {
1175 for (pane, item) in dirty_items {
1176 let (singleton, project_entry_ids) =
1177 cx.read(|cx| (item.is_singleton(cx), item.project_entry_ids(cx)));
1178 if singleton || !project_entry_ids.is_empty() {
1179 if let Some(ix) =
1180 pane.read_with(&cx, |pane, _| pane.index_for_item(item.as_ref()))?
1181 {
1182 if !Pane::save_item(
1183 project.clone(),
1184 &pane,
1185 ix,
1186 &*item,
1187 should_prompt_to_save,
1188 &mut cx,
1189 )
1190 .await?
1191 {
1192 return Ok(false);
1193 }
1194 }
1195 }
1196 }
1197 Ok(true)
1198 })
1199 }
1200
1201 pub fn open(&mut self, _: &Open, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
1202 let mut paths = cx.prompt_for_paths(PathPromptOptions {
1203 files: true,
1204 directories: true,
1205 multiple: true,
1206 });
1207
1208 Some(cx.spawn(|this, mut cx| async move {
1209 if let Some(paths) = paths.recv().await.flatten() {
1210 if let Some(task) = this
1211 .update(&mut cx, |this, cx| this.open_workspace_for_paths(paths, cx))
1212 .log_err()
1213 {
1214 task.await?
1215 }
1216 }
1217 Ok(())
1218 }))
1219 }
1220
1221 pub fn open_workspace_for_paths(
1222 &mut self,
1223 paths: Vec<PathBuf>,
1224 cx: &mut ViewContext<Self>,
1225 ) -> Task<Result<()>> {
1226 let window_id = cx.window_id();
1227 let is_remote = self.project.read(cx).is_remote();
1228 let has_worktree = self.project.read(cx).worktrees(cx).next().is_some();
1229 let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx));
1230 let close_task = if is_remote || has_worktree || has_dirty_items {
1231 None
1232 } else {
1233 Some(self.prepare_to_close(false, cx))
1234 };
1235 let app_state = self.app_state.clone();
1236
1237 cx.spawn(|_, mut cx| async move {
1238 let window_id_to_replace = if let Some(close_task) = close_task {
1239 if !close_task.await? {
1240 return Ok(());
1241 }
1242 Some(window_id)
1243 } else {
1244 None
1245 };
1246 cx.update(|cx| open_paths(&paths, &app_state, window_id_to_replace, cx))
1247 .await?;
1248 Ok(())
1249 })
1250 }
1251
1252 #[allow(clippy::type_complexity)]
1253 pub fn open_paths(
1254 &mut self,
1255 mut abs_paths: Vec<PathBuf>,
1256 visible: bool,
1257 cx: &mut ViewContext<Self>,
1258 ) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
1259 log::info!("open paths {:?}", abs_paths);
1260
1261 let fs = self.app_state.fs.clone();
1262
1263 // Sort the paths to ensure we add worktrees for parents before their children.
1264 abs_paths.sort_unstable();
1265 cx.spawn(|this, mut cx| async move {
1266 let mut project_paths = Vec::new();
1267 for path in &abs_paths {
1268 if let Some(project_path) = this
1269 .update(&mut cx, |this, cx| {
1270 Workspace::project_path_for_path(this.project.clone(), path, visible, cx)
1271 })
1272 .log_err()
1273 {
1274 project_paths.push(project_path.await.log_err());
1275 } else {
1276 project_paths.push(None);
1277 }
1278 }
1279
1280 let tasks = abs_paths
1281 .iter()
1282 .cloned()
1283 .zip(project_paths.into_iter())
1284 .map(|(abs_path, project_path)| {
1285 let this = this.clone();
1286 cx.spawn(|mut cx| {
1287 let fs = fs.clone();
1288 async move {
1289 let (_worktree, project_path) = project_path?;
1290 if fs.is_file(&abs_path).await {
1291 Some(
1292 this.update(&mut cx, |this, cx| {
1293 this.open_path(project_path, None, true, cx)
1294 })
1295 .log_err()?
1296 .await,
1297 )
1298 } else {
1299 None
1300 }
1301 }
1302 })
1303 })
1304 .collect::<Vec<_>>();
1305
1306 futures::future::join_all(tasks).await
1307 })
1308 }
1309
1310 pub fn absolute_path(&self, project_path: &ProjectPath, cx: &AppContext) -> Option<PathBuf> {
1311 let workspace_root = self
1312 .project()
1313 .read(cx)
1314 .worktree_for_id(project_path.worktree_id, cx)?
1315 .read(cx)
1316 .abs_path();
1317 let project_path = project_path.path.as_ref();
1318
1319 Some(if project_path == Path::new("") {
1320 workspace_root.to_path_buf()
1321 } else {
1322 workspace_root.join(project_path)
1323 })
1324 }
1325
1326 fn add_folder_to_project(&mut self, _: &AddFolderToProject, cx: &mut ViewContext<Self>) {
1327 let mut paths = cx.prompt_for_paths(PathPromptOptions {
1328 files: false,
1329 directories: true,
1330 multiple: true,
1331 });
1332 cx.spawn(|this, mut cx| async move {
1333 if let Some(paths) = paths.recv().await.flatten() {
1334 let results = this
1335 .update(&mut cx, |this, cx| this.open_paths(paths, true, cx))?
1336 .await;
1337 for result in results.into_iter().flatten() {
1338 result.log_err();
1339 }
1340 }
1341 anyhow::Ok(())
1342 })
1343 .detach_and_log_err(cx);
1344 }
1345
1346 fn project_path_for_path(
1347 project: ModelHandle<Project>,
1348 abs_path: &Path,
1349 visible: bool,
1350 cx: &mut AppContext,
1351 ) -> Task<Result<(ModelHandle<Worktree>, ProjectPath)>> {
1352 let entry = project.update(cx, |project, cx| {
1353 project.find_or_create_local_worktree(abs_path, visible, cx)
1354 });
1355 cx.spawn(|cx| async move {
1356 let (worktree, path) = entry.await?;
1357 let worktree_id = worktree.read_with(&cx, |t, _| t.id());
1358 Ok((
1359 worktree,
1360 ProjectPath {
1361 worktree_id,
1362 path: path.into(),
1363 },
1364 ))
1365 })
1366 }
1367
1368 /// Returns the modal that was toggled closed if it was open.
1369 pub fn toggle_modal<V, F>(
1370 &mut self,
1371 cx: &mut ViewContext<Self>,
1372 add_view: F,
1373 ) -> Option<ViewHandle<V>>
1374 where
1375 V: 'static + Modal,
1376 F: FnOnce(&mut Self, &mut ViewContext<Self>) -> ViewHandle<V>,
1377 {
1378 cx.notify();
1379 // Whatever modal was visible is getting clobbered. If its the same type as V, then return
1380 // it. Otherwise, create a new modal and set it as active.
1381 let already_open_modal = self.modal.take().and_then(|modal| modal.downcast::<V>());
1382 if let Some(already_open_modal) = already_open_modal {
1383 cx.focus_self();
1384 Some(already_open_modal)
1385 } else {
1386 let modal = add_view(self, cx);
1387 cx.subscribe(&modal, |this, _, event, cx| {
1388 if V::dismiss_on_event(event) {
1389 this.dismiss_modal(cx);
1390 }
1391 })
1392 .detach();
1393 cx.focus(&modal);
1394 self.modal = Some(modal.into_any());
1395 None
1396 }
1397 }
1398
1399 pub fn modal<V: 'static + View>(&self) -> Option<ViewHandle<V>> {
1400 self.modal
1401 .as_ref()
1402 .and_then(|modal| modal.clone().downcast::<V>())
1403 }
1404
1405 pub fn dismiss_modal(&mut self, cx: &mut ViewContext<Self>) {
1406 if self.modal.take().is_some() {
1407 cx.focus(&self.active_pane);
1408 cx.notify();
1409 }
1410 }
1411
1412 pub fn items<'a>(
1413 &'a self,
1414 cx: &'a AppContext,
1415 ) -> impl 'a + Iterator<Item = &Box<dyn ItemHandle>> {
1416 self.panes.iter().flat_map(|pane| pane.read(cx).items())
1417 }
1418
1419 pub fn item_of_type<T: Item>(&self, cx: &AppContext) -> Option<ViewHandle<T>> {
1420 self.items_of_type(cx).max_by_key(|item| item.id())
1421 }
1422
1423 pub fn items_of_type<'a, T: Item>(
1424 &'a self,
1425 cx: &'a AppContext,
1426 ) -> impl 'a + Iterator<Item = ViewHandle<T>> {
1427 self.panes
1428 .iter()
1429 .flat_map(|pane| pane.read(cx).items_of_type())
1430 }
1431
1432 pub fn active_item(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
1433 self.active_pane().read(cx).active_item()
1434 }
1435
1436 fn active_project_path(&self, cx: &ViewContext<Self>) -> Option<ProjectPath> {
1437 self.active_item(cx).and_then(|item| item.project_path(cx))
1438 }
1439
1440 pub fn save_active_item(
1441 &mut self,
1442 force_name_change: bool,
1443 cx: &mut ViewContext<Self>,
1444 ) -> Task<Result<()>> {
1445 let project = self.project.clone();
1446 if let Some(item) = self.active_item(cx) {
1447 if !force_name_change && item.can_save(cx) {
1448 if item.has_conflict(cx) {
1449 const CONFLICT_MESSAGE: &str = "This file has changed on disk since you started editing it. Do you want to overwrite it?";
1450
1451 let mut answer = cx.prompt(
1452 PromptLevel::Warning,
1453 CONFLICT_MESSAGE,
1454 &["Overwrite", "Cancel"],
1455 );
1456 cx.spawn(|this, mut cx| async move {
1457 let answer = answer.recv().await;
1458 if answer == Some(0) {
1459 this.update(&mut cx, |this, cx| item.save(this.project.clone(), cx))?
1460 .await?;
1461 }
1462 Ok(())
1463 })
1464 } else {
1465 item.save(self.project.clone(), cx)
1466 }
1467 } else if item.is_singleton(cx) {
1468 let worktree = self.worktrees(cx).next();
1469 let start_abs_path = worktree
1470 .and_then(|w| w.read(cx).as_local())
1471 .map_or(Path::new(""), |w| w.abs_path())
1472 .to_path_buf();
1473 let mut abs_path = cx.prompt_for_new_path(&start_abs_path);
1474 cx.spawn(|this, mut cx| async move {
1475 if let Some(abs_path) = abs_path.recv().await.flatten() {
1476 this.update(&mut cx, |_, cx| item.save_as(project, abs_path, cx))?
1477 .await?;
1478 }
1479 Ok(())
1480 })
1481 } else {
1482 Task::ready(Ok(()))
1483 }
1484 } else {
1485 Task::ready(Ok(()))
1486 }
1487 }
1488
1489 pub fn toggle_dock(
1490 &mut self,
1491 dock_side: DockPosition,
1492 focus: bool,
1493 cx: &mut ViewContext<Self>,
1494 ) {
1495 let dock = match dock_side {
1496 DockPosition::Left => &self.left_dock,
1497 DockPosition::Bottom => &self.bottom_dock,
1498 DockPosition::Right => &self.right_dock,
1499 };
1500 dock.update(cx, |dock, cx| {
1501 let open = !dock.is_open();
1502 dock.set_open(open, cx);
1503 });
1504
1505 if dock.read(cx).is_open() && focus {
1506 cx.focus(dock);
1507 } else {
1508 cx.focus_self();
1509 }
1510 cx.notify();
1511 self.serialize_workspace(cx);
1512 }
1513
1514 pub fn toggle_panel(
1515 &mut self,
1516 position: DockPosition,
1517 panel_index: usize,
1518 cx: &mut ViewContext<Self>,
1519 ) {
1520 let dock = match position {
1521 DockPosition::Left => &mut self.left_dock,
1522 DockPosition::Bottom => &mut self.bottom_dock,
1523 DockPosition::Right => &mut self.right_dock,
1524 };
1525 let active_item = dock.update(cx, move |dock, cx| {
1526 if dock.is_open() && dock.active_panel_index() == panel_index {
1527 dock.set_open(false, cx);
1528 None
1529 } else {
1530 dock.set_open(true, cx);
1531 dock.activate_panel(panel_index, cx);
1532 dock.active_panel().cloned()
1533 }
1534 });
1535
1536 if let Some(active_item) = active_item {
1537 if active_item.has_focus(cx) {
1538 cx.focus_self();
1539 } else {
1540 cx.focus(active_item.as_any());
1541 }
1542 } else {
1543 cx.focus_self();
1544 }
1545
1546 self.serialize_workspace(cx);
1547
1548 cx.notify();
1549 }
1550
1551 pub fn toggle_panel_focus<T: Panel>(&mut self, cx: &mut ViewContext<Self>) {
1552 for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
1553 if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
1554 let active_item = dock.update(cx, |dock, cx| {
1555 dock.set_open(true, cx);
1556 dock.activate_panel(panel_index, cx);
1557 dock.active_panel().cloned()
1558 });
1559 if let Some(active_item) = active_item {
1560 if active_item.has_focus(cx) {
1561 cx.focus_self();
1562 } else {
1563 cx.focus(active_item.as_any());
1564 }
1565 }
1566
1567 self.serialize_workspace(cx);
1568 cx.notify();
1569 break;
1570 }
1571 }
1572 }
1573
1574 fn zoom_out(&mut self, cx: &mut ViewContext<Self>) {
1575 for pane in &self.panes {
1576 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
1577 }
1578
1579 self.left_dock.update(cx, |dock, cx| dock.zoom_out(cx));
1580 self.bottom_dock.update(cx, |dock, cx| dock.zoom_out(cx));
1581 self.right_dock.update(cx, |dock, cx| dock.zoom_out(cx));
1582 self.zoomed = None;
1583
1584 cx.notify();
1585 }
1586
1587 fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
1588 let pane = cx.add_view(|cx| {
1589 Pane::new(
1590 self.weak_handle(),
1591 self.app_state.background_actions,
1592 self.pane_history_timestamp.clone(),
1593 cx,
1594 )
1595 });
1596 cx.subscribe(&pane, Self::handle_pane_event).detach();
1597 self.panes.push(pane.clone());
1598 cx.focus(&pane);
1599 cx.emit(Event::PaneAdded(pane.clone()));
1600 pane
1601 }
1602
1603 pub fn add_item_to_center(
1604 &mut self,
1605 item: Box<dyn ItemHandle>,
1606 cx: &mut ViewContext<Self>,
1607 ) -> bool {
1608 if let Some(center_pane) = self.last_active_center_pane.clone() {
1609 if let Some(center_pane) = center_pane.upgrade(cx) {
1610 Pane::add_item(self, ¢er_pane, item, true, true, None, cx);
1611 true
1612 } else {
1613 false
1614 }
1615 } else {
1616 false
1617 }
1618 }
1619
1620 pub fn add_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
1621 let active_pane = self.active_pane().clone();
1622 Pane::add_item(self, &active_pane, item, true, true, None, cx);
1623 }
1624
1625 pub fn open_abs_path(
1626 &mut self,
1627 abs_path: PathBuf,
1628 visible: bool,
1629 cx: &mut ViewContext<Self>,
1630 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
1631 cx.spawn(|workspace, mut cx| async move {
1632 let open_paths_task_result = workspace
1633 .update(&mut cx, |workspace, cx| {
1634 workspace.open_paths(vec![abs_path.clone()], visible, cx)
1635 })
1636 .with_context(|| format!("open abs path {abs_path:?} task spawn"))?
1637 .await;
1638 anyhow::ensure!(
1639 open_paths_task_result.len() == 1,
1640 "open abs path {abs_path:?} task returned incorrect number of results"
1641 );
1642 match open_paths_task_result
1643 .into_iter()
1644 .next()
1645 .expect("ensured single task result")
1646 {
1647 Some(open_result) => {
1648 open_result.with_context(|| format!("open abs path {abs_path:?} task join"))
1649 }
1650 None => anyhow::bail!("open abs path {abs_path:?} task returned None"),
1651 }
1652 })
1653 }
1654
1655 pub fn open_path(
1656 &mut self,
1657 path: impl Into<ProjectPath>,
1658 pane: Option<WeakViewHandle<Pane>>,
1659 focus_item: bool,
1660 cx: &mut ViewContext<Self>,
1661 ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
1662 let pane = pane.unwrap_or_else(|| {
1663 self.last_active_center_pane.clone().unwrap_or_else(|| {
1664 self.panes
1665 .first()
1666 .expect("There must be an active pane")
1667 .downgrade()
1668 })
1669 });
1670
1671 let task = self.load_path(path.into(), cx);
1672 cx.spawn(|this, mut cx| async move {
1673 let (project_entry_id, build_item) = task.await?;
1674 let pane = pane
1675 .upgrade(&cx)
1676 .ok_or_else(|| anyhow!("pane was closed"))?;
1677 this.update(&mut cx, |this, cx| {
1678 Pane::open_item(this, pane, project_entry_id, focus_item, cx, build_item)
1679 })
1680 })
1681 }
1682
1683 pub(crate) fn load_path(
1684 &mut self,
1685 path: ProjectPath,
1686 cx: &mut ViewContext<Self>,
1687 ) -> Task<
1688 Result<(
1689 ProjectEntryId,
1690 impl 'static + FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
1691 )>,
1692 > {
1693 let project = self.project().clone();
1694 let project_item = project.update(cx, |project, cx| project.open_path(path, cx));
1695 cx.spawn(|_, mut cx| async move {
1696 let (project_entry_id, project_item) = project_item.await?;
1697 let build_item = cx.update(|cx| {
1698 cx.default_global::<ProjectItemBuilders>()
1699 .get(&project_item.model_type())
1700 .ok_or_else(|| anyhow!("no item builder for project item"))
1701 .cloned()
1702 })?;
1703 let build_item =
1704 move |cx: &mut ViewContext<Pane>| build_item(project, project_item, cx);
1705 Ok((project_entry_id, build_item))
1706 })
1707 }
1708
1709 pub fn open_project_item<T>(
1710 &mut self,
1711 project_item: ModelHandle<T::Item>,
1712 cx: &mut ViewContext<Self>,
1713 ) -> ViewHandle<T>
1714 where
1715 T: ProjectItem,
1716 {
1717 use project::Item as _;
1718
1719 let entry_id = project_item.read(cx).entry_id(cx);
1720 if let Some(item) = entry_id
1721 .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id, cx))
1722 .and_then(|item| item.downcast())
1723 {
1724 self.activate_item(&item, cx);
1725 return item;
1726 }
1727
1728 let item = cx.add_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
1729 self.add_item(Box::new(item.clone()), cx);
1730 item
1731 }
1732
1733 pub fn open_shared_screen(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
1734 if let Some(shared_screen) = self.shared_screen_for_peer(peer_id, &self.active_pane, cx) {
1735 let pane = self.active_pane.clone();
1736 Pane::add_item(self, &pane, Box::new(shared_screen), false, true, None, cx);
1737 }
1738 }
1739
1740 pub fn activate_item(&mut self, item: &dyn ItemHandle, cx: &mut ViewContext<Self>) -> bool {
1741 let result = self.panes.iter().find_map(|pane| {
1742 pane.read(cx)
1743 .index_for_item(item)
1744 .map(|ix| (pane.clone(), ix))
1745 });
1746 if let Some((pane, ix)) = result {
1747 pane.update(cx, |pane, cx| pane.activate_item(ix, true, true, cx));
1748 true
1749 } else {
1750 false
1751 }
1752 }
1753
1754 fn activate_pane_at_index(&mut self, action: &ActivatePane, cx: &mut ViewContext<Self>) {
1755 let panes = self.center.panes();
1756 if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
1757 cx.focus(&pane);
1758 } else {
1759 self.split_pane(self.active_pane.clone(), SplitDirection::Right, cx);
1760 }
1761 }
1762
1763 pub fn activate_next_pane(&mut self, cx: &mut ViewContext<Self>) {
1764 let panes = self.center.panes();
1765 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
1766 let next_ix = (ix + 1) % panes.len();
1767 let next_pane = panes[next_ix].clone();
1768 cx.focus(&next_pane);
1769 }
1770 }
1771
1772 pub fn activate_previous_pane(&mut self, cx: &mut ViewContext<Self>) {
1773 let panes = self.center.panes();
1774 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
1775 let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
1776 let prev_pane = panes[prev_ix].clone();
1777 cx.focus(&prev_pane);
1778 }
1779 }
1780
1781 fn handle_pane_focused(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
1782 if self.active_pane != pane {
1783 self.active_pane
1784 .update(cx, |pane, cx| pane.set_active(false, cx));
1785 self.active_pane = pane.clone();
1786 self.active_pane
1787 .update(cx, |pane, cx| pane.set_active(true, cx));
1788 self.status_bar.update(cx, |status_bar, cx| {
1789 status_bar.set_active_pane(&self.active_pane, cx);
1790 });
1791 self.active_item_path_changed(cx);
1792 self.last_active_center_pane = Some(pane.downgrade());
1793 }
1794
1795 if pane.read(cx).is_zoomed() {
1796 self.zoomed = Some(pane.downgrade().into_any());
1797 } else {
1798 self.zoomed = None;
1799 }
1800
1801 self.update_followers(
1802 proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView {
1803 id: self.active_item(cx).and_then(|item| {
1804 item.to_followable_item_handle(cx)?
1805 .remote_id(&self.app_state.client, cx)
1806 .map(|id| id.to_proto())
1807 }),
1808 leader_id: self.leader_for_pane(&pane),
1809 }),
1810 cx,
1811 );
1812
1813 cx.notify();
1814 }
1815
1816 fn handle_pane_event(
1817 &mut self,
1818 pane: ViewHandle<Pane>,
1819 event: &pane::Event,
1820 cx: &mut ViewContext<Self>,
1821 ) {
1822 match event {
1823 pane::Event::Split(direction) => {
1824 self.split_pane(pane, *direction, cx);
1825 }
1826 pane::Event::Remove => self.remove_pane(pane, cx),
1827 pane::Event::ActivateItem { local } => {
1828 if *local {
1829 self.unfollow(&pane, cx);
1830 }
1831 if &pane == self.active_pane() {
1832 self.active_item_path_changed(cx);
1833 }
1834 }
1835 pane::Event::ChangeItemTitle => {
1836 if pane == self.active_pane {
1837 self.active_item_path_changed(cx);
1838 }
1839 self.update_window_edited(cx);
1840 }
1841 pane::Event::RemoveItem { item_id } => {
1842 self.update_window_edited(cx);
1843 if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) {
1844 if entry.get().id() == pane.id() {
1845 entry.remove();
1846 }
1847 }
1848 }
1849 pane::Event::Focus => {
1850 self.handle_pane_focused(pane.clone(), cx);
1851 }
1852 pane::Event::ZoomIn => {
1853 if pane == self.active_pane {
1854 self.zoom_out(cx);
1855 pane.update(cx, |pane, cx| pane.set_zoomed(true, cx));
1856 if pane.read(cx).has_focus() {
1857 self.zoomed = Some(pane.downgrade().into_any());
1858 }
1859 cx.notify();
1860 }
1861 }
1862 pane::Event::ZoomOut => self.zoom_out(cx),
1863 }
1864
1865 self.serialize_workspace(cx);
1866 }
1867
1868 pub fn split_pane(
1869 &mut self,
1870 pane: ViewHandle<Pane>,
1871 direction: SplitDirection,
1872 cx: &mut ViewContext<Self>,
1873 ) -> Option<ViewHandle<Pane>> {
1874 let item = pane.read(cx).active_item()?;
1875 let maybe_pane_handle = if let Some(clone) = item.clone_on_split(self.database_id(), cx) {
1876 let new_pane = self.add_pane(cx);
1877 Pane::add_item(self, &new_pane, clone, true, true, None, cx);
1878 self.center.split(&pane, &new_pane, direction).unwrap();
1879 Some(new_pane)
1880 } else {
1881 None
1882 };
1883 cx.notify();
1884 maybe_pane_handle
1885 }
1886
1887 pub fn split_pane_with_item(
1888 &mut self,
1889 pane_to_split: WeakViewHandle<Pane>,
1890 split_direction: SplitDirection,
1891 from: WeakViewHandle<Pane>,
1892 item_id_to_move: usize,
1893 cx: &mut ViewContext<Self>,
1894 ) {
1895 let Some(pane_to_split) = pane_to_split.upgrade(cx) else { return; };
1896 let Some(from) = from.upgrade(cx) else { return; };
1897
1898 let new_pane = self.add_pane(cx);
1899 Pane::move_item(self, from.clone(), new_pane.clone(), item_id_to_move, 0, cx);
1900 self.center
1901 .split(&pane_to_split, &new_pane, split_direction)
1902 .unwrap();
1903 cx.notify();
1904 }
1905
1906 pub fn split_pane_with_project_entry(
1907 &mut self,
1908 pane_to_split: WeakViewHandle<Pane>,
1909 split_direction: SplitDirection,
1910 project_entry: ProjectEntryId,
1911 cx: &mut ViewContext<Self>,
1912 ) -> Option<Task<Result<()>>> {
1913 let pane_to_split = pane_to_split.upgrade(cx)?;
1914 let new_pane = self.add_pane(cx);
1915 self.center
1916 .split(&pane_to_split, &new_pane, split_direction)
1917 .unwrap();
1918
1919 let path = self.project.read(cx).path_for_entry(project_entry, cx)?;
1920 let task = self.open_path(path, Some(new_pane.downgrade()), true, cx);
1921 Some(cx.foreground().spawn(async move {
1922 task.await?;
1923 Ok(())
1924 }))
1925 }
1926
1927 fn remove_pane(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
1928 if self.center.remove(&pane).unwrap() {
1929 self.force_remove_pane(&pane, cx);
1930 self.unfollow(&pane, cx);
1931 self.last_leaders_by_pane.remove(&pane.downgrade());
1932 for removed_item in pane.read(cx).items() {
1933 self.panes_by_item.remove(&removed_item.id());
1934 }
1935
1936 cx.notify();
1937 } else {
1938 self.active_item_path_changed(cx);
1939 }
1940 }
1941
1942 pub fn panes(&self) -> &[ViewHandle<Pane>] {
1943 &self.panes
1944 }
1945
1946 pub fn active_pane(&self) -> &ViewHandle<Pane> {
1947 &self.active_pane
1948 }
1949
1950 fn project_remote_id_changed(&mut self, remote_id: Option<u64>, cx: &mut ViewContext<Self>) {
1951 if let Some(remote_id) = remote_id {
1952 self.remote_entity_subscription = Some(
1953 self.app_state
1954 .client
1955 .add_view_for_remote_entity(remote_id, cx),
1956 );
1957 } else {
1958 self.remote_entity_subscription.take();
1959 }
1960 }
1961
1962 fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
1963 self.leader_state.followers.remove(&peer_id);
1964 if let Some(states_by_pane) = self.follower_states_by_leader.remove(&peer_id) {
1965 for state in states_by_pane.into_values() {
1966 for item in state.items_by_leader_view_id.into_values() {
1967 item.set_leader_replica_id(None, cx);
1968 }
1969 }
1970 }
1971 cx.notify();
1972 }
1973
1974 pub fn toggle_follow(
1975 &mut self,
1976 leader_id: PeerId,
1977 cx: &mut ViewContext<Self>,
1978 ) -> Option<Task<Result<()>>> {
1979 let pane = self.active_pane().clone();
1980
1981 if let Some(prev_leader_id) = self.unfollow(&pane, cx) {
1982 if leader_id == prev_leader_id {
1983 return None;
1984 }
1985 }
1986
1987 self.last_leaders_by_pane
1988 .insert(pane.downgrade(), leader_id);
1989 self.follower_states_by_leader
1990 .entry(leader_id)
1991 .or_default()
1992 .insert(pane.clone(), Default::default());
1993 cx.notify();
1994
1995 let project_id = self.project.read(cx).remote_id()?;
1996 let request = self.app_state.client.request(proto::Follow {
1997 project_id,
1998 leader_id: Some(leader_id),
1999 });
2000
2001 Some(cx.spawn(|this, mut cx| async move {
2002 let response = request.await?;
2003 this.update(&mut cx, |this, _| {
2004 let state = this
2005 .follower_states_by_leader
2006 .get_mut(&leader_id)
2007 .and_then(|states_by_pane| states_by_pane.get_mut(&pane))
2008 .ok_or_else(|| anyhow!("following interrupted"))?;
2009 state.active_view_id = if let Some(active_view_id) = response.active_view_id {
2010 Some(ViewId::from_proto(active_view_id)?)
2011 } else {
2012 None
2013 };
2014 Ok::<_, anyhow::Error>(())
2015 })??;
2016 Self::add_views_from_leader(
2017 this.clone(),
2018 leader_id,
2019 vec![pane],
2020 response.views,
2021 &mut cx,
2022 )
2023 .await?;
2024 this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx))?;
2025 Ok(())
2026 }))
2027 }
2028
2029 pub fn follow_next_collaborator(
2030 &mut self,
2031 _: &FollowNextCollaborator,
2032 cx: &mut ViewContext<Self>,
2033 ) -> Option<Task<Result<()>>> {
2034 let collaborators = self.project.read(cx).collaborators();
2035 let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
2036 let mut collaborators = collaborators.keys().copied();
2037 for peer_id in collaborators.by_ref() {
2038 if peer_id == leader_id {
2039 break;
2040 }
2041 }
2042 collaborators.next()
2043 } else if let Some(last_leader_id) =
2044 self.last_leaders_by_pane.get(&self.active_pane.downgrade())
2045 {
2046 if collaborators.contains_key(last_leader_id) {
2047 Some(*last_leader_id)
2048 } else {
2049 None
2050 }
2051 } else {
2052 None
2053 };
2054
2055 next_leader_id
2056 .or_else(|| collaborators.keys().copied().next())
2057 .and_then(|leader_id| self.toggle_follow(leader_id, cx))
2058 }
2059
2060 pub fn unfollow(
2061 &mut self,
2062 pane: &ViewHandle<Pane>,
2063 cx: &mut ViewContext<Self>,
2064 ) -> Option<PeerId> {
2065 for (leader_id, states_by_pane) in &mut self.follower_states_by_leader {
2066 let leader_id = *leader_id;
2067 if let Some(state) = states_by_pane.remove(pane) {
2068 for (_, item) in state.items_by_leader_view_id {
2069 item.set_leader_replica_id(None, cx);
2070 }
2071
2072 if states_by_pane.is_empty() {
2073 self.follower_states_by_leader.remove(&leader_id);
2074 if let Some(project_id) = self.project.read(cx).remote_id() {
2075 self.app_state
2076 .client
2077 .send(proto::Unfollow {
2078 project_id,
2079 leader_id: Some(leader_id),
2080 })
2081 .log_err();
2082 }
2083 }
2084
2085 cx.notify();
2086 return Some(leader_id);
2087 }
2088 }
2089 None
2090 }
2091
2092 pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
2093 self.follower_states_by_leader.contains_key(&peer_id)
2094 }
2095
2096 pub fn is_followed_by(&self, peer_id: PeerId) -> bool {
2097 self.leader_state.followers.contains(&peer_id)
2098 }
2099
2100 fn render_titlebar(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
2101 // TODO: There should be a better system in place for this
2102 // (https://github.com/zed-industries/zed/issues/1290)
2103 let is_fullscreen = cx.window_is_fullscreen();
2104 let container_theme = if is_fullscreen {
2105 let mut container_theme = theme.workspace.titlebar.container;
2106 container_theme.padding.left = container_theme.padding.right;
2107 container_theme
2108 } else {
2109 theme.workspace.titlebar.container
2110 };
2111
2112 enum TitleBar {}
2113 MouseEventHandler::<TitleBar, _>::new(0, cx, |_, cx| {
2114 Stack::new()
2115 .with_children(
2116 self.titlebar_item
2117 .as_ref()
2118 .map(|item| ChildView::new(item, cx)),
2119 )
2120 .contained()
2121 .with_style(container_theme)
2122 })
2123 .on_click(MouseButton::Left, |event, _, cx| {
2124 if event.click_count == 2 {
2125 cx.zoom_window();
2126 }
2127 })
2128 .constrained()
2129 .with_height(theme.workspace.titlebar.height)
2130 .into_any_named("titlebar")
2131 }
2132
2133 fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
2134 let active_entry = self.active_project_path(cx);
2135 self.project
2136 .update(cx, |project, cx| project.set_active_path(active_entry, cx));
2137 self.update_window_title(cx);
2138 }
2139
2140 fn update_window_title(&mut self, cx: &mut ViewContext<Self>) {
2141 let project = self.project().read(cx);
2142 let mut title = String::new();
2143
2144 if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
2145 let filename = path
2146 .path
2147 .file_name()
2148 .map(|s| s.to_string_lossy())
2149 .or_else(|| {
2150 Some(Cow::Borrowed(
2151 project
2152 .worktree_for_id(path.worktree_id, cx)?
2153 .read(cx)
2154 .root_name(),
2155 ))
2156 });
2157
2158 if let Some(filename) = filename {
2159 title.push_str(filename.as_ref());
2160 title.push_str(" β ");
2161 }
2162 }
2163
2164 for (i, name) in project.worktree_root_names(cx).enumerate() {
2165 if i > 0 {
2166 title.push_str(", ");
2167 }
2168 title.push_str(name);
2169 }
2170
2171 if title.is_empty() {
2172 title = "empty project".to_string();
2173 }
2174
2175 if project.is_remote() {
2176 title.push_str(" β");
2177 } else if project.is_shared() {
2178 title.push_str(" β");
2179 }
2180
2181 cx.set_window_title(&title);
2182 }
2183
2184 fn update_window_edited(&mut self, cx: &mut ViewContext<Self>) {
2185 let is_edited = !self.project.read(cx).is_read_only()
2186 && self
2187 .items(cx)
2188 .any(|item| item.has_conflict(cx) || item.is_dirty(cx));
2189 if is_edited != self.window_edited {
2190 self.window_edited = is_edited;
2191 cx.set_window_edited(self.window_edited)
2192 }
2193 }
2194
2195 fn render_disconnected_overlay(
2196 &self,
2197 cx: &mut ViewContext<Workspace>,
2198 ) -> Option<AnyElement<Workspace>> {
2199 if self.project.read(cx).is_read_only() {
2200 enum DisconnectedOverlay {}
2201 Some(
2202 MouseEventHandler::<DisconnectedOverlay, _>::new(0, cx, |_, cx| {
2203 let theme = &theme::current(cx);
2204 Label::new(
2205 "Your connection to the remote project has been lost.",
2206 theme.workspace.disconnected_overlay.text.clone(),
2207 )
2208 .aligned()
2209 .contained()
2210 .with_style(theme.workspace.disconnected_overlay.container)
2211 })
2212 .with_cursor_style(CursorStyle::Arrow)
2213 .capture_all()
2214 .into_any_named("disconnected overlay"),
2215 )
2216 } else {
2217 None
2218 }
2219 }
2220
2221 fn render_notifications(
2222 &self,
2223 theme: &theme::Workspace,
2224 cx: &AppContext,
2225 ) -> Option<AnyElement<Workspace>> {
2226 if self.notifications.is_empty() {
2227 None
2228 } else {
2229 Some(
2230 Flex::column()
2231 .with_children(self.notifications.iter().map(|(_, _, notification)| {
2232 ChildView::new(notification.as_any(), cx)
2233 .contained()
2234 .with_style(theme.notification)
2235 }))
2236 .constrained()
2237 .with_width(theme.notifications.width)
2238 .contained()
2239 .with_style(theme.notifications.container)
2240 .aligned()
2241 .bottom()
2242 .right()
2243 .into_any(),
2244 )
2245 }
2246 }
2247
2248 // RPC handlers
2249
2250 async fn handle_follow(
2251 this: WeakViewHandle<Self>,
2252 envelope: TypedEnvelope<proto::Follow>,
2253 _: Arc<Client>,
2254 mut cx: AsyncAppContext,
2255 ) -> Result<proto::FollowResponse> {
2256 this.update(&mut cx, |this, cx| {
2257 let client = &this.app_state.client;
2258 this.leader_state
2259 .followers
2260 .insert(envelope.original_sender_id()?);
2261
2262 let active_view_id = this.active_item(cx).and_then(|i| {
2263 Some(
2264 i.to_followable_item_handle(cx)?
2265 .remote_id(client, cx)?
2266 .to_proto(),
2267 )
2268 });
2269
2270 cx.notify();
2271
2272 Ok(proto::FollowResponse {
2273 active_view_id,
2274 views: this
2275 .panes()
2276 .iter()
2277 .flat_map(|pane| {
2278 let leader_id = this.leader_for_pane(pane);
2279 pane.read(cx).items().filter_map({
2280 let cx = &cx;
2281 move |item| {
2282 let item = item.to_followable_item_handle(cx)?;
2283 let id = item.remote_id(client, cx)?.to_proto();
2284 let variant = item.to_state_proto(cx)?;
2285 Some(proto::View {
2286 id: Some(id),
2287 leader_id,
2288 variant: Some(variant),
2289 })
2290 }
2291 })
2292 })
2293 .collect(),
2294 })
2295 })?
2296 }
2297
2298 async fn handle_unfollow(
2299 this: WeakViewHandle<Self>,
2300 envelope: TypedEnvelope<proto::Unfollow>,
2301 _: Arc<Client>,
2302 mut cx: AsyncAppContext,
2303 ) -> Result<()> {
2304 this.update(&mut cx, |this, cx| {
2305 this.leader_state
2306 .followers
2307 .remove(&envelope.original_sender_id()?);
2308 cx.notify();
2309 Ok(())
2310 })?
2311 }
2312
2313 async fn handle_update_followers(
2314 this: WeakViewHandle<Self>,
2315 envelope: TypedEnvelope<proto::UpdateFollowers>,
2316 _: Arc<Client>,
2317 cx: AsyncAppContext,
2318 ) -> Result<()> {
2319 let leader_id = envelope.original_sender_id()?;
2320 this.read_with(&cx, |this, _| {
2321 this.leader_updates_tx
2322 .unbounded_send((leader_id, envelope.payload))
2323 })??;
2324 Ok(())
2325 }
2326
2327 async fn process_leader_update(
2328 this: &WeakViewHandle<Self>,
2329 leader_id: PeerId,
2330 update: proto::UpdateFollowers,
2331 cx: &mut AsyncAppContext,
2332 ) -> Result<()> {
2333 match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
2334 proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
2335 this.update(cx, |this, _| {
2336 if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
2337 for state in state.values_mut() {
2338 state.active_view_id =
2339 if let Some(active_view_id) = update_active_view.id.clone() {
2340 Some(ViewId::from_proto(active_view_id)?)
2341 } else {
2342 None
2343 };
2344 }
2345 }
2346 anyhow::Ok(())
2347 })??;
2348 }
2349 proto::update_followers::Variant::UpdateView(update_view) => {
2350 let variant = update_view
2351 .variant
2352 .ok_or_else(|| anyhow!("missing update view variant"))?;
2353 let id = update_view
2354 .id
2355 .ok_or_else(|| anyhow!("missing update view id"))?;
2356 let mut tasks = Vec::new();
2357 this.update(cx, |this, cx| {
2358 let project = this.project.clone();
2359 if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
2360 for state in state.values_mut() {
2361 let view_id = ViewId::from_proto(id.clone())?;
2362 if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
2363 tasks.push(item.apply_update_proto(&project, variant.clone(), cx));
2364 }
2365 }
2366 }
2367 anyhow::Ok(())
2368 })??;
2369 try_join_all(tasks).await.log_err();
2370 }
2371 proto::update_followers::Variant::CreateView(view) => {
2372 let panes = this.read_with(cx, |this, _| {
2373 this.follower_states_by_leader
2374 .get(&leader_id)
2375 .into_iter()
2376 .flat_map(|states_by_pane| states_by_pane.keys())
2377 .cloned()
2378 .collect()
2379 })?;
2380 Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], cx).await?;
2381 }
2382 }
2383 this.update(cx, |this, cx| this.leader_updated(leader_id, cx))?;
2384 Ok(())
2385 }
2386
2387 async fn add_views_from_leader(
2388 this: WeakViewHandle<Self>,
2389 leader_id: PeerId,
2390 panes: Vec<ViewHandle<Pane>>,
2391 views: Vec<proto::View>,
2392 cx: &mut AsyncAppContext,
2393 ) -> Result<()> {
2394 let project = this.read_with(cx, |this, _| this.project.clone())?;
2395 let replica_id = project
2396 .read_with(cx, |project, _| {
2397 project
2398 .collaborators()
2399 .get(&leader_id)
2400 .map(|c| c.replica_id)
2401 })
2402 .ok_or_else(|| anyhow!("no such collaborator {}", leader_id))?;
2403
2404 let item_builders = cx.update(|cx| {
2405 cx.default_global::<FollowableItemBuilders>()
2406 .values()
2407 .map(|b| b.0)
2408 .collect::<Vec<_>>()
2409 });
2410
2411 let mut item_tasks_by_pane = HashMap::default();
2412 for pane in panes {
2413 let mut item_tasks = Vec::new();
2414 let mut leader_view_ids = Vec::new();
2415 for view in &views {
2416 let Some(id) = &view.id else { continue };
2417 let id = ViewId::from_proto(id.clone())?;
2418 let mut variant = view.variant.clone();
2419 if variant.is_none() {
2420 Err(anyhow!("missing variant"))?;
2421 }
2422 for build_item in &item_builders {
2423 let task = cx.update(|cx| {
2424 build_item(pane.clone(), project.clone(), id, &mut variant, cx)
2425 });
2426 if let Some(task) = task {
2427 item_tasks.push(task);
2428 leader_view_ids.push(id);
2429 break;
2430 } else {
2431 assert!(variant.is_some());
2432 }
2433 }
2434 }
2435
2436 item_tasks_by_pane.insert(pane, (item_tasks, leader_view_ids));
2437 }
2438
2439 for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane {
2440 let items = futures::future::try_join_all(item_tasks).await?;
2441 this.update(cx, |this, cx| {
2442 let state = this
2443 .follower_states_by_leader
2444 .get_mut(&leader_id)?
2445 .get_mut(&pane)?;
2446
2447 for (id, item) in leader_view_ids.into_iter().zip(items) {
2448 item.set_leader_replica_id(Some(replica_id), cx);
2449 state.items_by_leader_view_id.insert(id, item);
2450 }
2451
2452 Some(())
2453 })?;
2454 }
2455 Ok(())
2456 }
2457
2458 fn update_followers(
2459 &self,
2460 update: proto::update_followers::Variant,
2461 cx: &AppContext,
2462 ) -> Option<()> {
2463 let project_id = self.project.read(cx).remote_id()?;
2464 if !self.leader_state.followers.is_empty() {
2465 self.app_state
2466 .client
2467 .send(proto::UpdateFollowers {
2468 project_id,
2469 follower_ids: self.leader_state.followers.iter().copied().collect(),
2470 variant: Some(update),
2471 })
2472 .log_err();
2473 }
2474 None
2475 }
2476
2477 pub fn leader_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<PeerId> {
2478 self.follower_states_by_leader
2479 .iter()
2480 .find_map(|(leader_id, state)| {
2481 if state.contains_key(pane) {
2482 Some(*leader_id)
2483 } else {
2484 None
2485 }
2486 })
2487 }
2488
2489 fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
2490 cx.notify();
2491
2492 let call = self.active_call()?;
2493 let room = call.read(cx).room()?.read(cx);
2494 let participant = room.remote_participant_for_peer_id(leader_id)?;
2495 let mut items_to_activate = Vec::new();
2496 match participant.location {
2497 call::ParticipantLocation::SharedProject { project_id } => {
2498 if Some(project_id) == self.project.read(cx).remote_id() {
2499 for (pane, state) in self.follower_states_by_leader.get(&leader_id)? {
2500 if let Some(item) = state
2501 .active_view_id
2502 .and_then(|id| state.items_by_leader_view_id.get(&id))
2503 {
2504 items_to_activate.push((pane.clone(), item.boxed_clone()));
2505 } else {
2506 if let Some(shared_screen) =
2507 self.shared_screen_for_peer(leader_id, pane, cx)
2508 {
2509 items_to_activate.push((pane.clone(), Box::new(shared_screen)));
2510 }
2511 }
2512 }
2513 }
2514 }
2515 call::ParticipantLocation::UnsharedProject => {}
2516 call::ParticipantLocation::External => {
2517 for (pane, _) in self.follower_states_by_leader.get(&leader_id)? {
2518 if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
2519 items_to_activate.push((pane.clone(), Box::new(shared_screen)));
2520 }
2521 }
2522 }
2523 }
2524
2525 for (pane, item) in items_to_activate {
2526 let pane_was_focused = pane.read(cx).has_focus();
2527 if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) {
2528 pane.update(cx, |pane, cx| pane.activate_item(index, false, false, cx));
2529 } else {
2530 Pane::add_item(self, &pane, item.boxed_clone(), false, false, None, cx);
2531 }
2532
2533 if pane_was_focused {
2534 pane.update(cx, |pane, cx| pane.focus_active_item(cx));
2535 }
2536 }
2537
2538 None
2539 }
2540
2541 fn shared_screen_for_peer(
2542 &self,
2543 peer_id: PeerId,
2544 pane: &ViewHandle<Pane>,
2545 cx: &mut ViewContext<Self>,
2546 ) -> Option<ViewHandle<SharedScreen>> {
2547 let call = self.active_call()?;
2548 let room = call.read(cx).room()?.read(cx);
2549 let participant = room.remote_participant_for_peer_id(peer_id)?;
2550 let track = participant.tracks.values().next()?.clone();
2551 let user = participant.user.clone();
2552
2553 for item in pane.read(cx).items_of_type::<SharedScreen>() {
2554 if item.read(cx).peer_id == peer_id {
2555 return Some(item);
2556 }
2557 }
2558
2559 Some(cx.add_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx)))
2560 }
2561
2562 pub fn on_window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
2563 if active {
2564 cx.background()
2565 .spawn(persistence::DB.update_timestamp(self.database_id()))
2566 .detach();
2567 } else {
2568 for pane in &self.panes {
2569 pane.update(cx, |pane, cx| {
2570 if let Some(item) = pane.active_item() {
2571 item.workspace_deactivated(cx);
2572 }
2573 if matches!(
2574 settings::get::<WorkspaceSettings>(cx).autosave,
2575 AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
2576 ) {
2577 for item in pane.items() {
2578 Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
2579 .detach_and_log_err(cx);
2580 }
2581 }
2582 });
2583 }
2584 }
2585 }
2586
2587 fn active_call(&self) -> Option<&ModelHandle<ActiveCall>> {
2588 self.active_call.as_ref().map(|(call, _)| call)
2589 }
2590
2591 fn on_active_call_event(
2592 &mut self,
2593 _: ModelHandle<ActiveCall>,
2594 event: &call::room::Event,
2595 cx: &mut ViewContext<Self>,
2596 ) {
2597 match event {
2598 call::room::Event::ParticipantLocationChanged { participant_id }
2599 | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
2600 self.leader_updated(*participant_id, cx);
2601 }
2602 _ => {}
2603 }
2604 }
2605
2606 pub fn database_id(&self) -> WorkspaceId {
2607 self.database_id
2608 }
2609
2610 fn location(&self, cx: &AppContext) -> Option<WorkspaceLocation> {
2611 let project = self.project().read(cx);
2612
2613 if project.is_local() {
2614 Some(
2615 project
2616 .visible_worktrees(cx)
2617 .map(|worktree| worktree.read(cx).abs_path())
2618 .collect::<Vec<_>>()
2619 .into(),
2620 )
2621 } else {
2622 None
2623 }
2624 }
2625
2626 fn remove_panes(&mut self, member: Member, cx: &mut ViewContext<Workspace>) {
2627 match member {
2628 Member::Axis(PaneAxis { members, .. }) => {
2629 for child in members.iter() {
2630 self.remove_panes(child.clone(), cx)
2631 }
2632 }
2633 Member::Pane(pane) => {
2634 self.force_remove_pane(&pane, cx);
2635 }
2636 }
2637 }
2638
2639 fn force_remove_pane(&mut self, pane: &ViewHandle<Pane>, cx: &mut ViewContext<Workspace>) {
2640 self.panes.retain(|p| p != pane);
2641 cx.focus(self.panes.last().unwrap());
2642 if self.last_active_center_pane == Some(pane.downgrade()) {
2643 self.last_active_center_pane = None;
2644 }
2645 cx.notify();
2646 }
2647
2648 fn serialize_workspace(&self, cx: &AppContext) {
2649 fn serialize_pane_handle(
2650 pane_handle: &ViewHandle<Pane>,
2651 cx: &AppContext,
2652 ) -> SerializedPane {
2653 let (items, active) = {
2654 let pane = pane_handle.read(cx);
2655 let active_item_id = pane.active_item().map(|item| item.id());
2656 (
2657 pane.items()
2658 .filter_map(|item_handle| {
2659 Some(SerializedItem {
2660 kind: Arc::from(item_handle.serialized_item_kind()?),
2661 item_id: item_handle.id(),
2662 active: Some(item_handle.id()) == active_item_id,
2663 })
2664 })
2665 .collect::<Vec<_>>(),
2666 pane.is_active(),
2667 )
2668 };
2669
2670 SerializedPane::new(items, active)
2671 }
2672
2673 fn build_serialized_pane_group(
2674 pane_group: &Member,
2675 cx: &AppContext,
2676 ) -> SerializedPaneGroup {
2677 match pane_group {
2678 Member::Axis(PaneAxis { axis, members }) => SerializedPaneGroup::Group {
2679 axis: *axis,
2680 children: members
2681 .iter()
2682 .map(|member| build_serialized_pane_group(member, cx))
2683 .collect::<Vec<_>>(),
2684 },
2685 Member::Pane(pane_handle) => {
2686 SerializedPaneGroup::Pane(serialize_pane_handle(&pane_handle, cx))
2687 }
2688 }
2689 }
2690
2691 fn build_serialized_docks(this: &Workspace, cx: &AppContext) -> DockStructure {
2692 let left_dock = this.left_dock.read(cx);
2693 let left_visible = left_dock.is_open();
2694 let left_active_panel = left_dock.active_panel().and_then(|panel| {
2695 Some(
2696 cx.view_ui_name(panel.as_any().window_id(), panel.id())?
2697 .to_string(),
2698 )
2699 });
2700
2701 let right_dock = this.right_dock.read(cx);
2702 let right_visible = right_dock.is_open();
2703 let right_active_panel = right_dock.active_panel().and_then(|panel| {
2704 Some(
2705 cx.view_ui_name(panel.as_any().window_id(), panel.id())?
2706 .to_string(),
2707 )
2708 });
2709
2710 let bottom_dock = this.bottom_dock.read(cx);
2711 let bottom_visible = bottom_dock.is_open();
2712 let bottom_active_panel = bottom_dock.active_panel().and_then(|panel| {
2713 Some(
2714 cx.view_ui_name(panel.as_any().window_id(), panel.id())?
2715 .to_string(),
2716 )
2717 });
2718
2719 DockStructure {
2720 left: DockData {
2721 visible: left_visible,
2722 active_panel: left_active_panel,
2723 },
2724 right: DockData {
2725 visible: right_visible,
2726 active_panel: right_active_panel,
2727 },
2728 bottom: DockData {
2729 visible: bottom_visible,
2730 active_panel: bottom_active_panel,
2731 },
2732 }
2733 }
2734
2735 if let Some(location) = self.location(cx) {
2736 // Load bearing special case:
2737 // - with_local_workspace() relies on this to not have other stuff open
2738 // when you open your log
2739 if !location.paths().is_empty() {
2740 let center_group = build_serialized_pane_group(&self.center.root, cx);
2741 let docks = build_serialized_docks(self, cx);
2742
2743 let serialized_workspace = SerializedWorkspace {
2744 id: self.database_id,
2745 location,
2746 center_group,
2747 bounds: Default::default(),
2748 display: Default::default(),
2749 docks,
2750 };
2751
2752 cx.background()
2753 .spawn(persistence::DB.save_workspace(serialized_workspace))
2754 .detach();
2755 }
2756 }
2757 }
2758
2759 pub(crate) fn load_workspace(
2760 workspace: WeakViewHandle<Workspace>,
2761 serialized_workspace: SerializedWorkspace,
2762 paths_to_open: Vec<Option<ProjectPath>>,
2763 cx: &mut AppContext,
2764 ) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
2765 cx.spawn(|mut cx| async move {
2766 let result = async_iife! {{
2767 let (project, old_center_pane) =
2768 workspace.read_with(&cx, |workspace, _| {
2769 (
2770 workspace.project().clone(),
2771 workspace.last_active_center_pane.clone(),
2772 )
2773 })?;
2774
2775 let mut center_items = None;
2776 let mut center_group = None;
2777 // Traverse the splits tree and add to things
2778 if let Some((group, active_pane, items)) = serialized_workspace
2779 .center_group
2780 .deserialize(&project, serialized_workspace.id, &workspace, &mut cx)
2781 .await {
2782 center_items = Some(items);
2783 center_group = Some((group, active_pane))
2784 }
2785
2786 let resulting_list = cx.read(|cx| {
2787 let mut opened_items = center_items
2788 .unwrap_or_default()
2789 .into_iter()
2790 .filter_map(|item| {
2791 let item = item?;
2792 let project_path = item.project_path(cx)?;
2793 Some((project_path, item))
2794 })
2795 .collect::<HashMap<_, _>>();
2796
2797 paths_to_open
2798 .into_iter()
2799 .map(|path_to_open| {
2800 path_to_open.map(|path_to_open| {
2801 Ok(opened_items.remove(&path_to_open))
2802 })
2803 .transpose()
2804 .map(|item| item.flatten())
2805 .transpose()
2806 })
2807 .collect::<Vec<_>>()
2808 });
2809
2810 // Remove old panes from workspace panes list
2811 workspace.update(&mut cx, |workspace, cx| {
2812 if let Some((center_group, active_pane)) = center_group {
2813 workspace.remove_panes(workspace.center.root.clone(), cx);
2814
2815 // Swap workspace center group
2816 workspace.center = PaneGroup::with_root(center_group);
2817
2818 // Change the focus to the workspace first so that we retrigger focus in on the pane.
2819 cx.focus_self();
2820
2821 if let Some(active_pane) = active_pane {
2822 cx.focus(&active_pane);
2823 } else {
2824 cx.focus(workspace.panes.last().unwrap());
2825 }
2826 } else {
2827 let old_center_handle = old_center_pane.and_then(|weak| weak.upgrade(cx));
2828 if let Some(old_center_handle) = old_center_handle {
2829 cx.focus(&old_center_handle)
2830 } else {
2831 cx.focus_self()
2832 }
2833 }
2834
2835 let docks = serialized_workspace.docks;
2836 workspace.left_dock.update(cx, |dock, cx| {
2837 dock.set_open(docks.left.visible, cx);
2838 if let Some(active_panel) = docks.left.active_panel {
2839 if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
2840 dock.activate_panel(ix, cx);
2841 }
2842 }
2843 });
2844 workspace.right_dock.update(cx, |dock, cx| {
2845 dock.set_open(docks.right.visible, cx);
2846 if let Some(active_panel) = docks.right.active_panel {
2847 if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
2848 dock.activate_panel(ix, cx);
2849 }
2850 }
2851 });
2852 workspace.bottom_dock.update(cx, |dock, cx| {
2853 dock.set_open(docks.bottom.visible, cx);
2854 if let Some(active_panel) = docks.bottom.active_panel {
2855 if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
2856 dock.activate_panel(ix, cx);
2857 }
2858 }
2859 });
2860
2861 cx.notify();
2862 })?;
2863
2864 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
2865 workspace.read_with(&cx, |workspace, cx| workspace.serialize_workspace(cx))?;
2866
2867 Ok::<_, anyhow::Error>(resulting_list)
2868 }};
2869
2870 result.await.unwrap_or_default()
2871 })
2872 }
2873
2874 #[cfg(any(test, feature = "test-support"))]
2875 pub fn test_new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
2876 let app_state = Arc::new(AppState {
2877 languages: project.read(cx).languages().clone(),
2878 client: project.read(cx).client(),
2879 user_store: project.read(cx).user_store(),
2880 fs: project.read(cx).fs().clone(),
2881 build_window_options: |_, _, _| Default::default(),
2882 initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
2883 background_actions: || &[],
2884 });
2885 Self::new(0, project, app_state, cx)
2886 }
2887
2888 fn render_dock(&self, position: DockPosition, cx: &WindowContext) -> Option<AnyElement<Self>> {
2889 let dock = match position {
2890 DockPosition::Left => &self.left_dock,
2891 DockPosition::Right => &self.right_dock,
2892 DockPosition::Bottom => &self.bottom_dock,
2893 };
2894 let active_panel = dock.read(cx).active_panel()?;
2895 let element = if Some(active_panel.id()) == self.zoomed.as_ref().map(|zoomed| zoomed.id()) {
2896 dock.read(cx).render_placeholder(cx)
2897 } else {
2898 ChildView::new(dock, cx).into_any()
2899 };
2900
2901 Some(
2902 element
2903 .constrained()
2904 .dynamically(move |constraint, _, cx| match position {
2905 DockPosition::Left | DockPosition::Right => SizeConstraint::new(
2906 Vector2F::new(20., constraint.min.y()),
2907 Vector2F::new(cx.window_size().x() * 0.8, constraint.max.y()),
2908 ),
2909 DockPosition::Bottom => SizeConstraint::new(
2910 Vector2F::new(constraint.min.x(), 20.),
2911 Vector2F::new(constraint.max.x(), cx.window_size().y() * 0.8),
2912 ),
2913 })
2914 .into_any(),
2915 )
2916 }
2917}
2918
2919async fn open_items(
2920 serialized_workspace: Option<SerializedWorkspace>,
2921 workspace: &WeakViewHandle<Workspace>,
2922 mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
2923 app_state: Arc<AppState>,
2924 mut cx: AsyncAppContext,
2925) -> Vec<Option<anyhow::Result<Box<dyn ItemHandle>>>> {
2926 let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
2927
2928 if let Some(serialized_workspace) = serialized_workspace {
2929 let workspace = workspace.clone();
2930 let restored_items = cx
2931 .update(|cx| {
2932 Workspace::load_workspace(
2933 workspace,
2934 serialized_workspace,
2935 project_paths_to_open
2936 .iter()
2937 .map(|(_, project_path)| project_path)
2938 .cloned()
2939 .collect(),
2940 cx,
2941 )
2942 })
2943 .await;
2944
2945 let restored_project_paths = cx.read(|cx| {
2946 restored_items
2947 .iter()
2948 .filter_map(|item| item.as_ref()?.as_ref().ok()?.project_path(cx))
2949 .collect::<HashSet<_>>()
2950 });
2951
2952 opened_items = restored_items;
2953 project_paths_to_open
2954 .iter_mut()
2955 .for_each(|(_, project_path)| {
2956 if let Some(project_path_to_open) = project_path {
2957 if restored_project_paths.contains(project_path_to_open) {
2958 *project_path = None;
2959 }
2960 }
2961 });
2962 } else {
2963 for _ in 0..project_paths_to_open.len() {
2964 opened_items.push(None);
2965 }
2966 }
2967 assert!(opened_items.len() == project_paths_to_open.len());
2968
2969 let tasks =
2970 project_paths_to_open
2971 .into_iter()
2972 .enumerate()
2973 .map(|(i, (abs_path, project_path))| {
2974 let workspace = workspace.clone();
2975 cx.spawn(|mut cx| {
2976 let fs = app_state.fs.clone();
2977 async move {
2978 let file_project_path = project_path?;
2979 if fs.is_file(&abs_path).await {
2980 Some((
2981 i,
2982 workspace
2983 .update(&mut cx, |workspace, cx| {
2984 workspace.open_path(file_project_path, None, true, cx)
2985 })
2986 .log_err()?
2987 .await,
2988 ))
2989 } else {
2990 None
2991 }
2992 }
2993 })
2994 });
2995
2996 for maybe_opened_path in futures::future::join_all(tasks.into_iter())
2997 .await
2998 .into_iter()
2999 {
3000 if let Some((i, path_open_result)) = maybe_opened_path {
3001 opened_items[i] = Some(path_open_result);
3002 }
3003 }
3004
3005 opened_items
3006}
3007
3008fn notify_if_database_failed(workspace: &WeakViewHandle<Workspace>, cx: &mut AsyncAppContext) {
3009 const REPORT_ISSUE_URL: &str ="https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";
3010
3011 workspace
3012 .update(cx, |workspace, cx| {
3013 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
3014 workspace.show_notification_once(0, cx, |cx| {
3015 cx.add_view(|_| {
3016 MessageNotification::new("Failed to load any database file.")
3017 .with_click_message("Click to let us know about this error")
3018 .on_click(|cx| cx.platform().open_url(REPORT_ISSUE_URL))
3019 })
3020 });
3021 } else {
3022 let backup_path = (*db::BACKUP_DB_PATH).read();
3023 if let Some(backup_path) = backup_path.clone() {
3024 workspace.show_notification_once(0, cx, move |cx| {
3025 cx.add_view(move |_| {
3026 MessageNotification::new(format!(
3027 "Database file was corrupted. Old database backed up to {}",
3028 backup_path.display()
3029 ))
3030 .with_click_message("Click to show old database in finder")
3031 .on_click(move |cx| {
3032 cx.platform().open_url(&backup_path.to_string_lossy())
3033 })
3034 })
3035 });
3036 }
3037 }
3038 })
3039 .log_err();
3040}
3041
3042impl Entity for Workspace {
3043 type Event = Event;
3044}
3045
3046impl View for Workspace {
3047 fn ui_name() -> &'static str {
3048 "Workspace"
3049 }
3050
3051 fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
3052 let theme = theme::current(cx).clone();
3053 Stack::new()
3054 .with_child(
3055 Flex::column()
3056 .with_child(self.render_titlebar(&theme, cx))
3057 .with_child(
3058 Stack::new()
3059 .with_child({
3060 let project = self.project.clone();
3061 Flex::row()
3062 .with_children(self.render_dock(DockPosition::Left, cx))
3063 .with_child(
3064 Flex::column()
3065 .with_child(
3066 FlexItem::new(
3067 self.center.render(
3068 &project,
3069 &theme,
3070 &self.follower_states_by_leader,
3071 self.active_call(),
3072 self.active_pane(),
3073 self.zoomed
3074 .as_ref()
3075 .and_then(|zoomed| zoomed.upgrade(cx))
3076 .as_ref(),
3077 &self.app_state,
3078 cx,
3079 ),
3080 )
3081 .flex(1., true),
3082 )
3083 .with_children(
3084 self.render_dock(DockPosition::Bottom, cx),
3085 )
3086 .flex(1., true),
3087 )
3088 .with_children(self.render_dock(DockPosition::Right, cx))
3089 })
3090 .with_child(Overlay::new(
3091 Stack::new()
3092 .with_children(self.zoomed.as_ref().and_then(|zoomed| {
3093 enum ZoomBackground {}
3094 let zoomed = zoomed.upgrade(cx)?;
3095 Some(
3096 ChildView::new(&zoomed, cx)
3097 .contained()
3098 .with_style(theme.workspace.zoomed_foreground)
3099 .aligned()
3100 .contained()
3101 .with_style(theme.workspace.zoomed_background)
3102 .mouse::<ZoomBackground>(0)
3103 .capture_all()
3104 .on_down(
3105 MouseButton::Left,
3106 |_, this: &mut Self, cx| {
3107 this.zoom_out(cx);
3108 },
3109 ),
3110 )
3111 }))
3112 .with_children(self.modal.as_ref().map(|modal| {
3113 ChildView::new(modal, cx)
3114 .contained()
3115 .with_style(theme.workspace.modal)
3116 .aligned()
3117 .top()
3118 }))
3119 .with_children(self.render_notifications(&theme.workspace, cx)),
3120 ))
3121 .flex(1.0, true),
3122 )
3123 .with_child(ChildView::new(&self.status_bar, cx))
3124 .contained()
3125 .with_background_color(theme.workspace.background),
3126 )
3127 .with_children(DragAndDrop::render(cx))
3128 .with_children(self.render_disconnected_overlay(cx))
3129 .into_any_named("workspace")
3130 }
3131
3132 fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
3133 if cx.is_self_focused() {
3134 cx.focus(&self.active_pane);
3135 }
3136 }
3137}
3138
3139impl ViewId {
3140 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
3141 Ok(Self {
3142 creator: message
3143 .creator
3144 .ok_or_else(|| anyhow!("creator is missing"))?,
3145 id: message.id,
3146 })
3147 }
3148
3149 pub(crate) fn to_proto(&self) -> proto::ViewId {
3150 proto::ViewId {
3151 creator: Some(self.creator),
3152 id: self.id,
3153 }
3154 }
3155}
3156
3157pub trait WorkspaceHandle {
3158 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
3159}
3160
3161impl WorkspaceHandle for ViewHandle<Workspace> {
3162 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
3163 self.read(cx)
3164 .worktrees(cx)
3165 .flat_map(|worktree| {
3166 let worktree_id = worktree.read(cx).id();
3167 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
3168 worktree_id,
3169 path: f.path.clone(),
3170 })
3171 })
3172 .collect::<Vec<_>>()
3173 }
3174}
3175
3176impl std::fmt::Debug for OpenPaths {
3177 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3178 f.debug_struct("OpenPaths")
3179 .field("paths", &self.paths)
3180 .finish()
3181 }
3182}
3183
3184pub struct WorkspaceCreated(WeakViewHandle<Workspace>);
3185
3186pub fn activate_workspace_for_project(
3187 cx: &mut AsyncAppContext,
3188 predicate: impl Fn(&mut Project, &mut ModelContext<Project>) -> bool,
3189) -> Option<WeakViewHandle<Workspace>> {
3190 for window_id in cx.window_ids() {
3191 let handle = cx
3192 .update_window(window_id, |cx| {
3193 if let Some(workspace_handle) = cx.root_view().clone().downcast::<Workspace>() {
3194 let project = workspace_handle.read(cx).project.clone();
3195 if project.update(cx, &predicate) {
3196 cx.activate_window();
3197 return Some(workspace_handle.clone());
3198 }
3199 }
3200 None
3201 })
3202 .flatten();
3203
3204 if let Some(handle) = handle {
3205 return Some(handle.downgrade());
3206 }
3207 }
3208 None
3209}
3210
3211pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
3212 DB.last_workspace().await.log_err().flatten()
3213}
3214
3215#[allow(clippy::type_complexity)]
3216pub fn open_paths(
3217 abs_paths: &[PathBuf],
3218 app_state: &Arc<AppState>,
3219 requesting_window_id: Option<usize>,
3220 cx: &mut AppContext,
3221) -> Task<
3222 Result<(
3223 WeakViewHandle<Workspace>,
3224 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
3225 )>,
3226> {
3227 let app_state = app_state.clone();
3228 let abs_paths = abs_paths.to_vec();
3229 cx.spawn(|mut cx| async move {
3230 // Open paths in existing workspace if possible
3231 let existing = activate_workspace_for_project(&mut cx, |project, cx| {
3232 project.contains_paths(&abs_paths, cx)
3233 });
3234
3235 if let Some(existing) = existing {
3236 Ok((
3237 existing.clone(),
3238 existing
3239 .update(&mut cx, |workspace, cx| {
3240 workspace.open_paths(abs_paths, true, cx)
3241 })?
3242 .await,
3243 ))
3244 } else {
3245 Ok(cx
3246 .update(|cx| {
3247 Workspace::new_local(abs_paths, app_state.clone(), requesting_window_id, cx)
3248 })
3249 .await)
3250 }
3251 })
3252}
3253
3254pub fn open_new(
3255 app_state: &Arc<AppState>,
3256 cx: &mut AppContext,
3257 init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static,
3258) -> Task<()> {
3259 let task = Workspace::new_local(Vec::new(), app_state.clone(), None, cx);
3260 cx.spawn(|mut cx| async move {
3261 let (workspace, opened_paths) = task.await;
3262
3263 workspace
3264 .update(&mut cx, |workspace, cx| {
3265 if opened_paths.is_empty() {
3266 init(workspace, cx)
3267 }
3268 })
3269 .log_err();
3270 })
3271}
3272
3273pub fn create_and_open_local_file(
3274 path: &'static Path,
3275 cx: &mut ViewContext<Workspace>,
3276 default_content: impl 'static + Send + FnOnce() -> Rope,
3277) -> Task<Result<Box<dyn ItemHandle>>> {
3278 cx.spawn(|workspace, mut cx| async move {
3279 let fs = workspace.read_with(&cx, |workspace, _| workspace.app_state().fs.clone())?;
3280 if !fs.is_file(path).await {
3281 fs.create_file(path, Default::default()).await?;
3282 fs.save(path, &default_content(), Default::default())
3283 .await?;
3284 }
3285
3286 let mut items = workspace
3287 .update(&mut cx, |workspace, cx| {
3288 workspace.with_local_workspace(cx, |workspace, cx| {
3289 workspace.open_paths(vec![path.to_path_buf()], false, cx)
3290 })
3291 })?
3292 .await?
3293 .await;
3294
3295 let item = items.pop().flatten();
3296 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
3297 })
3298}
3299
3300pub fn join_remote_project(
3301 project_id: u64,
3302 follow_user_id: u64,
3303 app_state: Arc<AppState>,
3304 cx: &mut AppContext,
3305) -> Task<Result<()>> {
3306 cx.spawn(|mut cx| async move {
3307 let existing_workspace = cx
3308 .window_ids()
3309 .into_iter()
3310 .filter_map(|window_id| cx.root_view(window_id)?.clone().downcast::<Workspace>())
3311 .find(|workspace| {
3312 cx.read_window(workspace.window_id(), |cx| {
3313 workspace.read(cx).project().read(cx).remote_id() == Some(project_id)
3314 })
3315 .unwrap_or(false)
3316 });
3317
3318 let workspace = if let Some(existing_workspace) = existing_workspace {
3319 existing_workspace.downgrade()
3320 } else {
3321 let active_call = cx.read(ActiveCall::global);
3322 let room = active_call
3323 .read_with(&cx, |call, _| call.room().cloned())
3324 .ok_or_else(|| anyhow!("not in a call"))?;
3325 let project = room
3326 .update(&mut cx, |room, cx| {
3327 room.join_project(
3328 project_id,
3329 app_state.languages.clone(),
3330 app_state.fs.clone(),
3331 cx,
3332 )
3333 })
3334 .await?;
3335
3336 let (_, workspace) = cx.add_window(
3337 (app_state.build_window_options)(None, None, cx.platform().as_ref()),
3338 |cx| Workspace::new(0, project, app_state.clone(), cx),
3339 );
3340 (app_state.initialize_workspace)(
3341 workspace.downgrade(),
3342 false,
3343 app_state.clone(),
3344 cx.clone(),
3345 )
3346 .await
3347 .log_err();
3348
3349 workspace.downgrade()
3350 };
3351
3352 cx.activate_window(workspace.window_id());
3353 cx.platform().activate(true);
3354
3355 workspace.update(&mut cx, |workspace, cx| {
3356 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
3357 let follow_peer_id = room
3358 .read(cx)
3359 .remote_participants()
3360 .iter()
3361 .find(|(_, participant)| participant.user.id == follow_user_id)
3362 .map(|(_, p)| p.peer_id)
3363 .or_else(|| {
3364 // If we couldn't follow the given user, follow the host instead.
3365 let collaborator = workspace
3366 .project()
3367 .read(cx)
3368 .collaborators()
3369 .values()
3370 .find(|collaborator| collaborator.replica_id == 0)?;
3371 Some(collaborator.peer_id)
3372 });
3373
3374 if let Some(follow_peer_id) = follow_peer_id {
3375 if !workspace.is_being_followed(follow_peer_id) {
3376 workspace
3377 .toggle_follow(follow_peer_id, cx)
3378 .map(|follow| follow.detach_and_log_err(cx));
3379 }
3380 }
3381 }
3382 })?;
3383
3384 anyhow::Ok(())
3385 })
3386}
3387
3388pub fn restart(_: &Restart, cx: &mut AppContext) {
3389 let should_confirm = settings::get::<WorkspaceSettings>(cx).confirm_quit;
3390 cx.spawn(|mut cx| async move {
3391 let mut workspaces = cx
3392 .window_ids()
3393 .into_iter()
3394 .filter_map(|window_id| {
3395 Some(
3396 cx.root_view(window_id)?
3397 .clone()
3398 .downcast::<Workspace>()?
3399 .downgrade(),
3400 )
3401 })
3402 .collect::<Vec<_>>();
3403
3404 // If multiple windows have unsaved changes, and need a save prompt,
3405 // prompt in the active window before switching to a different window.
3406 workspaces.sort_by_key(|workspace| !cx.window_is_active(workspace.window_id()));
3407
3408 if let (true, Some(workspace)) = (should_confirm, workspaces.first()) {
3409 let answer = cx.prompt(
3410 workspace.window_id(),
3411 PromptLevel::Info,
3412 "Are you sure you want to restart?",
3413 &["Restart", "Cancel"],
3414 );
3415
3416 if let Some(mut answer) = answer {
3417 let answer = answer.next().await;
3418 if answer != Some(0) {
3419 return Ok(());
3420 }
3421 }
3422 }
3423
3424 // If the user cancels any save prompt, then keep the app open.
3425 for workspace in workspaces {
3426 if !workspace
3427 .update(&mut cx, |workspace, cx| {
3428 workspace.prepare_to_close(true, cx)
3429 })?
3430 .await?
3431 {
3432 return Ok(());
3433 }
3434 }
3435 cx.platform().restart();
3436 anyhow::Ok(())
3437 })
3438 .detach_and_log_err(cx);
3439}
3440
3441fn parse_pixel_position_env_var(value: &str) -> Option<Vector2F> {
3442 let mut parts = value.split(',');
3443 let width: usize = parts.next()?.parse().ok()?;
3444 let height: usize = parts.next()?.parse().ok()?;
3445 Some(vec2f(width as f32, height as f32))
3446}
3447
3448fn default_true() -> bool {
3449 true
3450}
3451
3452#[cfg(test)]
3453mod tests {
3454 use super::*;
3455 use crate::{
3456 dock::test::{TestPanel, TestPanelEvent},
3457 item::test::{TestItem, TestItemEvent, TestProjectItem},
3458 };
3459 use fs::FakeFs;
3460 use gpui::{executor::Deterministic, test::EmptyView, TestAppContext};
3461 use project::{Project, ProjectEntryId};
3462 use serde_json::json;
3463 use settings::SettingsStore;
3464 use std::{cell::RefCell, rc::Rc};
3465
3466 #[gpui::test]
3467 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
3468 init_test(cx);
3469
3470 let fs = FakeFs::new(cx.background());
3471 let project = Project::test(fs, [], cx).await;
3472 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
3473
3474 // Adding an item with no ambiguity renders the tab without detail.
3475 let item1 = cx.add_view(window_id, |_| {
3476 let mut item = TestItem::new();
3477 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
3478 item
3479 });
3480 workspace.update(cx, |workspace, cx| {
3481 workspace.add_item(Box::new(item1.clone()), cx);
3482 });
3483 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), None));
3484
3485 // Adding an item that creates ambiguity increases the level of detail on
3486 // both tabs.
3487 let item2 = cx.add_view(window_id, |_| {
3488 let mut item = TestItem::new();
3489 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
3490 item
3491 });
3492 workspace.update(cx, |workspace, cx| {
3493 workspace.add_item(Box::new(item2.clone()), cx);
3494 });
3495 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
3496 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
3497
3498 // Adding an item that creates ambiguity increases the level of detail only
3499 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
3500 // we stop at the highest detail available.
3501 let item3 = cx.add_view(window_id, |_| {
3502 let mut item = TestItem::new();
3503 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
3504 item
3505 });
3506 workspace.update(cx, |workspace, cx| {
3507 workspace.add_item(Box::new(item3.clone()), cx);
3508 });
3509 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
3510 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
3511 item3.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
3512 }
3513
3514 #[gpui::test]
3515 async fn test_tracking_active_path(cx: &mut TestAppContext) {
3516 init_test(cx);
3517
3518 let fs = FakeFs::new(cx.background());
3519 fs.insert_tree(
3520 "/root1",
3521 json!({
3522 "one.txt": "",
3523 "two.txt": "",
3524 }),
3525 )
3526 .await;
3527 fs.insert_tree(
3528 "/root2",
3529 json!({
3530 "three.txt": "",
3531 }),
3532 )
3533 .await;
3534
3535 let project = Project::test(fs, ["root1".as_ref()], cx).await;
3536 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
3537 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3538 let worktree_id = project.read_with(cx, |project, cx| {
3539 project.worktrees(cx).next().unwrap().read(cx).id()
3540 });
3541
3542 let item1 = cx.add_view(window_id, |cx| {
3543 TestItem::new().with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
3544 });
3545 let item2 = cx.add_view(window_id, |cx| {
3546 TestItem::new().with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
3547 });
3548
3549 // Add an item to an empty pane
3550 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx));
3551 project.read_with(cx, |project, cx| {
3552 assert_eq!(
3553 project.active_entry(),
3554 project
3555 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
3556 .map(|e| e.id)
3557 );
3558 });
3559 assert_eq!(
3560 cx.current_window_title(window_id).as_deref(),
3561 Some("one.txt β root1")
3562 );
3563
3564 // Add a second item to a non-empty pane
3565 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
3566 assert_eq!(
3567 cx.current_window_title(window_id).as_deref(),
3568 Some("two.txt β root1")
3569 );
3570 project.read_with(cx, |project, cx| {
3571 assert_eq!(
3572 project.active_entry(),
3573 project
3574 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
3575 .map(|e| e.id)
3576 );
3577 });
3578
3579 // Close the active item
3580 pane.update(cx, |pane, cx| {
3581 pane.close_active_item(&Default::default(), cx).unwrap()
3582 })
3583 .await
3584 .unwrap();
3585 assert_eq!(
3586 cx.current_window_title(window_id).as_deref(),
3587 Some("one.txt β root1")
3588 );
3589 project.read_with(cx, |project, cx| {
3590 assert_eq!(
3591 project.active_entry(),
3592 project
3593 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
3594 .map(|e| e.id)
3595 );
3596 });
3597
3598 // Add a project folder
3599 project
3600 .update(cx, |project, cx| {
3601 project.find_or_create_local_worktree("/root2", true, cx)
3602 })
3603 .await
3604 .unwrap();
3605 assert_eq!(
3606 cx.current_window_title(window_id).as_deref(),
3607 Some("one.txt β root1, root2")
3608 );
3609
3610 // Remove a project folder
3611 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
3612 assert_eq!(
3613 cx.current_window_title(window_id).as_deref(),
3614 Some("one.txt β root2")
3615 );
3616 }
3617
3618 #[gpui::test]
3619 async fn test_close_window(cx: &mut TestAppContext) {
3620 init_test(cx);
3621
3622 let fs = FakeFs::new(cx.background());
3623 fs.insert_tree("/root", json!({ "one": "" })).await;
3624
3625 let project = Project::test(fs, ["root".as_ref()], cx).await;
3626 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
3627
3628 // When there are no dirty items, there's nothing to do.
3629 let item1 = cx.add_view(window_id, |_| TestItem::new());
3630 workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
3631 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
3632 assert!(task.await.unwrap());
3633
3634 // When there are dirty untitled items, prompt to save each one. If the user
3635 // cancels any prompt, then abort.
3636 let item2 = cx.add_view(window_id, |_| TestItem::new().with_dirty(true));
3637 let item3 = cx.add_view(window_id, |cx| {
3638 TestItem::new()
3639 .with_dirty(true)
3640 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3641 });
3642 workspace.update(cx, |w, cx| {
3643 w.add_item(Box::new(item2.clone()), cx);
3644 w.add_item(Box::new(item3.clone()), cx);
3645 });
3646 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
3647 cx.foreground().run_until_parked();
3648 cx.simulate_prompt_answer(window_id, 2 /* cancel */);
3649 cx.foreground().run_until_parked();
3650 assert!(!cx.has_pending_prompt(window_id));
3651 assert!(!task.await.unwrap());
3652 }
3653
3654 #[gpui::test]
3655 async fn test_close_pane_items(cx: &mut TestAppContext) {
3656 init_test(cx);
3657
3658 let fs = FakeFs::new(cx.background());
3659
3660 let project = Project::test(fs, None, cx).await;
3661 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
3662
3663 let item1 = cx.add_view(window_id, |cx| {
3664 TestItem::new()
3665 .with_dirty(true)
3666 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3667 });
3668 let item2 = cx.add_view(window_id, |cx| {
3669 TestItem::new()
3670 .with_dirty(true)
3671 .with_conflict(true)
3672 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
3673 });
3674 let item3 = cx.add_view(window_id, |cx| {
3675 TestItem::new()
3676 .with_dirty(true)
3677 .with_conflict(true)
3678 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
3679 });
3680 let item4 = cx.add_view(window_id, |cx| {
3681 TestItem::new()
3682 .with_dirty(true)
3683 .with_project_items(&[TestProjectItem::new_untitled(cx)])
3684 });
3685 let pane = workspace.update(cx, |workspace, cx| {
3686 workspace.add_item(Box::new(item1.clone()), cx);
3687 workspace.add_item(Box::new(item2.clone()), cx);
3688 workspace.add_item(Box::new(item3.clone()), cx);
3689 workspace.add_item(Box::new(item4.clone()), cx);
3690 workspace.active_pane().clone()
3691 });
3692
3693 let close_items = pane.update(cx, |pane, cx| {
3694 pane.activate_item(1, true, true, cx);
3695 assert_eq!(pane.active_item().unwrap().id(), item2.id());
3696 let item1_id = item1.id();
3697 let item3_id = item3.id();
3698 let item4_id = item4.id();
3699 pane.close_items(cx, move |id| [item1_id, item3_id, item4_id].contains(&id))
3700 });
3701 cx.foreground().run_until_parked();
3702
3703 // There's a prompt to save item 1.
3704 pane.read_with(cx, |pane, _| {
3705 assert_eq!(pane.items_len(), 4);
3706 assert_eq!(pane.active_item().unwrap().id(), item1.id());
3707 });
3708 assert!(cx.has_pending_prompt(window_id));
3709
3710 // Confirm saving item 1.
3711 cx.simulate_prompt_answer(window_id, 0);
3712 cx.foreground().run_until_parked();
3713
3714 // Item 1 is saved. There's a prompt to save item 3.
3715 pane.read_with(cx, |pane, cx| {
3716 assert_eq!(item1.read(cx).save_count, 1);
3717 assert_eq!(item1.read(cx).save_as_count, 0);
3718 assert_eq!(item1.read(cx).reload_count, 0);
3719 assert_eq!(pane.items_len(), 3);
3720 assert_eq!(pane.active_item().unwrap().id(), item3.id());
3721 });
3722 assert!(cx.has_pending_prompt(window_id));
3723
3724 // Cancel saving item 3.
3725 cx.simulate_prompt_answer(window_id, 1);
3726 cx.foreground().run_until_parked();
3727
3728 // Item 3 is reloaded. There's a prompt to save item 4.
3729 pane.read_with(cx, |pane, cx| {
3730 assert_eq!(item3.read(cx).save_count, 0);
3731 assert_eq!(item3.read(cx).save_as_count, 0);
3732 assert_eq!(item3.read(cx).reload_count, 1);
3733 assert_eq!(pane.items_len(), 2);
3734 assert_eq!(pane.active_item().unwrap().id(), item4.id());
3735 });
3736 assert!(cx.has_pending_prompt(window_id));
3737
3738 // Confirm saving item 4.
3739 cx.simulate_prompt_answer(window_id, 0);
3740 cx.foreground().run_until_parked();
3741
3742 // There's a prompt for a path for item 4.
3743 cx.simulate_new_path_selection(|_| Some(Default::default()));
3744 close_items.await.unwrap();
3745
3746 // The requested items are closed.
3747 pane.read_with(cx, |pane, cx| {
3748 assert_eq!(item4.read(cx).save_count, 0);
3749 assert_eq!(item4.read(cx).save_as_count, 1);
3750 assert_eq!(item4.read(cx).reload_count, 0);
3751 assert_eq!(pane.items_len(), 1);
3752 assert_eq!(pane.active_item().unwrap().id(), item2.id());
3753 });
3754 }
3755
3756 #[gpui::test]
3757 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
3758 init_test(cx);
3759
3760 let fs = FakeFs::new(cx.background());
3761
3762 let project = Project::test(fs, [], cx).await;
3763 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
3764
3765 // Create several workspace items with single project entries, and two
3766 // workspace items with multiple project entries.
3767 let single_entry_items = (0..=4)
3768 .map(|project_entry_id| {
3769 cx.add_view(window_id, |cx| {
3770 TestItem::new()
3771 .with_dirty(true)
3772 .with_project_items(&[TestProjectItem::new(
3773 project_entry_id,
3774 &format!("{project_entry_id}.txt"),
3775 cx,
3776 )])
3777 })
3778 })
3779 .collect::<Vec<_>>();
3780 let item_2_3 = cx.add_view(window_id, |cx| {
3781 TestItem::new()
3782 .with_dirty(true)
3783 .with_singleton(false)
3784 .with_project_items(&[
3785 single_entry_items[2].read(cx).project_items[0].clone(),
3786 single_entry_items[3].read(cx).project_items[0].clone(),
3787 ])
3788 });
3789 let item_3_4 = cx.add_view(window_id, |cx| {
3790 TestItem::new()
3791 .with_dirty(true)
3792 .with_singleton(false)
3793 .with_project_items(&[
3794 single_entry_items[3].read(cx).project_items[0].clone(),
3795 single_entry_items[4].read(cx).project_items[0].clone(),
3796 ])
3797 });
3798
3799 // Create two panes that contain the following project entries:
3800 // left pane:
3801 // multi-entry items: (2, 3)
3802 // single-entry items: 0, 1, 2, 3, 4
3803 // right pane:
3804 // single-entry items: 1
3805 // multi-entry items: (3, 4)
3806 let left_pane = workspace.update(cx, |workspace, cx| {
3807 let left_pane = workspace.active_pane().clone();
3808 workspace.add_item(Box::new(item_2_3.clone()), cx);
3809 for item in single_entry_items {
3810 workspace.add_item(Box::new(item), cx);
3811 }
3812 left_pane.update(cx, |pane, cx| {
3813 pane.activate_item(2, true, true, cx);
3814 });
3815
3816 workspace
3817 .split_pane(left_pane.clone(), SplitDirection::Right, cx)
3818 .unwrap();
3819
3820 left_pane
3821 });
3822
3823 //Need to cause an effect flush in order to respect new focus
3824 workspace.update(cx, |workspace, cx| {
3825 workspace.add_item(Box::new(item_3_4.clone()), cx);
3826 cx.focus(&left_pane);
3827 });
3828
3829 // When closing all of the items in the left pane, we should be prompted twice:
3830 // once for project entry 0, and once for project entry 2. After those two
3831 // prompts, the task should complete.
3832
3833 let close = left_pane.update(cx, |pane, cx| pane.close_items(cx, |_| true));
3834 cx.foreground().run_until_parked();
3835 left_pane.read_with(cx, |pane, cx| {
3836 assert_eq!(
3837 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
3838 &[ProjectEntryId::from_proto(0)]
3839 );
3840 });
3841 cx.simulate_prompt_answer(window_id, 0);
3842
3843 cx.foreground().run_until_parked();
3844 left_pane.read_with(cx, |pane, cx| {
3845 assert_eq!(
3846 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
3847 &[ProjectEntryId::from_proto(2)]
3848 );
3849 });
3850 cx.simulate_prompt_answer(window_id, 0);
3851
3852 cx.foreground().run_until_parked();
3853 close.await.unwrap();
3854 left_pane.read_with(cx, |pane, _| {
3855 assert_eq!(pane.items_len(), 0);
3856 });
3857 }
3858
3859 #[gpui::test]
3860 async fn test_autosave(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
3861 init_test(cx);
3862
3863 let fs = FakeFs::new(cx.background());
3864
3865 let project = Project::test(fs, [], cx).await;
3866 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
3867 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3868
3869 let item = cx.add_view(window_id, |cx| {
3870 TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3871 });
3872 let item_id = item.id();
3873 workspace.update(cx, |workspace, cx| {
3874 workspace.add_item(Box::new(item.clone()), cx);
3875 });
3876
3877 // Autosave on window change.
3878 item.update(cx, |item, cx| {
3879 cx.update_global(|settings: &mut SettingsStore, cx| {
3880 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
3881 settings.autosave = Some(AutosaveSetting::OnWindowChange);
3882 })
3883 });
3884 item.is_dirty = true;
3885 });
3886
3887 // Deactivating the window saves the file.
3888 cx.simulate_window_activation(None);
3889 deterministic.run_until_parked();
3890 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
3891
3892 // Autosave on focus change.
3893 item.update(cx, |item, cx| {
3894 cx.focus_self();
3895 cx.update_global(|settings: &mut SettingsStore, cx| {
3896 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
3897 settings.autosave = Some(AutosaveSetting::OnFocusChange);
3898 })
3899 });
3900 item.is_dirty = true;
3901 });
3902
3903 // Blurring the item saves the file.
3904 item.update(cx, |_, cx| cx.blur());
3905 deterministic.run_until_parked();
3906 item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
3907
3908 // Deactivating the window still saves the file.
3909 cx.simulate_window_activation(Some(window_id));
3910 item.update(cx, |item, cx| {
3911 cx.focus_self();
3912 item.is_dirty = true;
3913 });
3914 cx.simulate_window_activation(None);
3915
3916 deterministic.run_until_parked();
3917 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
3918
3919 // Autosave after delay.
3920 item.update(cx, |item, cx| {
3921 cx.update_global(|settings: &mut SettingsStore, cx| {
3922 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
3923 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
3924 })
3925 });
3926 item.is_dirty = true;
3927 cx.emit(TestItemEvent::Edit);
3928 });
3929
3930 // Delay hasn't fully expired, so the file is still dirty and unsaved.
3931 deterministic.advance_clock(Duration::from_millis(250));
3932 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
3933
3934 // After delay expires, the file is saved.
3935 deterministic.advance_clock(Duration::from_millis(250));
3936 item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
3937
3938 // Autosave on focus change, ensuring closing the tab counts as such.
3939 item.update(cx, |item, cx| {
3940 cx.update_global(|settings: &mut SettingsStore, cx| {
3941 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
3942 settings.autosave = Some(AutosaveSetting::OnFocusChange);
3943 })
3944 });
3945 item.is_dirty = true;
3946 });
3947
3948 pane.update(cx, |pane, cx| pane.close_items(cx, move |id| id == item_id))
3949 .await
3950 .unwrap();
3951 assert!(!cx.has_pending_prompt(window_id));
3952 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
3953
3954 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
3955 workspace.update(cx, |workspace, cx| {
3956 workspace.add_item(Box::new(item.clone()), cx);
3957 });
3958 item.update(cx, |item, cx| {
3959 item.project_items[0].update(cx, |item, _| {
3960 item.entry_id = None;
3961 });
3962 item.is_dirty = true;
3963 cx.blur();
3964 });
3965 deterministic.run_until_parked();
3966 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
3967
3968 // Ensure autosave is prevented for deleted files also when closing the buffer.
3969 let _close_items =
3970 pane.update(cx, |pane, cx| pane.close_items(cx, move |id| id == item_id));
3971 deterministic.run_until_parked();
3972 assert!(cx.has_pending_prompt(window_id));
3973 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
3974 }
3975
3976 #[gpui::test]
3977 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
3978 init_test(cx);
3979
3980 let fs = FakeFs::new(cx.background());
3981
3982 let project = Project::test(fs, [], cx).await;
3983 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
3984
3985 let item = cx.add_view(window_id, |cx| {
3986 TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3987 });
3988 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3989 let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone());
3990 let toolbar_notify_count = Rc::new(RefCell::new(0));
3991
3992 workspace.update(cx, |workspace, cx| {
3993 workspace.add_item(Box::new(item.clone()), cx);
3994 let toolbar_notification_count = toolbar_notify_count.clone();
3995 cx.observe(&toolbar, move |_, _, _| {
3996 *toolbar_notification_count.borrow_mut() += 1
3997 })
3998 .detach();
3999 });
4000
4001 pane.read_with(cx, |pane, _| {
4002 assert!(!pane.can_navigate_backward());
4003 assert!(!pane.can_navigate_forward());
4004 });
4005
4006 item.update(cx, |item, cx| {
4007 item.set_state("one".to_string(), cx);
4008 });
4009
4010 // Toolbar must be notified to re-render the navigation buttons
4011 assert_eq!(*toolbar_notify_count.borrow(), 1);
4012
4013 pane.read_with(cx, |pane, _| {
4014 assert!(pane.can_navigate_backward());
4015 assert!(!pane.can_navigate_forward());
4016 });
4017
4018 workspace
4019 .update(cx, |workspace, cx| {
4020 Pane::go_back(workspace, Some(pane.downgrade()), cx)
4021 })
4022 .await
4023 .unwrap();
4024
4025 assert_eq!(*toolbar_notify_count.borrow(), 3);
4026 pane.read_with(cx, |pane, _| {
4027 assert!(!pane.can_navigate_backward());
4028 assert!(pane.can_navigate_forward());
4029 });
4030 }
4031
4032 #[gpui::test]
4033 async fn test_panels(cx: &mut gpui::TestAppContext) {
4034 init_test(cx);
4035 let fs = FakeFs::new(cx.background());
4036
4037 let project = Project::test(fs, [], cx).await;
4038 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
4039
4040 let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
4041 // Add panel_1 on the left, panel_2 on the right.
4042 let panel_1 = cx.add_view(|_| TestPanel::new(DockPosition::Left));
4043 workspace.add_panel(panel_1.clone(), cx);
4044 workspace
4045 .left_dock()
4046 .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
4047 let panel_2 = cx.add_view(|_| TestPanel::new(DockPosition::Right));
4048 workspace.add_panel(panel_2.clone(), cx);
4049 workspace
4050 .right_dock()
4051 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
4052
4053 let left_dock = workspace.left_dock();
4054 assert_eq!(
4055 left_dock.read(cx).active_panel().unwrap().id(),
4056 panel_1.id()
4057 );
4058 assert_eq!(
4059 left_dock.read(cx).active_panel_size(cx).unwrap(),
4060 panel_1.size(cx)
4061 );
4062
4063 left_dock.update(cx, |left_dock, cx| left_dock.resize_active_panel(1337., cx));
4064 assert_eq!(
4065 workspace.right_dock().read(cx).active_panel().unwrap().id(),
4066 panel_2.id()
4067 );
4068
4069 (panel_1, panel_2)
4070 });
4071
4072 // Move panel_1 to the right
4073 panel_1.update(cx, |panel_1, cx| {
4074 panel_1.set_position(DockPosition::Right, cx)
4075 });
4076
4077 workspace.update(cx, |workspace, cx| {
4078 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
4079 // Since it was the only panel on the left, the left dock should now be closed.
4080 assert!(!workspace.left_dock().read(cx).is_open());
4081 assert!(workspace.left_dock().read(cx).active_panel().is_none());
4082 let right_dock = workspace.right_dock();
4083 assert_eq!(
4084 right_dock.read(cx).active_panel().unwrap().id(),
4085 panel_1.id()
4086 );
4087 assert_eq!(right_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
4088
4089 // Now we move panel_2Β to the left
4090 panel_2.set_position(DockPosition::Left, cx);
4091 });
4092
4093 workspace.update(cx, |workspace, cx| {
4094 // Since panel_2 was not visible on the right, we don't open the left dock.
4095 assert!(!workspace.left_dock().read(cx).is_open());
4096 // And the right dock is unaffected in it's displaying of panel_1
4097 assert!(workspace.right_dock().read(cx).is_open());
4098 assert_eq!(
4099 workspace.right_dock().read(cx).active_panel().unwrap().id(),
4100 panel_1.id()
4101 );
4102 });
4103
4104 // Move panel_1 back to the left
4105 panel_1.update(cx, |panel_1, cx| {
4106 panel_1.set_position(DockPosition::Left, cx)
4107 });
4108
4109 workspace.update(cx, |workspace, cx| {
4110 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
4111 let left_dock = workspace.left_dock();
4112 assert!(left_dock.read(cx).is_open());
4113 assert_eq!(
4114 left_dock.read(cx).active_panel().unwrap().id(),
4115 panel_1.id()
4116 );
4117 assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
4118 // And right the dock should be closed as it no longer has any panels.
4119 assert!(!workspace.right_dock().read(cx).is_open());
4120
4121 // Now we move panel_1 to the bottom
4122 panel_1.set_position(DockPosition::Bottom, cx);
4123 });
4124
4125 workspace.update(cx, |workspace, cx| {
4126 // Since panel_1 was visible on the left, we close the left dock.
4127 assert!(!workspace.left_dock().read(cx).is_open());
4128 // The bottom dock is sized based on the panel's default size,
4129 // since the panel orientation changed from vertical to horizontal.
4130 let bottom_dock = workspace.bottom_dock();
4131 assert_eq!(
4132 bottom_dock.read(cx).active_panel_size(cx).unwrap(),
4133 panel_1.size(cx),
4134 );
4135 // Close bottom dock and move panel_1 back to the left.
4136 bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
4137 panel_1.set_position(DockPosition::Left, cx);
4138 });
4139
4140 // Emit activated event on panel 1
4141 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Activated));
4142
4143 // Now the left dock is open and panel_1 is active and focused.
4144 workspace.read_with(cx, |workspace, cx| {
4145 let left_dock = workspace.left_dock();
4146 assert!(left_dock.read(cx).is_open());
4147 assert_eq!(
4148 left_dock.read(cx).active_panel().unwrap().id(),
4149 panel_1.id()
4150 );
4151 assert!(panel_1.is_focused(cx));
4152 });
4153
4154 // Emit closed event on panel 2, which is not active
4155 panel_2.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed));
4156
4157 // Wo don't close the left dock, because panel_2 wasn't the active panel
4158 workspace.read_with(cx, |workspace, cx| {
4159 let left_dock = workspace.left_dock();
4160 assert!(left_dock.read(cx).is_open());
4161 assert_eq!(
4162 left_dock.read(cx).active_panel().unwrap().id(),
4163 panel_1.id()
4164 );
4165 });
4166
4167 // Emitting a ZoomIn event shows the panel as zoomed.
4168 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomIn));
4169 workspace.read_with(cx, |workspace, _| {
4170 assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
4171 });
4172
4173 // If focus is transferred to another view that's not a panel or another pane, we still show
4174 // the panel as zoomed.
4175 let focus_receiver = cx.add_view(window_id, |_| EmptyView);
4176 focus_receiver.update(cx, |_, cx| cx.focus_self());
4177 workspace.read_with(cx, |workspace, _| {
4178 assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
4179 });
4180
4181 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
4182 workspace.update(cx, |_, cx| cx.focus_self());
4183 workspace.read_with(cx, |workspace, _| {
4184 assert_eq!(workspace.zoomed, None);
4185 });
4186
4187 // If focus is transferred again to another view that's not a panel or a pane, we won't
4188 // show the panel as zoomed because it wasn't zoomed before.
4189 focus_receiver.update(cx, |_, cx| cx.focus_self());
4190 workspace.read_with(cx, |workspace, _| {
4191 assert_eq!(workspace.zoomed, None);
4192 });
4193
4194 // When focus is transferred back to the panel, it is zoomed again.
4195 panel_1.update(cx, |_, cx| cx.focus_self());
4196 workspace.read_with(cx, |workspace, _| {
4197 assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
4198 });
4199
4200 // Emitting a ZoomOut event unzooms the panel.
4201 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomOut));
4202 workspace.read_with(cx, |workspace, _| {
4203 assert_eq!(workspace.zoomed, None);
4204 });
4205
4206 // Emit closed event on panel 1, which is active
4207 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed));
4208
4209 // Now the left dock is closed, because panel_1 was the active panel
4210 workspace.read_with(cx, |workspace, cx| {
4211 let left_dock = workspace.left_dock();
4212 assert!(!left_dock.read(cx).is_open());
4213 });
4214 }
4215
4216 pub fn init_test(cx: &mut TestAppContext) {
4217 cx.foreground().forbid_parking();
4218 cx.update(|cx| {
4219 cx.set_global(SettingsStore::test(cx));
4220 theme::init((), cx);
4221 language::init(cx);
4222 crate::init_settings(cx);
4223 });
4224 }
4225}