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