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