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