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