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 let id = cx.window_ids().find(|&id| cx.window_is_active(id));
922 if let Some(id) = id {
923 //This can only get called when the window's project connection has been lost
924 //so we don't need to prompt the user for anything and instead just close the window
925 cx.remove_window(id);
926 }
927 }
928
929 pub fn close(
930 &mut self,
931 _: &CloseWindow,
932 cx: &mut ViewContext<Self>,
933 ) -> Option<Task<Result<()>>> {
934 let window_id = cx.window_id();
935 let prepare = self.prepare_to_close(false, cx);
936 Some(cx.spawn(|_, mut cx| async move {
937 if prepare.await? {
938 cx.remove_window(window_id);
939 }
940 Ok(())
941 }))
942 }
943
944 pub fn prepare_to_close(
945 &mut self,
946 quitting: bool,
947 cx: &mut ViewContext<Self>,
948 ) -> Task<Result<bool>> {
949 let active_call = self.active_call().cloned();
950 let window_id = cx.window_id();
951 let workspace_count = cx
952 .window_ids()
953 .collect::<Vec<_>>()
954 .into_iter()
955 .filter_map(|window_id| {
956 cx.app_context()
957 .root_view(window_id)?
958 .clone()
959 .downcast::<Workspace>()
960 })
961 .count();
962
963 cx.spawn(|this, mut cx| async move {
964 if let Some(active_call) = active_call {
965 if !quitting
966 && workspace_count == 1
967 && active_call.read_with(&cx, |call, _| call.room().is_some())
968 {
969 let answer = cx.prompt(
970 window_id,
971 PromptLevel::Warning,
972 "Do you want to leave the current call?",
973 &["Close window and hang up", "Cancel"],
974 );
975
976 if let Some(mut answer) = answer {
977 if answer.next().await == Some(1) {
978 return anyhow::Ok(false);
979 } else {
980 active_call
981 .update(&mut cx, |call, cx| call.hang_up(cx))
982 .await
983 .log_err();
984 }
985 }
986 }
987 }
988
989 Ok(this
990 .update(&mut cx, |this, cx| this.save_all_internal(true, cx))?
991 .await?)
992 })
993 }
994
995 fn save_all(&mut self, _: &SaveAll, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
996 let save_all = self.save_all_internal(false, cx);
997 Some(cx.foreground().spawn(async move {
998 save_all.await?;
999 Ok(())
1000 }))
1001 }
1002
1003 fn save_all_internal(
1004 &mut self,
1005 should_prompt_to_save: bool,
1006 cx: &mut ViewContext<Self>,
1007 ) -> Task<Result<bool>> {
1008 if self.project.read(cx).is_read_only() {
1009 return Task::ready(Ok(true));
1010 }
1011
1012 let dirty_items = self
1013 .panes
1014 .iter()
1015 .flat_map(|pane| {
1016 pane.read(cx).items().filter_map(|item| {
1017 if item.is_dirty(cx) {
1018 Some((pane.downgrade(), item.boxed_clone()))
1019 } else {
1020 None
1021 }
1022 })
1023 })
1024 .collect::<Vec<_>>();
1025
1026 let project = self.project.clone();
1027 cx.spawn(|_, mut cx| async move {
1028 for (pane, item) in dirty_items {
1029 let (singleton, project_entry_ids) =
1030 cx.read(|cx| (item.is_singleton(cx), item.project_entry_ids(cx)));
1031 if singleton || !project_entry_ids.is_empty() {
1032 if let Some(ix) =
1033 pane.read_with(&cx, |pane, _| pane.index_for_item(item.as_ref()))?
1034 {
1035 if !Pane::save_item(
1036 project.clone(),
1037 &pane,
1038 ix,
1039 &*item,
1040 should_prompt_to_save,
1041 &mut cx,
1042 )
1043 .await?
1044 {
1045 return Ok(false);
1046 }
1047 }
1048 }
1049 }
1050 Ok(true)
1051 })
1052 }
1053
1054 pub fn open(&mut self, _: &Open, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
1055 let mut paths = cx.prompt_for_paths(PathPromptOptions {
1056 files: true,
1057 directories: true,
1058 multiple: true,
1059 });
1060
1061 Some(cx.spawn(|this, mut cx| async move {
1062 if let Some(paths) = paths.recv().await.flatten() {
1063 if let Some(task) = this
1064 .update(&mut cx, |this, cx| this.open_workspace_for_paths(paths, cx))
1065 .log_err()
1066 {
1067 task.await?
1068 }
1069 }
1070 Ok(())
1071 }))
1072 }
1073
1074 pub fn open_workspace_for_paths(
1075 &mut self,
1076 paths: Vec<PathBuf>,
1077 cx: &mut ViewContext<Self>,
1078 ) -> Task<Result<()>> {
1079 let window_id = cx.window_id();
1080 let is_remote = self.project.read(cx).is_remote();
1081 let has_worktree = self.project.read(cx).worktrees(cx).next().is_some();
1082 let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx));
1083 let close_task = if is_remote || has_worktree || has_dirty_items {
1084 None
1085 } else {
1086 Some(self.prepare_to_close(false, cx))
1087 };
1088 let app_state = self.app_state.clone();
1089
1090 cx.spawn(|_, mut cx| async move {
1091 let window_id_to_replace = if let Some(close_task) = close_task {
1092 if !close_task.await? {
1093 return Ok(());
1094 }
1095 Some(window_id)
1096 } else {
1097 None
1098 };
1099 cx.update(|cx| open_paths(&paths, &app_state, window_id_to_replace, cx))
1100 .await?;
1101 Ok(())
1102 })
1103 }
1104
1105 #[allow(clippy::type_complexity)]
1106 pub fn open_paths(
1107 &mut self,
1108 mut abs_paths: Vec<PathBuf>,
1109 visible: bool,
1110 cx: &mut ViewContext<Self>,
1111 ) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
1112 let fs = self.app_state.fs.clone();
1113
1114 // Sort the paths to ensure we add worktrees for parents before their children.
1115 abs_paths.sort_unstable();
1116 cx.spawn(|this, mut cx| async move {
1117 let mut project_paths = Vec::new();
1118 for path in &abs_paths {
1119 if let Some(project_path) = this
1120 .update(&mut cx, |this, cx| {
1121 Workspace::project_path_for_path(this.project.clone(), path, visible, cx)
1122 })
1123 .log_err()
1124 {
1125 project_paths.push(project_path.await.log_err());
1126 } else {
1127 project_paths.push(None);
1128 }
1129 }
1130
1131 let tasks = abs_paths
1132 .iter()
1133 .cloned()
1134 .zip(project_paths.into_iter())
1135 .map(|(abs_path, project_path)| {
1136 let this = this.clone();
1137 cx.spawn(|mut cx| {
1138 let fs = fs.clone();
1139 async move {
1140 let (_worktree, project_path) = project_path?;
1141 if fs.is_file(&abs_path).await {
1142 Some(
1143 this.update(&mut cx, |this, cx| {
1144 this.open_path(project_path, None, true, cx)
1145 })
1146 .log_err()?
1147 .await,
1148 )
1149 } else {
1150 None
1151 }
1152 }
1153 })
1154 })
1155 .collect::<Vec<_>>();
1156
1157 futures::future::join_all(tasks).await
1158 })
1159 }
1160
1161 fn add_folder_to_project(&mut self, _: &AddFolderToProject, cx: &mut ViewContext<Self>) {
1162 let mut paths = cx.prompt_for_paths(PathPromptOptions {
1163 files: false,
1164 directories: true,
1165 multiple: true,
1166 });
1167 cx.spawn(|this, mut cx| async move {
1168 if let Some(paths) = paths.recv().await.flatten() {
1169 let results = this
1170 .update(&mut cx, |this, cx| this.open_paths(paths, true, cx))?
1171 .await;
1172 for result in results.into_iter().flatten() {
1173 result.log_err();
1174 }
1175 }
1176 anyhow::Ok(())
1177 })
1178 .detach_and_log_err(cx);
1179 }
1180
1181 fn project_path_for_path(
1182 project: ModelHandle<Project>,
1183 abs_path: &Path,
1184 visible: bool,
1185 cx: &mut AppContext,
1186 ) -> Task<Result<(ModelHandle<Worktree>, ProjectPath)>> {
1187 let entry = project.update(cx, |project, cx| {
1188 project.find_or_create_local_worktree(abs_path, visible, cx)
1189 });
1190 cx.spawn(|cx| async move {
1191 let (worktree, path) = entry.await?;
1192 let worktree_id = worktree.read_with(&cx, |t, _| t.id());
1193 Ok((
1194 worktree,
1195 ProjectPath {
1196 worktree_id,
1197 path: path.into(),
1198 },
1199 ))
1200 })
1201 }
1202
1203 /// Returns the modal that was toggled closed if it was open.
1204 pub fn toggle_modal<V, F>(
1205 &mut self,
1206 cx: &mut ViewContext<Self>,
1207 add_view: F,
1208 ) -> Option<ViewHandle<V>>
1209 where
1210 V: 'static + Modal,
1211 F: FnOnce(&mut Self, &mut ViewContext<Self>) -> ViewHandle<V>,
1212 {
1213 cx.notify();
1214 // Whatever modal was visible is getting clobbered. If its the same type as V, then return
1215 // it. Otherwise, create a new modal and set it as active.
1216 let already_open_modal = self.modal.take().and_then(|modal| modal.downcast::<V>());
1217 if let Some(already_open_modal) = already_open_modal {
1218 cx.focus_self();
1219 Some(already_open_modal)
1220 } else {
1221 let modal = add_view(self, cx);
1222 cx.subscribe(&modal, |this, _, event, cx| {
1223 if V::dismiss_on_event(event) {
1224 this.dismiss_modal(cx);
1225 }
1226 })
1227 .detach();
1228 cx.focus(&modal);
1229 self.modal = Some(modal.into_any());
1230 None
1231 }
1232 }
1233
1234 pub fn modal<V: 'static + View>(&self) -> Option<ViewHandle<V>> {
1235 self.modal
1236 .as_ref()
1237 .and_then(|modal| modal.clone().downcast::<V>())
1238 }
1239
1240 pub fn dismiss_modal(&mut self, cx: &mut ViewContext<Self>) {
1241 if self.modal.take().is_some() {
1242 cx.focus(&self.active_pane);
1243 cx.notify();
1244 }
1245 }
1246
1247 pub fn items<'a>(
1248 &'a self,
1249 cx: &'a AppContext,
1250 ) -> impl 'a + Iterator<Item = &Box<dyn ItemHandle>> {
1251 self.panes.iter().flat_map(|pane| pane.read(cx).items())
1252 }
1253
1254 pub fn item_of_type<T: Item>(&self, cx: &AppContext) -> Option<ViewHandle<T>> {
1255 self.items_of_type(cx).max_by_key(|item| item.id())
1256 }
1257
1258 pub fn items_of_type<'a, T: Item>(
1259 &'a self,
1260 cx: &'a AppContext,
1261 ) -> impl 'a + Iterator<Item = ViewHandle<T>> {
1262 self.panes
1263 .iter()
1264 .flat_map(|pane| pane.read(cx).items_of_type())
1265 }
1266
1267 pub fn active_item(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
1268 self.active_pane().read(cx).active_item()
1269 }
1270
1271 fn active_project_path(&self, cx: &ViewContext<Self>) -> Option<ProjectPath> {
1272 self.active_item(cx).and_then(|item| item.project_path(cx))
1273 }
1274
1275 pub fn save_active_item(
1276 &mut self,
1277 force_name_change: bool,
1278 cx: &mut ViewContext<Self>,
1279 ) -> Task<Result<()>> {
1280 let project = self.project.clone();
1281 if let Some(item) = self.active_item(cx) {
1282 if !force_name_change && item.can_save(cx) {
1283 if item.has_conflict(cx) {
1284 const CONFLICT_MESSAGE: &str = "This file has changed on disk since you started editing it. Do you want to overwrite it?";
1285
1286 let mut answer = cx.prompt(
1287 PromptLevel::Warning,
1288 CONFLICT_MESSAGE,
1289 &["Overwrite", "Cancel"],
1290 );
1291 cx.spawn(|this, mut cx| async move {
1292 let answer = answer.recv().await;
1293 if answer == Some(0) {
1294 this.update(&mut cx, |this, cx| item.save(this.project.clone(), cx))?
1295 .await?;
1296 }
1297 Ok(())
1298 })
1299 } else {
1300 item.save(self.project.clone(), cx)
1301 }
1302 } else if item.is_singleton(cx) {
1303 let worktree = self.worktrees(cx).next();
1304 let start_abs_path = worktree
1305 .and_then(|w| w.read(cx).as_local())
1306 .map_or(Path::new(""), |w| w.abs_path())
1307 .to_path_buf();
1308 let mut abs_path = cx.prompt_for_new_path(&start_abs_path);
1309 cx.spawn(|this, mut cx| async move {
1310 if let Some(abs_path) = abs_path.recv().await.flatten() {
1311 this.update(&mut cx, |_, cx| item.save_as(project, abs_path, cx))?
1312 .await?;
1313 }
1314 Ok(())
1315 })
1316 } else {
1317 Task::ready(Ok(()))
1318 }
1319 } else {
1320 Task::ready(Ok(()))
1321 }
1322 }
1323
1324 pub fn toggle_sidebar(&mut self, sidebar_side: SidebarSide, cx: &mut ViewContext<Self>) {
1325 let sidebar = match sidebar_side {
1326 SidebarSide::Left => &mut self.left_sidebar,
1327 SidebarSide::Right => &mut self.right_sidebar,
1328 };
1329 let open = sidebar.update(cx, |sidebar, cx| {
1330 let open = !sidebar.is_open();
1331 sidebar.set_open(open, cx);
1332 open
1333 });
1334
1335 if open {
1336 Dock::hide_on_sidebar_shown(self, sidebar_side, cx);
1337 }
1338
1339 self.serialize_workspace(cx);
1340
1341 cx.focus_self();
1342 cx.notify();
1343 }
1344
1345 pub fn toggle_sidebar_item(&mut self, action: &ToggleSidebarItem, cx: &mut ViewContext<Self>) {
1346 let sidebar = match action.sidebar_side {
1347 SidebarSide::Left => &mut self.left_sidebar,
1348 SidebarSide::Right => &mut self.right_sidebar,
1349 };
1350 let active_item = sidebar.update(cx, move |sidebar, cx| {
1351 if sidebar.is_open() && sidebar.active_item_ix() == action.item_index {
1352 sidebar.set_open(false, cx);
1353 None
1354 } else {
1355 sidebar.set_open(true, cx);
1356 sidebar.activate_item(action.item_index, cx);
1357 sidebar.active_item().cloned()
1358 }
1359 });
1360
1361 if let Some(active_item) = active_item {
1362 Dock::hide_on_sidebar_shown(self, action.sidebar_side, cx);
1363
1364 if active_item.is_focused(cx) {
1365 cx.focus_self();
1366 } else {
1367 cx.focus(active_item.as_any());
1368 }
1369 } else {
1370 cx.focus_self();
1371 }
1372
1373 self.serialize_workspace(cx);
1374
1375 cx.notify();
1376 }
1377
1378 pub fn toggle_sidebar_item_focus(
1379 &mut self,
1380 sidebar_side: SidebarSide,
1381 item_index: usize,
1382 cx: &mut ViewContext<Self>,
1383 ) {
1384 let sidebar = match sidebar_side {
1385 SidebarSide::Left => &mut self.left_sidebar,
1386 SidebarSide::Right => &mut self.right_sidebar,
1387 };
1388 let active_item = sidebar.update(cx, |sidebar, cx| {
1389 sidebar.set_open(true, cx);
1390 sidebar.activate_item(item_index, cx);
1391 sidebar.active_item().cloned()
1392 });
1393 if let Some(active_item) = active_item {
1394 Dock::hide_on_sidebar_shown(self, sidebar_side, cx);
1395
1396 if active_item.is_focused(cx) {
1397 cx.focus_self();
1398 } else {
1399 cx.focus(active_item.as_any());
1400 }
1401 }
1402
1403 self.serialize_workspace(cx);
1404
1405 cx.notify();
1406 }
1407
1408 pub fn focus_center(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
1409 cx.focus_self();
1410 cx.notify();
1411 }
1412
1413 fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
1414 let pane = cx.add_view(|cx| {
1415 Pane::new(
1416 self.weak_handle(),
1417 None,
1418 self.app_state.background_actions,
1419 cx,
1420 )
1421 });
1422 let pane_id = pane.id();
1423 cx.subscribe(&pane, move |this, _, event, cx| {
1424 this.handle_pane_event(pane_id, event, cx)
1425 })
1426 .detach();
1427 self.panes.push(pane.clone());
1428 cx.focus(&pane);
1429 cx.emit(Event::PaneAdded(pane.clone()));
1430 pane
1431 }
1432
1433 pub fn add_item_to_center(
1434 &mut self,
1435 item: Box<dyn ItemHandle>,
1436 cx: &mut ViewContext<Self>,
1437 ) -> bool {
1438 if let Some(center_pane) = self.last_active_center_pane.clone() {
1439 if let Some(center_pane) = center_pane.upgrade(cx) {
1440 Pane::add_item(self, ¢er_pane, item, true, true, None, cx);
1441 true
1442 } else {
1443 false
1444 }
1445 } else {
1446 false
1447 }
1448 }
1449
1450 pub fn add_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
1451 let active_pane = self.active_pane().clone();
1452 Pane::add_item(self, &active_pane, item, true, true, None, cx);
1453 }
1454
1455 pub fn open_path(
1456 &mut self,
1457 path: impl Into<ProjectPath>,
1458 pane: Option<WeakViewHandle<Pane>>,
1459 focus_item: bool,
1460 cx: &mut ViewContext<Self>,
1461 ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
1462 let pane = pane.unwrap_or_else(|| {
1463 if !self.dock_active() {
1464 self.active_pane().downgrade()
1465 } else {
1466 self.last_active_center_pane.clone().unwrap_or_else(|| {
1467 self.panes
1468 .first()
1469 .expect("There must be an active pane")
1470 .downgrade()
1471 })
1472 }
1473 });
1474
1475 let task = self.load_path(path.into(), cx);
1476 cx.spawn(|this, mut cx| async move {
1477 let (project_entry_id, build_item) = task.await?;
1478 let pane = pane
1479 .upgrade(&cx)
1480 .ok_or_else(|| anyhow!("pane was closed"))?;
1481 this.update(&mut cx, |this, cx| {
1482 Pane::open_item(this, pane, project_entry_id, focus_item, cx, build_item)
1483 })
1484 })
1485 }
1486
1487 pub(crate) fn load_path(
1488 &mut self,
1489 path: ProjectPath,
1490 cx: &mut ViewContext<Self>,
1491 ) -> Task<
1492 Result<(
1493 ProjectEntryId,
1494 impl 'static + FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
1495 )>,
1496 > {
1497 let project = self.project().clone();
1498 let project_item = project.update(cx, |project, cx| project.open_path(path, cx));
1499 cx.spawn(|_, mut cx| async move {
1500 let (project_entry_id, project_item) = project_item.await?;
1501 let build_item = cx.update(|cx| {
1502 cx.default_global::<ProjectItemBuilders>()
1503 .get(&project_item.model_type())
1504 .ok_or_else(|| anyhow!("no item builder for project item"))
1505 .cloned()
1506 })?;
1507 let build_item =
1508 move |cx: &mut ViewContext<Pane>| build_item(project, project_item, cx);
1509 Ok((project_entry_id, build_item))
1510 })
1511 }
1512
1513 pub fn open_project_item<T>(
1514 &mut self,
1515 project_item: ModelHandle<T::Item>,
1516 cx: &mut ViewContext<Self>,
1517 ) -> ViewHandle<T>
1518 where
1519 T: ProjectItem,
1520 {
1521 use project::Item as _;
1522
1523 let entry_id = project_item.read(cx).entry_id(cx);
1524 if let Some(item) = entry_id
1525 .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id, cx))
1526 .and_then(|item| item.downcast())
1527 {
1528 self.activate_item(&item, cx);
1529 return item;
1530 }
1531
1532 let item = cx.add_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
1533 self.add_item(Box::new(item.clone()), cx);
1534 item
1535 }
1536
1537 pub fn open_shared_screen(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
1538 if let Some(shared_screen) = self.shared_screen_for_peer(peer_id, &self.active_pane, cx) {
1539 let pane = self.active_pane.clone();
1540 Pane::add_item(self, &pane, Box::new(shared_screen), false, true, None, cx);
1541 }
1542 }
1543
1544 pub fn activate_item(&mut self, item: &dyn ItemHandle, cx: &mut ViewContext<Self>) -> bool {
1545 let result = self.panes.iter().find_map(|pane| {
1546 pane.read(cx)
1547 .index_for_item(item)
1548 .map(|ix| (pane.clone(), ix))
1549 });
1550 if let Some((pane, ix)) = result {
1551 pane.update(cx, |pane, cx| pane.activate_item(ix, true, true, cx));
1552 true
1553 } else {
1554 false
1555 }
1556 }
1557
1558 fn activate_pane_at_index(&mut self, action: &ActivatePane, cx: &mut ViewContext<Self>) {
1559 let panes = self.center.panes();
1560 if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
1561 cx.focus(&pane);
1562 } else {
1563 self.split_pane(self.active_pane.clone(), SplitDirection::Right, cx);
1564 }
1565 }
1566
1567 pub fn activate_next_pane(&mut self, cx: &mut ViewContext<Self>) {
1568 let panes = self.center.panes();
1569 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
1570 let next_ix = (ix + 1) % panes.len();
1571 let next_pane = panes[next_ix].clone();
1572 cx.focus(&next_pane);
1573 }
1574 }
1575
1576 pub fn activate_previous_pane(&mut self, cx: &mut ViewContext<Self>) {
1577 let panes = self.center.panes();
1578 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
1579 let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
1580 let prev_pane = panes[prev_ix].clone();
1581 cx.focus(&prev_pane);
1582 }
1583 }
1584
1585 fn handle_pane_focused(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
1586 if self.active_pane != pane {
1587 self.active_pane
1588 .update(cx, |pane, cx| pane.set_active(false, cx));
1589 self.active_pane = pane.clone();
1590 self.active_pane
1591 .update(cx, |pane, cx| pane.set_active(true, cx));
1592 self.status_bar.update(cx, |status_bar, cx| {
1593 status_bar.set_active_pane(&self.active_pane, cx);
1594 });
1595 self.active_item_path_changed(cx);
1596
1597 if &pane == self.dock_pane() {
1598 Dock::show(self, true, cx);
1599 } else {
1600 self.last_active_center_pane = Some(pane.downgrade());
1601 if self.dock.is_anchored_at(DockAnchor::Expanded) {
1602 Dock::hide(self, cx);
1603 }
1604 }
1605 cx.notify();
1606 }
1607
1608 self.update_followers(
1609 proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView {
1610 id: self.active_item(cx).and_then(|item| {
1611 item.to_followable_item_handle(cx)?
1612 .remote_id(&self.app_state.client, cx)
1613 .map(|id| id.to_proto())
1614 }),
1615 leader_id: self.leader_for_pane(&pane),
1616 }),
1617 cx,
1618 );
1619 }
1620
1621 fn handle_pane_event(
1622 &mut self,
1623 pane_id: usize,
1624 event: &pane::Event,
1625 cx: &mut ViewContext<Self>,
1626 ) {
1627 if let Some(pane) = self.pane(pane_id) {
1628 let is_dock = &pane == self.dock.pane();
1629 match event {
1630 pane::Event::Split(direction) if !is_dock => {
1631 self.split_pane(pane, *direction, cx);
1632 }
1633 pane::Event::Remove if !is_dock => self.remove_pane(pane, cx),
1634 pane::Event::Remove if is_dock => Dock::hide(self, cx),
1635 pane::Event::ActivateItem { local } => {
1636 if *local {
1637 self.unfollow(&pane, cx);
1638 }
1639 if &pane == self.active_pane() {
1640 self.active_item_path_changed(cx);
1641 }
1642 }
1643 pane::Event::ChangeItemTitle => {
1644 if pane == self.active_pane {
1645 self.active_item_path_changed(cx);
1646 }
1647 self.update_window_edited(cx);
1648 }
1649 pane::Event::RemoveItem { item_id } => {
1650 self.update_window_edited(cx);
1651 if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) {
1652 if entry.get().id() == pane.id() {
1653 entry.remove();
1654 }
1655 }
1656 }
1657 _ => {}
1658 }
1659
1660 self.serialize_workspace(cx);
1661 } else if self.dock.visible_pane().is_none() {
1662 error!("pane {} not found", pane_id);
1663 }
1664 }
1665
1666 pub fn split_pane(
1667 &mut self,
1668 pane: ViewHandle<Pane>,
1669 direction: SplitDirection,
1670 cx: &mut ViewContext<Self>,
1671 ) -> Option<ViewHandle<Pane>> {
1672 if &pane == self.dock_pane() {
1673 warn!("Can't split dock pane.");
1674 return None;
1675 }
1676
1677 let item = pane.read(cx).active_item()?;
1678 let maybe_pane_handle = if let Some(clone) = item.clone_on_split(self.database_id(), cx) {
1679 let new_pane = self.add_pane(cx);
1680 Pane::add_item(self, &new_pane, clone, true, true, None, cx);
1681 self.center.split(&pane, &new_pane, direction).unwrap();
1682 Some(new_pane)
1683 } else {
1684 None
1685 };
1686 cx.notify();
1687 maybe_pane_handle
1688 }
1689
1690 pub fn split_pane_with_item(
1691 &mut self,
1692 pane_to_split: WeakViewHandle<Pane>,
1693 split_direction: SplitDirection,
1694 from: WeakViewHandle<Pane>,
1695 item_id_to_move: usize,
1696 cx: &mut ViewContext<Self>,
1697 ) {
1698 let Some(pane_to_split) = pane_to_split.upgrade(cx) else { return; };
1699 let Some(from) = from.upgrade(cx) else { return; };
1700 if &pane_to_split == self.dock_pane() {
1701 warn!("Can't split dock pane.");
1702 return;
1703 }
1704
1705 let new_pane = self.add_pane(cx);
1706 Pane::move_item(self, from.clone(), new_pane.clone(), item_id_to_move, 0, cx);
1707 self.center
1708 .split(&pane_to_split, &new_pane, split_direction)
1709 .unwrap();
1710 cx.notify();
1711 }
1712
1713 pub fn split_pane_with_project_entry(
1714 &mut self,
1715 pane_to_split: WeakViewHandle<Pane>,
1716 split_direction: SplitDirection,
1717 project_entry: ProjectEntryId,
1718 cx: &mut ViewContext<Self>,
1719 ) -> Option<Task<Result<()>>> {
1720 let pane_to_split = pane_to_split.upgrade(cx)?;
1721 if &pane_to_split == self.dock_pane() {
1722 warn!("Can't split dock pane.");
1723 return None;
1724 }
1725
1726 let new_pane = self.add_pane(cx);
1727 self.center
1728 .split(&pane_to_split, &new_pane, split_direction)
1729 .unwrap();
1730
1731 let path = self.project.read(cx).path_for_entry(project_entry, cx)?;
1732 let task = self.open_path(path, Some(new_pane.downgrade()), true, cx);
1733 Some(cx.foreground().spawn(async move {
1734 task.await?;
1735 Ok(())
1736 }))
1737 }
1738
1739 fn remove_pane(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
1740 if self.center.remove(&pane).unwrap() {
1741 self.panes.retain(|p| p != &pane);
1742 cx.focus(self.panes.last().unwrap());
1743 self.unfollow(&pane, cx);
1744 self.last_leaders_by_pane.remove(&pane.downgrade());
1745 for removed_item in pane.read(cx).items() {
1746 self.panes_by_item.remove(&removed_item.id());
1747 }
1748 if self.last_active_center_pane == Some(pane.downgrade()) {
1749 self.last_active_center_pane = None;
1750 }
1751
1752 cx.notify();
1753 } else {
1754 self.active_item_path_changed(cx);
1755 }
1756 }
1757
1758 pub fn panes(&self) -> &[ViewHandle<Pane>] {
1759 &self.panes
1760 }
1761
1762 fn pane(&self, pane_id: usize) -> Option<ViewHandle<Pane>> {
1763 self.panes.iter().find(|pane| pane.id() == pane_id).cloned()
1764 }
1765
1766 pub fn active_pane(&self) -> &ViewHandle<Pane> {
1767 &self.active_pane
1768 }
1769
1770 pub fn dock_pane(&self) -> &ViewHandle<Pane> {
1771 self.dock.pane()
1772 }
1773
1774 fn dock_active(&self) -> bool {
1775 &self.active_pane == self.dock.pane()
1776 }
1777
1778 fn project_remote_id_changed(&mut self, remote_id: Option<u64>, cx: &mut ViewContext<Self>) {
1779 if let Some(remote_id) = remote_id {
1780 self.remote_entity_subscription = Some(
1781 self.app_state
1782 .client
1783 .add_view_for_remote_entity(remote_id, cx),
1784 );
1785 } else {
1786 self.remote_entity_subscription.take();
1787 }
1788 }
1789
1790 fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
1791 self.leader_state.followers.remove(&peer_id);
1792 if let Some(states_by_pane) = self.follower_states_by_leader.remove(&peer_id) {
1793 for state in states_by_pane.into_values() {
1794 for item in state.items_by_leader_view_id.into_values() {
1795 item.set_leader_replica_id(None, cx);
1796 }
1797 }
1798 }
1799 cx.notify();
1800 }
1801
1802 pub fn toggle_follow(
1803 &mut self,
1804 leader_id: PeerId,
1805 cx: &mut ViewContext<Self>,
1806 ) -> Option<Task<Result<()>>> {
1807 let pane = self.active_pane().clone();
1808
1809 if let Some(prev_leader_id) = self.unfollow(&pane, cx) {
1810 if leader_id == prev_leader_id {
1811 return None;
1812 }
1813 }
1814
1815 self.last_leaders_by_pane
1816 .insert(pane.downgrade(), leader_id);
1817 self.follower_states_by_leader
1818 .entry(leader_id)
1819 .or_default()
1820 .insert(pane.clone(), Default::default());
1821 cx.notify();
1822
1823 let project_id = self.project.read(cx).remote_id()?;
1824 let request = self.app_state.client.request(proto::Follow {
1825 project_id,
1826 leader_id: Some(leader_id),
1827 });
1828
1829 Some(cx.spawn(|this, mut cx| async move {
1830 let response = request.await?;
1831 this.update(&mut cx, |this, _| {
1832 let state = this
1833 .follower_states_by_leader
1834 .get_mut(&leader_id)
1835 .and_then(|states_by_pane| states_by_pane.get_mut(&pane))
1836 .ok_or_else(|| anyhow!("following interrupted"))?;
1837 state.active_view_id = if let Some(active_view_id) = response.active_view_id {
1838 Some(ViewId::from_proto(active_view_id)?)
1839 } else {
1840 None
1841 };
1842 Ok::<_, anyhow::Error>(())
1843 })??;
1844 Self::add_views_from_leader(
1845 this.clone(),
1846 leader_id,
1847 vec![pane],
1848 response.views,
1849 &mut cx,
1850 )
1851 .await?;
1852 this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx))?;
1853 Ok(())
1854 }))
1855 }
1856
1857 pub fn follow_next_collaborator(
1858 &mut self,
1859 _: &FollowNextCollaborator,
1860 cx: &mut ViewContext<Self>,
1861 ) -> Option<Task<Result<()>>> {
1862 let collaborators = self.project.read(cx).collaborators();
1863 let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
1864 let mut collaborators = collaborators.keys().copied();
1865 for peer_id in collaborators.by_ref() {
1866 if peer_id == leader_id {
1867 break;
1868 }
1869 }
1870 collaborators.next()
1871 } else if let Some(last_leader_id) =
1872 self.last_leaders_by_pane.get(&self.active_pane.downgrade())
1873 {
1874 if collaborators.contains_key(last_leader_id) {
1875 Some(*last_leader_id)
1876 } else {
1877 None
1878 }
1879 } else {
1880 None
1881 };
1882
1883 next_leader_id
1884 .or_else(|| collaborators.keys().copied().next())
1885 .and_then(|leader_id| self.toggle_follow(leader_id, cx))
1886 }
1887
1888 pub fn unfollow(
1889 &mut self,
1890 pane: &ViewHandle<Pane>,
1891 cx: &mut ViewContext<Self>,
1892 ) -> Option<PeerId> {
1893 for (leader_id, states_by_pane) in &mut self.follower_states_by_leader {
1894 let leader_id = *leader_id;
1895 if let Some(state) = states_by_pane.remove(pane) {
1896 for (_, item) in state.items_by_leader_view_id {
1897 item.set_leader_replica_id(None, cx);
1898 }
1899
1900 if states_by_pane.is_empty() {
1901 self.follower_states_by_leader.remove(&leader_id);
1902 if let Some(project_id) = self.project.read(cx).remote_id() {
1903 self.app_state
1904 .client
1905 .send(proto::Unfollow {
1906 project_id,
1907 leader_id: Some(leader_id),
1908 })
1909 .log_err();
1910 }
1911 }
1912
1913 cx.notify();
1914 return Some(leader_id);
1915 }
1916 }
1917 None
1918 }
1919
1920 pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
1921 self.follower_states_by_leader.contains_key(&peer_id)
1922 }
1923
1924 pub fn is_followed_by(&self, peer_id: PeerId) -> bool {
1925 self.leader_state.followers.contains(&peer_id)
1926 }
1927
1928 fn render_titlebar(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
1929 // TODO: There should be a better system in place for this
1930 // (https://github.com/zed-industries/zed/issues/1290)
1931 let is_fullscreen = cx.window_is_fullscreen();
1932 let container_theme = if is_fullscreen {
1933 let mut container_theme = theme.workspace.titlebar.container;
1934 container_theme.padding.left = container_theme.padding.right;
1935 container_theme
1936 } else {
1937 theme.workspace.titlebar.container
1938 };
1939
1940 enum TitleBar {}
1941 MouseEventHandler::<TitleBar, _>::new(0, cx, |_, cx| {
1942 Stack::new()
1943 .with_children(
1944 self.titlebar_item
1945 .as_ref()
1946 .map(|item| ChildView::new(item, cx)),
1947 )
1948 .contained()
1949 .with_style(container_theme)
1950 })
1951 .on_click(MouseButton::Left, |event, _, cx| {
1952 if event.click_count == 2 {
1953 cx.zoom_window();
1954 }
1955 })
1956 .constrained()
1957 .with_height(theme.workspace.titlebar.height)
1958 .into_any_named("titlebar")
1959 }
1960
1961 fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
1962 let active_entry = self.active_project_path(cx);
1963 self.project
1964 .update(cx, |project, cx| project.set_active_path(active_entry, cx));
1965 self.update_window_title(cx);
1966 }
1967
1968 fn update_window_title(&mut self, cx: &mut ViewContext<Self>) {
1969 let project = self.project().read(cx);
1970 let mut title = String::new();
1971
1972 if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
1973 let filename = path
1974 .path
1975 .file_name()
1976 .map(|s| s.to_string_lossy())
1977 .or_else(|| {
1978 Some(Cow::Borrowed(
1979 project
1980 .worktree_for_id(path.worktree_id, cx)?
1981 .read(cx)
1982 .root_name(),
1983 ))
1984 });
1985
1986 if let Some(filename) = filename {
1987 title.push_str(filename.as_ref());
1988 title.push_str(" — ");
1989 }
1990 }
1991
1992 for (i, name) in project.worktree_root_names(cx).enumerate() {
1993 if i > 0 {
1994 title.push_str(", ");
1995 }
1996 title.push_str(name);
1997 }
1998
1999 if title.is_empty() {
2000 title = "empty project".to_string();
2001 }
2002
2003 if project.is_remote() {
2004 title.push_str(" ↙");
2005 } else if project.is_shared() {
2006 title.push_str(" ↗");
2007 }
2008
2009 cx.set_window_title(&title);
2010 }
2011
2012 fn update_window_edited(&mut self, cx: &mut ViewContext<Self>) {
2013 let is_edited = !self.project.read(cx).is_read_only()
2014 && self
2015 .items(cx)
2016 .any(|item| item.has_conflict(cx) || item.is_dirty(cx));
2017 if is_edited != self.window_edited {
2018 self.window_edited = is_edited;
2019 cx.set_window_edited(self.window_edited)
2020 }
2021 }
2022
2023 fn render_disconnected_overlay(
2024 &self,
2025 cx: &mut ViewContext<Workspace>,
2026 ) -> Option<AnyElement<Workspace>> {
2027 if self.project.read(cx).is_read_only() {
2028 enum DisconnectedOverlay {}
2029 Some(
2030 MouseEventHandler::<DisconnectedOverlay, _>::new(0, cx, |_, cx| {
2031 let theme = &cx.global::<Settings>().theme;
2032 Label::new(
2033 "Your connection to the remote project has been lost.",
2034 theme.workspace.disconnected_overlay.text.clone(),
2035 )
2036 .aligned()
2037 .contained()
2038 .with_style(theme.workspace.disconnected_overlay.container)
2039 })
2040 .with_cursor_style(CursorStyle::Arrow)
2041 .capture_all()
2042 .into_any_named("disconnected overlay"),
2043 )
2044 } else {
2045 None
2046 }
2047 }
2048
2049 fn render_notifications(
2050 &self,
2051 theme: &theme::Workspace,
2052 cx: &AppContext,
2053 ) -> Option<AnyElement<Workspace>> {
2054 if self.notifications.is_empty() {
2055 None
2056 } else {
2057 Some(
2058 Flex::column()
2059 .with_children(self.notifications.iter().map(|(_, _, notification)| {
2060 ChildView::new(notification.as_any(), cx)
2061 .contained()
2062 .with_style(theme.notification)
2063 }))
2064 .constrained()
2065 .with_width(theme.notifications.width)
2066 .contained()
2067 .with_style(theme.notifications.container)
2068 .aligned()
2069 .bottom()
2070 .right()
2071 .into_any(),
2072 )
2073 }
2074 }
2075
2076 // RPC handlers
2077
2078 async fn handle_follow(
2079 this: WeakViewHandle<Self>,
2080 envelope: TypedEnvelope<proto::Follow>,
2081 _: Arc<Client>,
2082 mut cx: AsyncAppContext,
2083 ) -> Result<proto::FollowResponse> {
2084 this.update(&mut cx, |this, cx| {
2085 let client = &this.app_state.client;
2086 this.leader_state
2087 .followers
2088 .insert(envelope.original_sender_id()?);
2089
2090 let active_view_id = this.active_item(cx).and_then(|i| {
2091 Some(
2092 i.to_followable_item_handle(cx)?
2093 .remote_id(client, cx)?
2094 .to_proto(),
2095 )
2096 });
2097
2098 cx.notify();
2099
2100 Ok(proto::FollowResponse {
2101 active_view_id,
2102 views: this
2103 .panes()
2104 .iter()
2105 .flat_map(|pane| {
2106 let leader_id = this.leader_for_pane(pane);
2107 pane.read(cx).items().filter_map({
2108 let cx = &cx;
2109 move |item| {
2110 let item = item.to_followable_item_handle(cx)?;
2111 let id = item.remote_id(client, cx)?.to_proto();
2112 let variant = item.to_state_proto(cx)?;
2113 Some(proto::View {
2114 id: Some(id),
2115 leader_id,
2116 variant: Some(variant),
2117 })
2118 }
2119 })
2120 })
2121 .collect(),
2122 })
2123 })?
2124 }
2125
2126 async fn handle_unfollow(
2127 this: WeakViewHandle<Self>,
2128 envelope: TypedEnvelope<proto::Unfollow>,
2129 _: Arc<Client>,
2130 mut cx: AsyncAppContext,
2131 ) -> Result<()> {
2132 this.update(&mut cx, |this, cx| {
2133 this.leader_state
2134 .followers
2135 .remove(&envelope.original_sender_id()?);
2136 cx.notify();
2137 Ok(())
2138 })?
2139 }
2140
2141 async fn handle_update_followers(
2142 this: WeakViewHandle<Self>,
2143 envelope: TypedEnvelope<proto::UpdateFollowers>,
2144 _: Arc<Client>,
2145 cx: AsyncAppContext,
2146 ) -> Result<()> {
2147 let leader_id = envelope.original_sender_id()?;
2148 this.read_with(&cx, |this, _| {
2149 this.leader_updates_tx
2150 .unbounded_send((leader_id, envelope.payload))
2151 })??;
2152 Ok(())
2153 }
2154
2155 async fn process_leader_update(
2156 this: &WeakViewHandle<Self>,
2157 leader_id: PeerId,
2158 update: proto::UpdateFollowers,
2159 cx: &mut AsyncAppContext,
2160 ) -> Result<()> {
2161 match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
2162 proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
2163 this.update(cx, |this, _| {
2164 if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
2165 for state in state.values_mut() {
2166 state.active_view_id =
2167 if let Some(active_view_id) = update_active_view.id.clone() {
2168 Some(ViewId::from_proto(active_view_id)?)
2169 } else {
2170 None
2171 };
2172 }
2173 }
2174 anyhow::Ok(())
2175 })??;
2176 }
2177 proto::update_followers::Variant::UpdateView(update_view) => {
2178 let variant = update_view
2179 .variant
2180 .ok_or_else(|| anyhow!("missing update view variant"))?;
2181 let id = update_view
2182 .id
2183 .ok_or_else(|| anyhow!("missing update view id"))?;
2184 let mut tasks = Vec::new();
2185 this.update(cx, |this, cx| {
2186 let project = this.project.clone();
2187 if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
2188 for state in state.values_mut() {
2189 let view_id = ViewId::from_proto(id.clone())?;
2190 if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
2191 tasks.push(item.apply_update_proto(&project, variant.clone(), cx));
2192 }
2193 }
2194 }
2195 anyhow::Ok(())
2196 })??;
2197 try_join_all(tasks).await.log_err();
2198 }
2199 proto::update_followers::Variant::CreateView(view) => {
2200 let panes = this.read_with(cx, |this, _| {
2201 this.follower_states_by_leader
2202 .get(&leader_id)
2203 .into_iter()
2204 .flat_map(|states_by_pane| states_by_pane.keys())
2205 .cloned()
2206 .collect()
2207 })?;
2208 Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], cx).await?;
2209 }
2210 }
2211 this.update(cx, |this, cx| this.leader_updated(leader_id, cx))?;
2212 Ok(())
2213 }
2214
2215 async fn add_views_from_leader(
2216 this: WeakViewHandle<Self>,
2217 leader_id: PeerId,
2218 panes: Vec<ViewHandle<Pane>>,
2219 views: Vec<proto::View>,
2220 cx: &mut AsyncAppContext,
2221 ) -> Result<()> {
2222 let project = this.read_with(cx, |this, _| this.project.clone())?;
2223 let replica_id = project
2224 .read_with(cx, |project, _| {
2225 project
2226 .collaborators()
2227 .get(&leader_id)
2228 .map(|c| c.replica_id)
2229 })
2230 .ok_or_else(|| anyhow!("no such collaborator {}", leader_id))?;
2231
2232 let item_builders = cx.update(|cx| {
2233 cx.default_global::<FollowableItemBuilders>()
2234 .values()
2235 .map(|b| b.0)
2236 .collect::<Vec<_>>()
2237 });
2238
2239 let mut item_tasks_by_pane = HashMap::default();
2240 for pane in panes {
2241 let mut item_tasks = Vec::new();
2242 let mut leader_view_ids = Vec::new();
2243 for view in &views {
2244 let Some(id) = &view.id else { continue };
2245 let id = ViewId::from_proto(id.clone())?;
2246 let mut variant = view.variant.clone();
2247 if variant.is_none() {
2248 Err(anyhow!("missing variant"))?;
2249 }
2250 for build_item in &item_builders {
2251 let task = cx.update(|cx| {
2252 build_item(pane.clone(), project.clone(), id, &mut variant, cx)
2253 });
2254 if let Some(task) = task {
2255 item_tasks.push(task);
2256 leader_view_ids.push(id);
2257 break;
2258 } else {
2259 assert!(variant.is_some());
2260 }
2261 }
2262 }
2263
2264 item_tasks_by_pane.insert(pane, (item_tasks, leader_view_ids));
2265 }
2266
2267 for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane {
2268 let items = futures::future::try_join_all(item_tasks).await?;
2269 this.update(cx, |this, cx| {
2270 let state = this
2271 .follower_states_by_leader
2272 .get_mut(&leader_id)?
2273 .get_mut(&pane)?;
2274
2275 for (id, item) in leader_view_ids.into_iter().zip(items) {
2276 item.set_leader_replica_id(Some(replica_id), cx);
2277 state.items_by_leader_view_id.insert(id, item);
2278 }
2279
2280 Some(())
2281 })?;
2282 }
2283 Ok(())
2284 }
2285
2286 fn update_followers(
2287 &self,
2288 update: proto::update_followers::Variant,
2289 cx: &AppContext,
2290 ) -> Option<()> {
2291 let project_id = self.project.read(cx).remote_id()?;
2292 if !self.leader_state.followers.is_empty() {
2293 self.app_state
2294 .client
2295 .send(proto::UpdateFollowers {
2296 project_id,
2297 follower_ids: self.leader_state.followers.iter().copied().collect(),
2298 variant: Some(update),
2299 })
2300 .log_err();
2301 }
2302 None
2303 }
2304
2305 pub fn leader_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<PeerId> {
2306 self.follower_states_by_leader
2307 .iter()
2308 .find_map(|(leader_id, state)| {
2309 if state.contains_key(pane) {
2310 Some(*leader_id)
2311 } else {
2312 None
2313 }
2314 })
2315 }
2316
2317 fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
2318 cx.notify();
2319
2320 let call = self.active_call()?;
2321 let room = call.read(cx).room()?.read(cx);
2322 let participant = room.remote_participant_for_peer_id(leader_id)?;
2323 let mut items_to_activate = Vec::new();
2324 match participant.location {
2325 call::ParticipantLocation::SharedProject { project_id } => {
2326 if Some(project_id) == self.project.read(cx).remote_id() {
2327 for (pane, state) in self.follower_states_by_leader.get(&leader_id)? {
2328 if let Some(item) = state
2329 .active_view_id
2330 .and_then(|id| state.items_by_leader_view_id.get(&id))
2331 {
2332 items_to_activate.push((pane.clone(), item.boxed_clone()));
2333 } else {
2334 if let Some(shared_screen) =
2335 self.shared_screen_for_peer(leader_id, pane, cx)
2336 {
2337 items_to_activate.push((pane.clone(), Box::new(shared_screen)));
2338 }
2339 }
2340 }
2341 }
2342 }
2343 call::ParticipantLocation::UnsharedProject => {}
2344 call::ParticipantLocation::External => {
2345 for (pane, _) in self.follower_states_by_leader.get(&leader_id)? {
2346 if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
2347 items_to_activate.push((pane.clone(), Box::new(shared_screen)));
2348 }
2349 }
2350 }
2351 }
2352
2353 for (pane, item) in items_to_activate {
2354 let active_item_was_focused = pane
2355 .read(cx)
2356 .active_item()
2357 .map(|active_item| cx.is_child_focused(active_item.as_any()))
2358 .unwrap_or_default();
2359
2360 if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) {
2361 pane.update(cx, |pane, cx| pane.activate_item(index, false, false, cx));
2362 } else {
2363 Pane::add_item(self, &pane, item.boxed_clone(), false, false, None, cx);
2364 }
2365
2366 if active_item_was_focused {
2367 pane.update(cx, |pane, cx| pane.focus_active_item(cx));
2368 }
2369 }
2370
2371 None
2372 }
2373
2374 fn shared_screen_for_peer(
2375 &self,
2376 peer_id: PeerId,
2377 pane: &ViewHandle<Pane>,
2378 cx: &mut ViewContext<Self>,
2379 ) -> Option<ViewHandle<SharedScreen>> {
2380 let call = self.active_call()?;
2381 let room = call.read(cx).room()?.read(cx);
2382 let participant = room.remote_participant_for_peer_id(peer_id)?;
2383 let track = participant.tracks.values().next()?.clone();
2384 let user = participant.user.clone();
2385
2386 for item in pane.read(cx).items_of_type::<SharedScreen>() {
2387 if item.read(cx).peer_id == peer_id {
2388 return Some(item);
2389 }
2390 }
2391
2392 Some(cx.add_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx)))
2393 }
2394
2395 pub fn on_window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
2396 if active {
2397 cx.background()
2398 .spawn(persistence::DB.update_timestamp(self.database_id()))
2399 .detach();
2400 } else {
2401 for pane in &self.panes {
2402 pane.update(cx, |pane, cx| {
2403 if let Some(item) = pane.active_item() {
2404 item.workspace_deactivated(cx);
2405 }
2406 if matches!(
2407 cx.global::<Settings>().autosave,
2408 Autosave::OnWindowChange | Autosave::OnFocusChange
2409 ) {
2410 for item in pane.items() {
2411 Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
2412 .detach_and_log_err(cx);
2413 }
2414 }
2415 });
2416 }
2417 }
2418 }
2419
2420 fn active_call(&self) -> Option<&ModelHandle<ActiveCall>> {
2421 self.active_call.as_ref().map(|(call, _)| call)
2422 }
2423
2424 fn on_active_call_event(
2425 &mut self,
2426 _: ModelHandle<ActiveCall>,
2427 event: &call::room::Event,
2428 cx: &mut ViewContext<Self>,
2429 ) {
2430 match event {
2431 call::room::Event::ParticipantLocationChanged { participant_id }
2432 | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
2433 self.leader_updated(*participant_id, cx);
2434 }
2435 _ => {}
2436 }
2437 }
2438
2439 pub fn database_id(&self) -> WorkspaceId {
2440 self.database_id
2441 }
2442
2443 fn location(&self, cx: &AppContext) -> Option<WorkspaceLocation> {
2444 let project = self.project().read(cx);
2445
2446 if project.is_local() {
2447 Some(
2448 project
2449 .visible_worktrees(cx)
2450 .map(|worktree| worktree.read(cx).abs_path())
2451 .collect::<Vec<_>>()
2452 .into(),
2453 )
2454 } else {
2455 None
2456 }
2457 }
2458
2459 fn remove_panes(&mut self, member: Member, cx: &mut ViewContext<Workspace>) {
2460 match member {
2461 Member::Axis(PaneAxis { members, .. }) => {
2462 for child in members.iter() {
2463 self.remove_panes(child.clone(), cx)
2464 }
2465 }
2466 Member::Pane(pane) => self.remove_pane(pane.clone(), cx),
2467 }
2468 }
2469
2470 fn serialize_workspace(&self, cx: &AppContext) {
2471 fn serialize_pane_handle(
2472 pane_handle: &ViewHandle<Pane>,
2473 cx: &AppContext,
2474 ) -> SerializedPane {
2475 let (items, active) = {
2476 let pane = pane_handle.read(cx);
2477 let active_item_id = pane.active_item().map(|item| item.id());
2478 (
2479 pane.items()
2480 .filter_map(|item_handle| {
2481 Some(SerializedItem {
2482 kind: Arc::from(item_handle.serialized_item_kind()?),
2483 item_id: item_handle.id(),
2484 active: Some(item_handle.id()) == active_item_id,
2485 })
2486 })
2487 .collect::<Vec<_>>(),
2488 pane.is_active(),
2489 )
2490 };
2491
2492 SerializedPane::new(items, active)
2493 }
2494
2495 fn build_serialized_pane_group(
2496 pane_group: &Member,
2497 cx: &AppContext,
2498 ) -> SerializedPaneGroup {
2499 match pane_group {
2500 Member::Axis(PaneAxis { axis, members }) => SerializedPaneGroup::Group {
2501 axis: *axis,
2502 children: members
2503 .iter()
2504 .map(|member| build_serialized_pane_group(member, cx))
2505 .collect::<Vec<_>>(),
2506 },
2507 Member::Pane(pane_handle) => {
2508 SerializedPaneGroup::Pane(serialize_pane_handle(&pane_handle, cx))
2509 }
2510 }
2511 }
2512
2513 if let Some(location) = self.location(cx) {
2514 // Load bearing special case:
2515 // - with_local_workspace() relies on this to not have other stuff open
2516 // when you open your log
2517 if !location.paths().is_empty() {
2518 let dock_pane = serialize_pane_handle(self.dock.pane(), cx);
2519 let center_group = build_serialized_pane_group(&self.center.root, cx);
2520
2521 let serialized_workspace = SerializedWorkspace {
2522 id: self.database_id,
2523 location,
2524 dock_position: self.dock.position(),
2525 dock_pane,
2526 center_group,
2527 left_sidebar_open: self.left_sidebar.read(cx).is_open(),
2528 bounds: Default::default(),
2529 display: Default::default(),
2530 };
2531
2532 cx.background()
2533 .spawn(persistence::DB.save_workspace(serialized_workspace))
2534 .detach();
2535 }
2536 }
2537 }
2538
2539 fn load_from_serialized_workspace(
2540 workspace: WeakViewHandle<Workspace>,
2541 serialized_workspace: SerializedWorkspace,
2542 cx: &mut AppContext,
2543 ) {
2544 cx.spawn(|mut cx| async move {
2545 let (project, dock_pane_handle, old_center_pane) =
2546 workspace.read_with(&cx, |workspace, _| {
2547 (
2548 workspace.project().clone(),
2549 workspace.dock_pane().downgrade(),
2550 workspace.last_active_center_pane.clone(),
2551 )
2552 })?;
2553
2554 serialized_workspace
2555 .dock_pane
2556 .deserialize_to(
2557 &project,
2558 &dock_pane_handle,
2559 serialized_workspace.id,
2560 &workspace,
2561 &mut cx,
2562 )
2563 .await?;
2564
2565 // Traverse the splits tree and add to things
2566 let center_group = serialized_workspace
2567 .center_group
2568 .deserialize(&project, serialized_workspace.id, &workspace, &mut cx)
2569 .await;
2570
2571 // Remove old panes from workspace panes list
2572 workspace.update(&mut cx, |workspace, cx| {
2573 if let Some((center_group, active_pane)) = center_group {
2574 workspace.remove_panes(workspace.center.root.clone(), cx);
2575
2576 // Swap workspace center group
2577 workspace.center = PaneGroup::with_root(center_group);
2578
2579 // Change the focus to the workspace first so that we retrigger focus in on the pane.
2580 cx.focus_self();
2581
2582 if let Some(active_pane) = active_pane {
2583 cx.focus(&active_pane);
2584 } else {
2585 cx.focus(workspace.panes.last().unwrap());
2586 }
2587 } else {
2588 let old_center_handle = old_center_pane.and_then(|weak| weak.upgrade(cx));
2589 if let Some(old_center_handle) = old_center_handle {
2590 cx.focus(&old_center_handle)
2591 } else {
2592 cx.focus_self()
2593 }
2594 }
2595
2596 if workspace.left_sidebar().read(cx).is_open()
2597 != serialized_workspace.left_sidebar_open
2598 {
2599 workspace.toggle_sidebar(SidebarSide::Left, cx);
2600 }
2601
2602 // Note that without after_window, the focus_self() and
2603 // the focus the dock generates start generating alternating
2604 // focus due to the deferred execution each triggering each other
2605 cx.after_window_update(move |workspace, cx| {
2606 Dock::set_dock_position(
2607 workspace,
2608 serialized_workspace.dock_position,
2609 true,
2610 cx,
2611 );
2612 });
2613
2614 cx.notify();
2615 })?;
2616
2617 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
2618 workspace.read_with(&cx, |workspace, cx| workspace.serialize_workspace(cx))?;
2619 anyhow::Ok(())
2620 })
2621 .detach_and_log_err(cx);
2622 }
2623
2624 #[cfg(any(test, feature = "test-support"))]
2625 pub fn test_new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
2626 let app_state = Arc::new(AppState {
2627 languages: project.read(cx).languages().clone(),
2628 themes: ThemeRegistry::new((), cx.font_cache().clone()),
2629 client: project.read(cx).client(),
2630 user_store: project.read(cx).user_store(),
2631 fs: project.read(cx).fs().clone(),
2632 build_window_options: |_, _, _| Default::default(),
2633 initialize_workspace: |_, _, _| {},
2634 dock_default_item_factory: |_, _| None,
2635 background_actions: || &[],
2636 });
2637 Self::new(None, 0, project, app_state, cx)
2638 }
2639}
2640
2641fn notify_if_database_failed(workspace: &WeakViewHandle<Workspace>, cx: &mut AsyncAppContext) {
2642 const REPORT_ISSUE_URL: &str ="https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";
2643
2644 workspace
2645 .update(cx, |workspace, cx| {
2646 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
2647 workspace.show_notification_once(0, cx, |cx| {
2648 cx.add_view(|_| {
2649 MessageNotification::new("Failed to load any database file.")
2650 .with_click_message("Click to let us know about this error")
2651 .on_click(|cx| cx.platform().open_url(REPORT_ISSUE_URL))
2652 })
2653 });
2654 } else {
2655 let backup_path = (*db::BACKUP_DB_PATH).read();
2656 if let Some(backup_path) = backup_path.clone() {
2657 workspace.show_notification_once(0, cx, move |cx| {
2658 cx.add_view(move |_| {
2659 MessageNotification::new(format!(
2660 "Database file was corrupted. Old database backed up to {}",
2661 backup_path.display()
2662 ))
2663 .with_click_message("Click to show old database in finder")
2664 .on_click(move |cx| {
2665 cx.platform().open_url(&backup_path.to_string_lossy())
2666 })
2667 })
2668 });
2669 }
2670 }
2671 })
2672 .log_err();
2673}
2674
2675impl Entity for Workspace {
2676 type Event = Event;
2677}
2678
2679impl View for Workspace {
2680 fn ui_name() -> &'static str {
2681 "Workspace"
2682 }
2683
2684 fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
2685 let theme = cx.global::<Settings>().theme.clone();
2686 Stack::new()
2687 .with_child(
2688 Flex::column()
2689 .with_child(self.render_titlebar(&theme, cx))
2690 .with_child(
2691 Stack::new()
2692 .with_child({
2693 let project = self.project.clone();
2694 Flex::row()
2695 .with_children(
2696 if self.left_sidebar.read(cx).active_item().is_some() {
2697 Some(
2698 ChildView::new(&self.left_sidebar, cx)
2699 .constrained()
2700 .dynamically(|constraint, _, cx| {
2701 SizeConstraint::new(
2702 Vector2F::new(20., constraint.min.y()),
2703 Vector2F::new(
2704 cx.window_size().x() * 0.8,
2705 constraint.max.y(),
2706 ),
2707 )
2708 }),
2709 )
2710 } else {
2711 None
2712 },
2713 )
2714 .with_child(
2715 FlexItem::new(
2716 Flex::column()
2717 .with_child(
2718 FlexItem::new(self.center.render(
2719 &project,
2720 &theme,
2721 &self.follower_states_by_leader,
2722 self.active_call(),
2723 self.active_pane(),
2724 &self.app_state,
2725 cx,
2726 ))
2727 .flex(1., true),
2728 )
2729 .with_children(self.dock.render(
2730 &theme,
2731 DockAnchor::Bottom,
2732 cx,
2733 )),
2734 )
2735 .flex(1., true),
2736 )
2737 .with_children(self.dock.render(&theme, DockAnchor::Right, cx))
2738 .with_children(
2739 if self.right_sidebar.read(cx).active_item().is_some() {
2740 Some(
2741 ChildView::new(&self.right_sidebar, cx)
2742 .constrained()
2743 .dynamically(|constraint, _, cx| {
2744 SizeConstraint::new(
2745 Vector2F::new(20., constraint.min.y()),
2746 Vector2F::new(
2747 cx.window_size().x() * 0.8,
2748 constraint.max.y(),
2749 ),
2750 )
2751 }),
2752 )
2753 } else {
2754 None
2755 },
2756 )
2757 })
2758 .with_child(Overlay::new(
2759 Stack::new()
2760 .with_children(self.dock.render(
2761 &theme,
2762 DockAnchor::Expanded,
2763 cx,
2764 ))
2765 .with_children(self.modal.as_ref().map(|modal| {
2766 ChildView::new(modal, cx)
2767 .contained()
2768 .with_style(theme.workspace.modal)
2769 .aligned()
2770 .top()
2771 }))
2772 .with_children(self.render_notifications(&theme.workspace, cx)),
2773 ))
2774 .flex(1.0, true),
2775 )
2776 .with_child(ChildView::new(&self.status_bar, cx))
2777 .contained()
2778 .with_background_color(theme.workspace.background),
2779 )
2780 .with_children(DragAndDrop::render(cx))
2781 .with_children(self.render_disconnected_overlay(cx))
2782 .into_any_named("workspace")
2783 }
2784
2785 fn focus_in(&mut self, view: AnyViewHandle, cx: &mut ViewContext<Self>) {
2786 if cx.is_self_focused() {
2787 cx.focus(&self.active_pane);
2788 } else {
2789 for pane in self.panes() {
2790 let view = view.clone();
2791 if pane.update(cx, |_, cx| view.id() == cx.view_id() || cx.is_child(view)) {
2792 self.handle_pane_focused(pane.clone(), cx);
2793 break;
2794 }
2795 }
2796 }
2797 }
2798
2799 fn keymap_context(&self, _: &AppContext) -> KeymapContext {
2800 Self::default_keymap_context()
2801 }
2802}
2803
2804impl ViewId {
2805 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
2806 Ok(Self {
2807 creator: message
2808 .creator
2809 .ok_or_else(|| anyhow!("creator is missing"))?,
2810 id: message.id,
2811 })
2812 }
2813
2814 pub(crate) fn to_proto(&self) -> proto::ViewId {
2815 proto::ViewId {
2816 creator: Some(self.creator),
2817 id: self.id,
2818 }
2819 }
2820}
2821
2822pub trait WorkspaceHandle {
2823 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
2824}
2825
2826impl WorkspaceHandle for ViewHandle<Workspace> {
2827 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
2828 self.read(cx)
2829 .worktrees(cx)
2830 .flat_map(|worktree| {
2831 let worktree_id = worktree.read(cx).id();
2832 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
2833 worktree_id,
2834 path: f.path.clone(),
2835 })
2836 })
2837 .collect::<Vec<_>>()
2838 }
2839}
2840
2841impl std::fmt::Debug for OpenPaths {
2842 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2843 f.debug_struct("OpenPaths")
2844 .field("paths", &self.paths)
2845 .finish()
2846 }
2847}
2848
2849pub struct WorkspaceCreated(WeakViewHandle<Workspace>);
2850
2851pub fn activate_workspace_for_project(
2852 cx: &mut AppContext,
2853 predicate: impl Fn(&mut Project, &mut ModelContext<Project>) -> bool,
2854) -> Option<WeakViewHandle<Workspace>> {
2855 for window_id in cx.window_ids().collect::<Vec<_>>() {
2856 let handle = cx
2857 .update_window(window_id, |cx| {
2858 if let Some(workspace_handle) = cx.root_view().clone().downcast::<Workspace>() {
2859 let project = workspace_handle.read(cx).project.clone();
2860 if project.update(cx, &predicate) {
2861 cx.activate_window();
2862 return Some(workspace_handle.clone());
2863 }
2864 }
2865 None
2866 })
2867 .flatten();
2868
2869 if let Some(handle) = handle {
2870 return Some(handle.downgrade());
2871 }
2872 }
2873 None
2874}
2875
2876pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
2877 DB.last_workspace().await.log_err().flatten()
2878}
2879
2880#[allow(clippy::type_complexity)]
2881pub fn open_paths(
2882 abs_paths: &[PathBuf],
2883 app_state: &Arc<AppState>,
2884 requesting_window_id: Option<usize>,
2885 cx: &mut AppContext,
2886) -> Task<
2887 Result<(
2888 WeakViewHandle<Workspace>,
2889 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
2890 )>,
2891> {
2892 log::info!("open paths {:?}", abs_paths);
2893
2894 // Open paths in existing workspace if possible
2895 let existing =
2896 activate_workspace_for_project(cx, |project, cx| project.contains_paths(abs_paths, cx));
2897
2898 let app_state = app_state.clone();
2899 let abs_paths = abs_paths.to_vec();
2900 cx.spawn(|mut cx| async move {
2901 if let Some(existing) = existing {
2902 Ok((
2903 existing.clone(),
2904 existing
2905 .update(&mut cx, |workspace, cx| {
2906 workspace.open_paths(abs_paths, true, cx)
2907 })?
2908 .await,
2909 ))
2910 } else {
2911 let contains_directory =
2912 futures::future::join_all(abs_paths.iter().map(|path| app_state.fs.is_file(path)))
2913 .await
2914 .contains(&false);
2915
2916 cx.update(|cx| {
2917 let task =
2918 Workspace::new_local(abs_paths, app_state.clone(), requesting_window_id, cx);
2919
2920 cx.spawn(|mut cx| async move {
2921 let (workspace, items) = task.await;
2922
2923 workspace.update(&mut cx, |workspace, cx| {
2924 if contains_directory {
2925 workspace.toggle_sidebar(SidebarSide::Left, cx);
2926 }
2927 })?;
2928
2929 anyhow::Ok((workspace, items))
2930 })
2931 })
2932 .await
2933 }
2934 })
2935}
2936
2937pub fn open_new(
2938 app_state: &Arc<AppState>,
2939 cx: &mut AppContext,
2940 init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static,
2941) -> Task<()> {
2942 let task = Workspace::new_local(Vec::new(), app_state.clone(), None, cx);
2943 cx.spawn(|mut cx| async move {
2944 let (workspace, opened_paths) = task.await;
2945
2946 workspace
2947 .update(&mut cx, |workspace, cx| {
2948 if opened_paths.is_empty() {
2949 init(workspace, cx)
2950 }
2951 })
2952 .log_err();
2953 })
2954}
2955
2956pub fn join_remote_project(
2957 project_id: u64,
2958 follow_user_id: u64,
2959 app_state: Arc<AppState>,
2960 cx: &mut AppContext,
2961) -> Task<Result<()>> {
2962 cx.spawn(|mut cx| async move {
2963 let existing_workspace = cx.update(|cx| {
2964 cx.window_ids()
2965 .filter_map(|window_id| cx.root_view(window_id)?.clone().downcast::<Workspace>())
2966 .find(|workspace| {
2967 workspace.read(cx).project().read(cx).remote_id() == Some(project_id)
2968 })
2969 });
2970
2971 let workspace = if let Some(existing_workspace) = existing_workspace {
2972 existing_workspace.downgrade()
2973 } else {
2974 let active_call = cx.read(ActiveCall::global);
2975 let room = active_call
2976 .read_with(&cx, |call, _| call.room().cloned())
2977 .ok_or_else(|| anyhow!("not in a call"))?;
2978 let project = room
2979 .update(&mut cx, |room, cx| {
2980 room.join_project(
2981 project_id,
2982 app_state.languages.clone(),
2983 app_state.fs.clone(),
2984 cx,
2985 )
2986 })
2987 .await?;
2988
2989 let (_, workspace) = cx.add_window(
2990 (app_state.build_window_options)(None, None, cx.platform().as_ref()),
2991 |cx| {
2992 let mut workspace =
2993 Workspace::new(Default::default(), 0, project, app_state.clone(), cx);
2994 (app_state.initialize_workspace)(&mut workspace, &app_state, cx);
2995 workspace
2996 },
2997 );
2998 workspace.downgrade()
2999 };
3000
3001 cx.activate_window(workspace.window_id());
3002 cx.platform().activate(true);
3003
3004 workspace.update(&mut cx, |workspace, cx| {
3005 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
3006 let follow_peer_id = room
3007 .read(cx)
3008 .remote_participants()
3009 .iter()
3010 .find(|(_, participant)| participant.user.id == follow_user_id)
3011 .map(|(_, p)| p.peer_id)
3012 .or_else(|| {
3013 // If we couldn't follow the given user, follow the host instead.
3014 let collaborator = workspace
3015 .project()
3016 .read(cx)
3017 .collaborators()
3018 .values()
3019 .find(|collaborator| collaborator.replica_id == 0)?;
3020 Some(collaborator.peer_id)
3021 });
3022
3023 if let Some(follow_peer_id) = follow_peer_id {
3024 if !workspace.is_being_followed(follow_peer_id) {
3025 workspace
3026 .toggle_follow(follow_peer_id, cx)
3027 .map(|follow| follow.detach_and_log_err(cx));
3028 }
3029 }
3030 }
3031 })?;
3032
3033 anyhow::Ok(())
3034 })
3035}
3036
3037pub fn restart(_: &Restart, cx: &mut AppContext) {
3038 let mut workspaces = cx
3039 .window_ids()
3040 .filter_map(|window_id| {
3041 Some(
3042 cx.root_view(window_id)?
3043 .clone()
3044 .downcast::<Workspace>()?
3045 .downgrade(),
3046 )
3047 })
3048 .collect::<Vec<_>>();
3049
3050 // If multiple windows have unsaved changes, and need a save prompt,
3051 // prompt in the active window before switching to a different window.
3052 workspaces.sort_by_key(|workspace| !cx.window_is_active(workspace.window_id()));
3053
3054 let should_confirm = cx.global::<Settings>().confirm_quit;
3055 cx.spawn(|mut cx| async move {
3056 if let (true, Some(workspace)) = (should_confirm, workspaces.first()) {
3057 let answer = cx.prompt(
3058 workspace.window_id(),
3059 PromptLevel::Info,
3060 "Are you sure you want to restart?",
3061 &["Restart", "Cancel"],
3062 );
3063
3064 if let Some(mut answer) = answer {
3065 let answer = answer.next().await;
3066 if answer != Some(0) {
3067 return Ok(());
3068 }
3069 }
3070 }
3071
3072 // If the user cancels any save prompt, then keep the app open.
3073 for workspace in workspaces {
3074 if !workspace
3075 .update(&mut cx, |workspace, cx| {
3076 workspace.prepare_to_close(true, cx)
3077 })?
3078 .await?
3079 {
3080 return Ok(());
3081 }
3082 }
3083 cx.platform().restart();
3084 anyhow::Ok(())
3085 })
3086 .detach_and_log_err(cx);
3087}
3088
3089fn parse_pixel_position_env_var(value: &str) -> Option<Vector2F> {
3090 let mut parts = value.split(',');
3091 let width: usize = parts.next()?.parse().ok()?;
3092 let height: usize = parts.next()?.parse().ok()?;
3093 Some(vec2f(width as f32, height as f32))
3094}
3095
3096#[cfg(test)]
3097mod tests {
3098 use std::{cell::RefCell, rc::Rc};
3099
3100 use crate::item::test::{TestItem, TestItemEvent, TestProjectItem};
3101
3102 use super::*;
3103 use fs::FakeFs;
3104 use gpui::{executor::Deterministic, TestAppContext};
3105 use project::{Project, ProjectEntryId};
3106 use serde_json::json;
3107
3108 #[gpui::test]
3109 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
3110 cx.foreground().forbid_parking();
3111 Settings::test_async(cx);
3112
3113 let fs = FakeFs::new(cx.background());
3114 let project = Project::test(fs, [], cx).await;
3115 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
3116
3117 // Adding an item with no ambiguity renders the tab without detail.
3118 let item1 = cx.add_view(&workspace, |_| {
3119 let mut item = TestItem::new();
3120 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
3121 item
3122 });
3123 workspace.update(cx, |workspace, cx| {
3124 workspace.add_item(Box::new(item1.clone()), cx);
3125 });
3126 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), None));
3127
3128 // Adding an item that creates ambiguity increases the level of detail on
3129 // both tabs.
3130 let item2 = cx.add_view(&workspace, |_| {
3131 let mut item = TestItem::new();
3132 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
3133 item
3134 });
3135 workspace.update(cx, |workspace, cx| {
3136 workspace.add_item(Box::new(item2.clone()), cx);
3137 });
3138 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
3139 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
3140
3141 // Adding an item that creates ambiguity increases the level of detail only
3142 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
3143 // we stop at the highest detail available.
3144 let item3 = cx.add_view(&workspace, |_| {
3145 let mut item = TestItem::new();
3146 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
3147 item
3148 });
3149 workspace.update(cx, |workspace, cx| {
3150 workspace.add_item(Box::new(item3.clone()), cx);
3151 });
3152 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
3153 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
3154 item3.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
3155 }
3156
3157 #[gpui::test]
3158 async fn test_tracking_active_path(cx: &mut TestAppContext) {
3159 cx.foreground().forbid_parking();
3160 Settings::test_async(cx);
3161 let fs = FakeFs::new(cx.background());
3162 fs.insert_tree(
3163 "/root1",
3164 json!({
3165 "one.txt": "",
3166 "two.txt": "",
3167 }),
3168 )
3169 .await;
3170 fs.insert_tree(
3171 "/root2",
3172 json!({
3173 "three.txt": "",
3174 }),
3175 )
3176 .await;
3177
3178 let project = Project::test(fs, ["root1".as_ref()], cx).await;
3179 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
3180 let worktree_id = project.read_with(cx, |project, cx| {
3181 project.worktrees(cx).next().unwrap().read(cx).id()
3182 });
3183
3184 let item1 = cx.add_view(&workspace, |cx| {
3185 TestItem::new().with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
3186 });
3187 let item2 = cx.add_view(&workspace, |cx| {
3188 TestItem::new().with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
3189 });
3190
3191 // Add an item to an empty pane
3192 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx));
3193 project.read_with(cx, |project, cx| {
3194 assert_eq!(
3195 project.active_entry(),
3196 project
3197 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
3198 .map(|e| e.id)
3199 );
3200 });
3201 assert_eq!(
3202 cx.current_window_title(window_id).as_deref(),
3203 Some("one.txt — root1")
3204 );
3205
3206 // Add a second item to a non-empty pane
3207 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
3208 assert_eq!(
3209 cx.current_window_title(window_id).as_deref(),
3210 Some("two.txt — root1")
3211 );
3212 project.read_with(cx, |project, cx| {
3213 assert_eq!(
3214 project.active_entry(),
3215 project
3216 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
3217 .map(|e| e.id)
3218 );
3219 });
3220
3221 // Close the active item
3222 workspace
3223 .update(cx, |workspace, cx| {
3224 Pane::close_active_item(workspace, &Default::default(), cx).unwrap()
3225 })
3226 .await
3227 .unwrap();
3228 assert_eq!(
3229 cx.current_window_title(window_id).as_deref(),
3230 Some("one.txt — root1")
3231 );
3232 project.read_with(cx, |project, cx| {
3233 assert_eq!(
3234 project.active_entry(),
3235 project
3236 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
3237 .map(|e| e.id)
3238 );
3239 });
3240
3241 // Add a project folder
3242 project
3243 .update(cx, |project, cx| {
3244 project.find_or_create_local_worktree("/root2", true, cx)
3245 })
3246 .await
3247 .unwrap();
3248 assert_eq!(
3249 cx.current_window_title(window_id).as_deref(),
3250 Some("one.txt — root1, root2")
3251 );
3252
3253 // Remove a project folder
3254 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
3255 assert_eq!(
3256 cx.current_window_title(window_id).as_deref(),
3257 Some("one.txt — root2")
3258 );
3259 }
3260
3261 #[gpui::test]
3262 async fn test_close_window(cx: &mut TestAppContext) {
3263 cx.foreground().forbid_parking();
3264 Settings::test_async(cx);
3265 let fs = FakeFs::new(cx.background());
3266 fs.insert_tree("/root", json!({ "one": "" })).await;
3267
3268 let project = Project::test(fs, ["root".as_ref()], cx).await;
3269 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
3270
3271 // When there are no dirty items, there's nothing to do.
3272 let item1 = cx.add_view(&workspace, |_| TestItem::new());
3273 workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
3274 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
3275 assert!(task.await.unwrap());
3276
3277 // When there are dirty untitled items, prompt to save each one. If the user
3278 // cancels any prompt, then abort.
3279 let item2 = cx.add_view(&workspace, |_| TestItem::new().with_dirty(true));
3280 let item3 = cx.add_view(&workspace, |cx| {
3281 TestItem::new()
3282 .with_dirty(true)
3283 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3284 });
3285 workspace.update(cx, |w, cx| {
3286 w.add_item(Box::new(item2.clone()), cx);
3287 w.add_item(Box::new(item3.clone()), cx);
3288 });
3289 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
3290 cx.foreground().run_until_parked();
3291 cx.simulate_prompt_answer(window_id, 2 /* cancel */);
3292 cx.foreground().run_until_parked();
3293 assert!(!cx.has_pending_prompt(window_id));
3294 assert!(!task.await.unwrap());
3295 }
3296
3297 #[gpui::test]
3298 async fn test_close_pane_items(cx: &mut TestAppContext) {
3299 cx.foreground().forbid_parking();
3300 Settings::test_async(cx);
3301 let fs = FakeFs::new(cx.background());
3302
3303 let project = Project::test(fs, None, cx).await;
3304 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
3305
3306 let item1 = cx.add_view(&workspace, |cx| {
3307 TestItem::new()
3308 .with_dirty(true)
3309 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3310 });
3311 let item2 = cx.add_view(&workspace, |cx| {
3312 TestItem::new()
3313 .with_dirty(true)
3314 .with_conflict(true)
3315 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
3316 });
3317 let item3 = cx.add_view(&workspace, |cx| {
3318 TestItem::new()
3319 .with_dirty(true)
3320 .with_conflict(true)
3321 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
3322 });
3323 let item4 = cx.add_view(&workspace, |cx| {
3324 TestItem::new()
3325 .with_dirty(true)
3326 .with_project_items(&[TestProjectItem::new_untitled(cx)])
3327 });
3328 let pane = workspace.update(cx, |workspace, cx| {
3329 workspace.add_item(Box::new(item1.clone()), cx);
3330 workspace.add_item(Box::new(item2.clone()), cx);
3331 workspace.add_item(Box::new(item3.clone()), cx);
3332 workspace.add_item(Box::new(item4.clone()), cx);
3333 workspace.active_pane().clone()
3334 });
3335
3336 let close_items = workspace.update(cx, |workspace, cx| {
3337 pane.update(cx, |pane, cx| {
3338 pane.activate_item(1, true, true, cx);
3339 assert_eq!(pane.active_item().unwrap().id(), item2.id());
3340 });
3341
3342 let item1_id = item1.id();
3343 let item3_id = item3.id();
3344 let item4_id = item4.id();
3345 Pane::close_items(workspace, pane.clone(), cx, move |id| {
3346 [item1_id, item3_id, item4_id].contains(&id)
3347 })
3348 });
3349 cx.foreground().run_until_parked();
3350
3351 // There's a prompt to save item 1.
3352 pane.read_with(cx, |pane, _| {
3353 assert_eq!(pane.items_len(), 4);
3354 assert_eq!(pane.active_item().unwrap().id(), item1.id());
3355 });
3356 assert!(cx.has_pending_prompt(window_id));
3357
3358 // Confirm saving item 1.
3359 cx.simulate_prompt_answer(window_id, 0);
3360 cx.foreground().run_until_parked();
3361
3362 // Item 1 is saved. There's a prompt to save item 3.
3363 pane.read_with(cx, |pane, cx| {
3364 assert_eq!(item1.read(cx).save_count, 1);
3365 assert_eq!(item1.read(cx).save_as_count, 0);
3366 assert_eq!(item1.read(cx).reload_count, 0);
3367 assert_eq!(pane.items_len(), 3);
3368 assert_eq!(pane.active_item().unwrap().id(), item3.id());
3369 });
3370 assert!(cx.has_pending_prompt(window_id));
3371
3372 // Cancel saving item 3.
3373 cx.simulate_prompt_answer(window_id, 1);
3374 cx.foreground().run_until_parked();
3375
3376 // Item 3 is reloaded. There's a prompt to save item 4.
3377 pane.read_with(cx, |pane, cx| {
3378 assert_eq!(item3.read(cx).save_count, 0);
3379 assert_eq!(item3.read(cx).save_as_count, 0);
3380 assert_eq!(item3.read(cx).reload_count, 1);
3381 assert_eq!(pane.items_len(), 2);
3382 assert_eq!(pane.active_item().unwrap().id(), item4.id());
3383 });
3384 assert!(cx.has_pending_prompt(window_id));
3385
3386 // Confirm saving item 4.
3387 cx.simulate_prompt_answer(window_id, 0);
3388 cx.foreground().run_until_parked();
3389
3390 // There's a prompt for a path for item 4.
3391 cx.simulate_new_path_selection(|_| Some(Default::default()));
3392 close_items.await.unwrap();
3393
3394 // The requested items are closed.
3395 pane.read_with(cx, |pane, cx| {
3396 assert_eq!(item4.read(cx).save_count, 0);
3397 assert_eq!(item4.read(cx).save_as_count, 1);
3398 assert_eq!(item4.read(cx).reload_count, 0);
3399 assert_eq!(pane.items_len(), 1);
3400 assert_eq!(pane.active_item().unwrap().id(), item2.id());
3401 });
3402 }
3403
3404 #[gpui::test]
3405 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
3406 cx.foreground().forbid_parking();
3407 Settings::test_async(cx);
3408 let fs = FakeFs::new(cx.background());
3409
3410 let project = Project::test(fs, [], cx).await;
3411 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
3412
3413 // Create several workspace items with single project entries, and two
3414 // workspace items with multiple project entries.
3415 let single_entry_items = (0..=4)
3416 .map(|project_entry_id| {
3417 cx.add_view(&workspace, |cx| {
3418 TestItem::new()
3419 .with_dirty(true)
3420 .with_project_items(&[TestProjectItem::new(
3421 project_entry_id,
3422 &format!("{project_entry_id}.txt"),
3423 cx,
3424 )])
3425 })
3426 })
3427 .collect::<Vec<_>>();
3428 let item_2_3 = cx.add_view(&workspace, |cx| {
3429 TestItem::new()
3430 .with_dirty(true)
3431 .with_singleton(false)
3432 .with_project_items(&[
3433 single_entry_items[2].read(cx).project_items[0].clone(),
3434 single_entry_items[3].read(cx).project_items[0].clone(),
3435 ])
3436 });
3437 let item_3_4 = cx.add_view(&workspace, |cx| {
3438 TestItem::new()
3439 .with_dirty(true)
3440 .with_singleton(false)
3441 .with_project_items(&[
3442 single_entry_items[3].read(cx).project_items[0].clone(),
3443 single_entry_items[4].read(cx).project_items[0].clone(),
3444 ])
3445 });
3446
3447 // Create two panes that contain the following project entries:
3448 // left pane:
3449 // multi-entry items: (2, 3)
3450 // single-entry items: 0, 1, 2, 3, 4
3451 // right pane:
3452 // single-entry items: 1
3453 // multi-entry items: (3, 4)
3454 let left_pane = workspace.update(cx, |workspace, cx| {
3455 let left_pane = workspace.active_pane().clone();
3456 workspace.add_item(Box::new(item_2_3.clone()), cx);
3457 for item in single_entry_items {
3458 workspace.add_item(Box::new(item), cx);
3459 }
3460 left_pane.update(cx, |pane, cx| {
3461 pane.activate_item(2, true, true, cx);
3462 });
3463
3464 workspace
3465 .split_pane(left_pane.clone(), SplitDirection::Right, cx)
3466 .unwrap();
3467
3468 left_pane
3469 });
3470
3471 //Need to cause an effect flush in order to respect new focus
3472 workspace.update(cx, |workspace, cx| {
3473 workspace.add_item(Box::new(item_3_4.clone()), cx);
3474 cx.focus(&left_pane);
3475 });
3476
3477 // When closing all of the items in the left pane, we should be prompted twice:
3478 // once for project entry 0, and once for project entry 2. After those two
3479 // prompts, the task should complete.
3480
3481 let close = workspace.update(cx, |workspace, cx| {
3482 Pane::close_items(workspace, left_pane.clone(), cx, |_| true)
3483 });
3484
3485 cx.foreground().run_until_parked();
3486 left_pane.read_with(cx, |pane, cx| {
3487 assert_eq!(
3488 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
3489 &[ProjectEntryId::from_proto(0)]
3490 );
3491 });
3492 cx.simulate_prompt_answer(window_id, 0);
3493
3494 cx.foreground().run_until_parked();
3495 left_pane.read_with(cx, |pane, cx| {
3496 assert_eq!(
3497 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
3498 &[ProjectEntryId::from_proto(2)]
3499 );
3500 });
3501 cx.simulate_prompt_answer(window_id, 0);
3502
3503 cx.foreground().run_until_parked();
3504 close.await.unwrap();
3505 left_pane.read_with(cx, |pane, _| {
3506 assert_eq!(pane.items_len(), 0);
3507 });
3508 }
3509
3510 #[gpui::test]
3511 async fn test_autosave(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
3512 deterministic.forbid_parking();
3513
3514 Settings::test_async(cx);
3515 let fs = FakeFs::new(cx.background());
3516
3517 let project = Project::test(fs, [], cx).await;
3518 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
3519
3520 let item = cx.add_view(&workspace, |cx| {
3521 TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3522 });
3523 let item_id = item.id();
3524 workspace.update(cx, |workspace, cx| {
3525 workspace.add_item(Box::new(item.clone()), cx);
3526 });
3527
3528 // Autosave on window change.
3529 item.update(cx, |item, cx| {
3530 cx.update_global(|settings: &mut Settings, _| {
3531 settings.autosave = Autosave::OnWindowChange;
3532 });
3533 item.is_dirty = true;
3534 });
3535
3536 // Deactivating the window saves the file.
3537 cx.simulate_window_activation(None);
3538 deterministic.run_until_parked();
3539 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
3540
3541 // Autosave on focus change.
3542 item.update(cx, |item, cx| {
3543 cx.focus_self();
3544 cx.update_global(|settings: &mut Settings, _| {
3545 settings.autosave = Autosave::OnFocusChange;
3546 });
3547 item.is_dirty = true;
3548 });
3549
3550 // Blurring the item saves the file.
3551 item.update(cx, |_, cx| cx.blur());
3552 deterministic.run_until_parked();
3553 item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
3554
3555 // Deactivating the window still saves the file.
3556 cx.simulate_window_activation(Some(window_id));
3557 item.update(cx, |item, cx| {
3558 cx.focus_self();
3559 item.is_dirty = true;
3560 });
3561 cx.simulate_window_activation(None);
3562
3563 deterministic.run_until_parked();
3564 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
3565
3566 // Autosave after delay.
3567 item.update(cx, |item, cx| {
3568 cx.update_global(|settings: &mut Settings, _| {
3569 settings.autosave = Autosave::AfterDelay { milliseconds: 500 };
3570 });
3571 item.is_dirty = true;
3572 cx.emit(TestItemEvent::Edit);
3573 });
3574
3575 // Delay hasn't fully expired, so the file is still dirty and unsaved.
3576 deterministic.advance_clock(Duration::from_millis(250));
3577 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
3578
3579 // After delay expires, the file is saved.
3580 deterministic.advance_clock(Duration::from_millis(250));
3581 item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
3582
3583 // Autosave on focus change, ensuring closing the tab counts as such.
3584 item.update(cx, |item, cx| {
3585 cx.update_global(|settings: &mut Settings, _| {
3586 settings.autosave = Autosave::OnFocusChange;
3587 });
3588 item.is_dirty = true;
3589 });
3590
3591 workspace
3592 .update(cx, |workspace, cx| {
3593 let pane = workspace.active_pane().clone();
3594 Pane::close_items(workspace, pane, cx, move |id| id == item_id)
3595 })
3596 .await
3597 .unwrap();
3598 assert!(!cx.has_pending_prompt(window_id));
3599 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
3600
3601 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
3602 workspace.update(cx, |workspace, cx| {
3603 workspace.add_item(Box::new(item.clone()), cx);
3604 });
3605 item.update(cx, |item, cx| {
3606 item.project_items[0].update(cx, |item, _| {
3607 item.entry_id = None;
3608 });
3609 item.is_dirty = true;
3610 cx.blur();
3611 });
3612 deterministic.run_until_parked();
3613 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
3614
3615 // Ensure autosave is prevented for deleted files also when closing the buffer.
3616 let _close_items = workspace.update(cx, |workspace, cx| {
3617 let pane = workspace.active_pane().clone();
3618 Pane::close_items(workspace, pane, cx, move |id| id == item_id)
3619 });
3620 deterministic.run_until_parked();
3621 assert!(cx.has_pending_prompt(window_id));
3622 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
3623 }
3624
3625 #[gpui::test]
3626 async fn test_pane_navigation(
3627 deterministic: Arc<Deterministic>,
3628 cx: &mut gpui::TestAppContext,
3629 ) {
3630 deterministic.forbid_parking();
3631 Settings::test_async(cx);
3632 let fs = FakeFs::new(cx.background());
3633
3634 let project = Project::test(fs, [], cx).await;
3635 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
3636
3637 let item = cx.add_view(&workspace, |cx| {
3638 TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3639 });
3640 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3641 let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone());
3642 let toolbar_notify_count = Rc::new(RefCell::new(0));
3643
3644 workspace.update(cx, |workspace, cx| {
3645 workspace.add_item(Box::new(item.clone()), cx);
3646 let toolbar_notification_count = toolbar_notify_count.clone();
3647 cx.observe(&toolbar, move |_, _, _| {
3648 *toolbar_notification_count.borrow_mut() += 1
3649 })
3650 .detach();
3651 });
3652
3653 pane.read_with(cx, |pane, _| {
3654 assert!(!pane.can_navigate_backward());
3655 assert!(!pane.can_navigate_forward());
3656 });
3657
3658 item.update(cx, |item, cx| {
3659 item.set_state("one".to_string(), cx);
3660 });
3661
3662 // Toolbar must be notified to re-render the navigation buttons
3663 assert_eq!(*toolbar_notify_count.borrow(), 1);
3664
3665 pane.read_with(cx, |pane, _| {
3666 assert!(pane.can_navigate_backward());
3667 assert!(!pane.can_navigate_forward());
3668 });
3669
3670 workspace
3671 .update(cx, |workspace, cx| {
3672 Pane::go_back(workspace, Some(pane.downgrade()), cx)
3673 })
3674 .await
3675 .unwrap();
3676
3677 assert_eq!(*toolbar_notify_count.borrow(), 3);
3678 pane.read_with(cx, |pane, _| {
3679 assert!(!pane.can_navigate_backward());
3680 assert!(pane.can_navigate_forward());
3681 });
3682 }
3683}