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