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