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