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