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