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 pane.update(cx, |pane, cx| pane.activate_item(ix, true, true, cx));
1558 true
1559 } else {
1560 false
1561 }
1562 }
1563
1564 fn activate_pane_at_index(&mut self, action: &ActivatePane, cx: &mut ViewContext<Self>) {
1565 let panes = self.center.panes();
1566 if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
1567 cx.focus(&pane);
1568 } else {
1569 self.split_pane(self.active_pane.clone(), SplitDirection::Right, cx);
1570 }
1571 }
1572
1573 pub fn activate_next_pane(&mut self, cx: &mut ViewContext<Self>) {
1574 let panes = self.center.panes();
1575 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
1576 let next_ix = (ix + 1) % panes.len();
1577 let next_pane = panes[next_ix].clone();
1578 cx.focus(&next_pane);
1579 }
1580 }
1581
1582 pub fn activate_previous_pane(&mut self, cx: &mut ViewContext<Self>) {
1583 let panes = self.center.panes();
1584 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
1585 let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
1586 let prev_pane = panes[prev_ix].clone();
1587 cx.focus(&prev_pane);
1588 }
1589 }
1590
1591 fn handle_pane_focused(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
1592 if self.active_pane != pane {
1593 self.active_pane
1594 .update(cx, |pane, cx| pane.set_active(false, cx));
1595 self.active_pane = pane.clone();
1596 self.active_pane
1597 .update(cx, |pane, cx| pane.set_active(true, cx));
1598 self.status_bar.update(cx, |status_bar, cx| {
1599 status_bar.set_active_pane(&self.active_pane, cx);
1600 });
1601 self.active_item_path_changed(cx);
1602
1603 if &pane == self.dock_pane() {
1604 Dock::show(self, true, cx);
1605 } else {
1606 self.last_active_center_pane = Some(pane.downgrade());
1607 if self.dock.is_anchored_at(DockAnchor::Expanded) {
1608 Dock::hide(self, cx);
1609 }
1610 }
1611 cx.notify();
1612 }
1613
1614 self.update_followers(
1615 proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView {
1616 id: self.active_item(cx).and_then(|item| {
1617 item.to_followable_item_handle(cx)?
1618 .remote_id(&self.app_state.client, cx)
1619 .map(|id| id.to_proto())
1620 }),
1621 leader_id: self.leader_for_pane(&pane),
1622 }),
1623 cx,
1624 );
1625 }
1626
1627 fn handle_pane_event(
1628 &mut self,
1629 pane: ViewHandle<Pane>,
1630 event: &pane::Event,
1631 cx: &mut ViewContext<Self>,
1632 ) {
1633 let is_dock = &pane == self.dock.pane();
1634 match event {
1635 pane::Event::Split(direction) if !is_dock => {
1636 self.split_pane(pane, *direction, cx);
1637 }
1638 pane::Event::Remove if !is_dock => self.remove_pane(pane, cx),
1639 pane::Event::Remove if is_dock => Dock::hide(self, cx),
1640 pane::Event::ActivateItem { local } => {
1641 if *local {
1642 self.unfollow(&pane, cx);
1643 }
1644 if &pane == self.active_pane() {
1645 self.active_item_path_changed(cx);
1646 }
1647 }
1648 pane::Event::ChangeItemTitle => {
1649 if pane == self.active_pane {
1650 self.active_item_path_changed(cx);
1651 }
1652 self.update_window_edited(cx);
1653 }
1654 pane::Event::RemoveItem { item_id } => {
1655 self.update_window_edited(cx);
1656 if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) {
1657 if entry.get().id() == pane.id() {
1658 entry.remove();
1659 }
1660 }
1661 }
1662 pane::Event::Focus => {
1663 self.handle_pane_focused(pane.clone(), cx);
1664 }
1665 _ => {}
1666 }
1667
1668 self.serialize_workspace(cx);
1669 }
1670
1671 pub fn split_pane(
1672 &mut self,
1673 pane: ViewHandle<Pane>,
1674 direction: SplitDirection,
1675 cx: &mut ViewContext<Self>,
1676 ) -> Option<ViewHandle<Pane>> {
1677 if &pane == self.dock_pane() {
1678 warn!("Can't split dock pane.");
1679 return None;
1680 }
1681
1682 let item = pane.read(cx).active_item()?;
1683 let maybe_pane_handle = if let Some(clone) = item.clone_on_split(self.database_id(), cx) {
1684 let new_pane = self.add_pane(cx);
1685 Pane::add_item(self, &new_pane, clone, true, true, None, cx);
1686 self.center.split(&pane, &new_pane, direction).unwrap();
1687 Some(new_pane)
1688 } else {
1689 None
1690 };
1691 cx.notify();
1692 maybe_pane_handle
1693 }
1694
1695 pub fn split_pane_with_item(
1696 &mut self,
1697 pane_to_split: WeakViewHandle<Pane>,
1698 split_direction: SplitDirection,
1699 from: WeakViewHandle<Pane>,
1700 item_id_to_move: usize,
1701 cx: &mut ViewContext<Self>,
1702 ) {
1703 let Some(pane_to_split) = pane_to_split.upgrade(cx) else { return; };
1704 let Some(from) = from.upgrade(cx) else { return; };
1705 if &pane_to_split == self.dock_pane() {
1706 warn!("Can't split dock pane.");
1707 return;
1708 }
1709
1710 let new_pane = self.add_pane(cx);
1711 Pane::move_item(self, from.clone(), new_pane.clone(), item_id_to_move, 0, cx);
1712 self.center
1713 .split(&pane_to_split, &new_pane, split_direction)
1714 .unwrap();
1715 cx.notify();
1716 }
1717
1718 pub fn split_pane_with_project_entry(
1719 &mut self,
1720 pane_to_split: WeakViewHandle<Pane>,
1721 split_direction: SplitDirection,
1722 project_entry: ProjectEntryId,
1723 cx: &mut ViewContext<Self>,
1724 ) -> Option<Task<Result<()>>> {
1725 let pane_to_split = pane_to_split.upgrade(cx)?;
1726 if &pane_to_split == self.dock_pane() {
1727 warn!("Can't split dock pane.");
1728 return None;
1729 }
1730
1731 let new_pane = self.add_pane(cx);
1732 self.center
1733 .split(&pane_to_split, &new_pane, split_direction)
1734 .unwrap();
1735
1736 let path = self.project.read(cx).path_for_entry(project_entry, cx)?;
1737 let task = self.open_path(path, Some(new_pane.downgrade()), true, cx);
1738 Some(cx.foreground().spawn(async move {
1739 task.await?;
1740 Ok(())
1741 }))
1742 }
1743
1744 fn remove_pane(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
1745 if self.center.remove(&pane).unwrap() {
1746 self.panes.retain(|p| p != &pane);
1747 cx.focus(self.panes.last().unwrap());
1748 self.unfollow(&pane, cx);
1749 self.last_leaders_by_pane.remove(&pane.downgrade());
1750 for removed_item in pane.read(cx).items() {
1751 self.panes_by_item.remove(&removed_item.id());
1752 }
1753 if self.last_active_center_pane == Some(pane.downgrade()) {
1754 self.last_active_center_pane = None;
1755 }
1756
1757 cx.notify();
1758 } else {
1759 self.active_item_path_changed(cx);
1760 }
1761 }
1762
1763 pub fn panes(&self) -> &[ViewHandle<Pane>] {
1764 &self.panes
1765 }
1766
1767 pub fn active_pane(&self) -> &ViewHandle<Pane> {
1768 &self.active_pane
1769 }
1770
1771 pub fn dock_pane(&self) -> &ViewHandle<Pane> {
1772 self.dock.pane()
1773 }
1774
1775 fn dock_active(&self) -> bool {
1776 &self.active_pane == self.dock.pane()
1777 }
1778
1779 fn project_remote_id_changed(&mut self, remote_id: Option<u64>, cx: &mut ViewContext<Self>) {
1780 if let Some(remote_id) = remote_id {
1781 self.remote_entity_subscription = Some(
1782 self.app_state
1783 .client
1784 .add_view_for_remote_entity(remote_id, cx),
1785 );
1786 } else {
1787 self.remote_entity_subscription.take();
1788 }
1789 }
1790
1791 fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
1792 self.leader_state.followers.remove(&peer_id);
1793 if let Some(states_by_pane) = self.follower_states_by_leader.remove(&peer_id) {
1794 for state in states_by_pane.into_values() {
1795 for item in state.items_by_leader_view_id.into_values() {
1796 item.set_leader_replica_id(None, cx);
1797 }
1798 }
1799 }
1800 cx.notify();
1801 }
1802
1803 pub fn toggle_follow(
1804 &mut self,
1805 leader_id: PeerId,
1806 cx: &mut ViewContext<Self>,
1807 ) -> Option<Task<Result<()>>> {
1808 let pane = self.active_pane().clone();
1809
1810 if let Some(prev_leader_id) = self.unfollow(&pane, cx) {
1811 if leader_id == prev_leader_id {
1812 return None;
1813 }
1814 }
1815
1816 self.last_leaders_by_pane
1817 .insert(pane.downgrade(), leader_id);
1818 self.follower_states_by_leader
1819 .entry(leader_id)
1820 .or_default()
1821 .insert(pane.clone(), Default::default());
1822 cx.notify();
1823
1824 let project_id = self.project.read(cx).remote_id()?;
1825 let request = self.app_state.client.request(proto::Follow {
1826 project_id,
1827 leader_id: Some(leader_id),
1828 });
1829
1830 Some(cx.spawn(|this, mut cx| async move {
1831 let response = request.await?;
1832 this.update(&mut cx, |this, _| {
1833 let state = this
1834 .follower_states_by_leader
1835 .get_mut(&leader_id)
1836 .and_then(|states_by_pane| states_by_pane.get_mut(&pane))
1837 .ok_or_else(|| anyhow!("following interrupted"))?;
1838 state.active_view_id = if let Some(active_view_id) = response.active_view_id {
1839 Some(ViewId::from_proto(active_view_id)?)
1840 } else {
1841 None
1842 };
1843 Ok::<_, anyhow::Error>(())
1844 })??;
1845 Self::add_views_from_leader(
1846 this.clone(),
1847 leader_id,
1848 vec![pane],
1849 response.views,
1850 &mut cx,
1851 )
1852 .await?;
1853 this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx))?;
1854 Ok(())
1855 }))
1856 }
1857
1858 pub fn follow_next_collaborator(
1859 &mut self,
1860 _: &FollowNextCollaborator,
1861 cx: &mut ViewContext<Self>,
1862 ) -> Option<Task<Result<()>>> {
1863 let collaborators = self.project.read(cx).collaborators();
1864 let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
1865 let mut collaborators = collaborators.keys().copied();
1866 for peer_id in collaborators.by_ref() {
1867 if peer_id == leader_id {
1868 break;
1869 }
1870 }
1871 collaborators.next()
1872 } else if let Some(last_leader_id) =
1873 self.last_leaders_by_pane.get(&self.active_pane.downgrade())
1874 {
1875 if collaborators.contains_key(last_leader_id) {
1876 Some(*last_leader_id)
1877 } else {
1878 None
1879 }
1880 } else {
1881 None
1882 };
1883
1884 next_leader_id
1885 .or_else(|| collaborators.keys().copied().next())
1886 .and_then(|leader_id| self.toggle_follow(leader_id, cx))
1887 }
1888
1889 pub fn unfollow(
1890 &mut self,
1891 pane: &ViewHandle<Pane>,
1892 cx: &mut ViewContext<Self>,
1893 ) -> Option<PeerId> {
1894 for (leader_id, states_by_pane) in &mut self.follower_states_by_leader {
1895 let leader_id = *leader_id;
1896 if let Some(state) = states_by_pane.remove(pane) {
1897 for (_, item) in state.items_by_leader_view_id {
1898 item.set_leader_replica_id(None, cx);
1899 }
1900
1901 if states_by_pane.is_empty() {
1902 self.follower_states_by_leader.remove(&leader_id);
1903 if let Some(project_id) = self.project.read(cx).remote_id() {
1904 self.app_state
1905 .client
1906 .send(proto::Unfollow {
1907 project_id,
1908 leader_id: Some(leader_id),
1909 })
1910 .log_err();
1911 }
1912 }
1913
1914 cx.notify();
1915 return Some(leader_id);
1916 }
1917 }
1918 None
1919 }
1920
1921 pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
1922 self.follower_states_by_leader.contains_key(&peer_id)
1923 }
1924
1925 pub fn is_followed_by(&self, peer_id: PeerId) -> bool {
1926 self.leader_state.followers.contains(&peer_id)
1927 }
1928
1929 fn render_titlebar(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
1930 // TODO: There should be a better system in place for this
1931 // (https://github.com/zed-industries/zed/issues/1290)
1932 let is_fullscreen = cx.window_is_fullscreen();
1933 let container_theme = if is_fullscreen {
1934 let mut container_theme = theme.workspace.titlebar.container;
1935 container_theme.padding.left = container_theme.padding.right;
1936 container_theme
1937 } else {
1938 theme.workspace.titlebar.container
1939 };
1940
1941 enum TitleBar {}
1942 MouseEventHandler::<TitleBar, _>::new(0, cx, |_, cx| {
1943 Stack::new()
1944 .with_children(
1945 self.titlebar_item
1946 .as_ref()
1947 .map(|item| ChildView::new(item, cx)),
1948 )
1949 .contained()
1950 .with_style(container_theme)
1951 })
1952 .on_click(MouseButton::Left, |event, _, cx| {
1953 if event.click_count == 2 {
1954 cx.zoom_window();
1955 }
1956 })
1957 .constrained()
1958 .with_height(theme.workspace.titlebar.height)
1959 .into_any_named("titlebar")
1960 }
1961
1962 fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
1963 let active_entry = self.active_project_path(cx);
1964 self.project
1965 .update(cx, |project, cx| project.set_active_path(active_entry, cx));
1966 self.update_window_title(cx);
1967 }
1968
1969 fn update_window_title(&mut self, cx: &mut ViewContext<Self>) {
1970 let project = self.project().read(cx);
1971 let mut title = String::new();
1972
1973 if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
1974 let filename = path
1975 .path
1976 .file_name()
1977 .map(|s| s.to_string_lossy())
1978 .or_else(|| {
1979 Some(Cow::Borrowed(
1980 project
1981 .worktree_for_id(path.worktree_id, cx)?
1982 .read(cx)
1983 .root_name(),
1984 ))
1985 });
1986
1987 if let Some(filename) = filename {
1988 title.push_str(filename.as_ref());
1989 title.push_str(" — ");
1990 }
1991 }
1992
1993 for (i, name) in project.worktree_root_names(cx).enumerate() {
1994 if i > 0 {
1995 title.push_str(", ");
1996 }
1997 title.push_str(name);
1998 }
1999
2000 if title.is_empty() {
2001 title = "empty project".to_string();
2002 }
2003
2004 if project.is_remote() {
2005 title.push_str(" ↙");
2006 } else if project.is_shared() {
2007 title.push_str(" ↗");
2008 }
2009
2010 cx.set_window_title(&title);
2011 }
2012
2013 fn update_window_edited(&mut self, cx: &mut ViewContext<Self>) {
2014 let is_edited = !self.project.read(cx).is_read_only()
2015 && self
2016 .items(cx)
2017 .any(|item| item.has_conflict(cx) || item.is_dirty(cx));
2018 if is_edited != self.window_edited {
2019 self.window_edited = is_edited;
2020 cx.set_window_edited(self.window_edited)
2021 }
2022 }
2023
2024 fn render_disconnected_overlay(
2025 &self,
2026 cx: &mut ViewContext<Workspace>,
2027 ) -> Option<AnyElement<Workspace>> {
2028 if self.project.read(cx).is_read_only() {
2029 enum DisconnectedOverlay {}
2030 Some(
2031 MouseEventHandler::<DisconnectedOverlay, _>::new(0, cx, |_, cx| {
2032 let theme = &cx.global::<Settings>().theme;
2033 Label::new(
2034 "Your connection to the remote project has been lost.",
2035 theme.workspace.disconnected_overlay.text.clone(),
2036 )
2037 .aligned()
2038 .contained()
2039 .with_style(theme.workspace.disconnected_overlay.container)
2040 })
2041 .with_cursor_style(CursorStyle::Arrow)
2042 .capture_all()
2043 .into_any_named("disconnected overlay"),
2044 )
2045 } else {
2046 None
2047 }
2048 }
2049
2050 fn render_notifications(
2051 &self,
2052 theme: &theme::Workspace,
2053 cx: &AppContext,
2054 ) -> Option<AnyElement<Workspace>> {
2055 if self.notifications.is_empty() {
2056 None
2057 } else {
2058 Some(
2059 Flex::column()
2060 .with_children(self.notifications.iter().map(|(_, _, notification)| {
2061 ChildView::new(notification.as_any(), cx)
2062 .contained()
2063 .with_style(theme.notification)
2064 }))
2065 .constrained()
2066 .with_width(theme.notifications.width)
2067 .contained()
2068 .with_style(theme.notifications.container)
2069 .aligned()
2070 .bottom()
2071 .right()
2072 .into_any(),
2073 )
2074 }
2075 }
2076
2077 // RPC handlers
2078
2079 async fn handle_follow(
2080 this: WeakViewHandle<Self>,
2081 envelope: TypedEnvelope<proto::Follow>,
2082 _: Arc<Client>,
2083 mut cx: AsyncAppContext,
2084 ) -> Result<proto::FollowResponse> {
2085 this.update(&mut cx, |this, cx| {
2086 let client = &this.app_state.client;
2087 this.leader_state
2088 .followers
2089 .insert(envelope.original_sender_id()?);
2090
2091 let active_view_id = this.active_item(cx).and_then(|i| {
2092 Some(
2093 i.to_followable_item_handle(cx)?
2094 .remote_id(client, cx)?
2095 .to_proto(),
2096 )
2097 });
2098
2099 cx.notify();
2100
2101 Ok(proto::FollowResponse {
2102 active_view_id,
2103 views: this
2104 .panes()
2105 .iter()
2106 .flat_map(|pane| {
2107 let leader_id = this.leader_for_pane(pane);
2108 pane.read(cx).items().filter_map({
2109 let cx = &cx;
2110 move |item| {
2111 let item = item.to_followable_item_handle(cx)?;
2112 let id = item.remote_id(client, cx)?.to_proto();
2113 let variant = item.to_state_proto(cx)?;
2114 Some(proto::View {
2115 id: Some(id),
2116 leader_id,
2117 variant: Some(variant),
2118 })
2119 }
2120 })
2121 })
2122 .collect(),
2123 })
2124 })?
2125 }
2126
2127 async fn handle_unfollow(
2128 this: WeakViewHandle<Self>,
2129 envelope: TypedEnvelope<proto::Unfollow>,
2130 _: Arc<Client>,
2131 mut cx: AsyncAppContext,
2132 ) -> Result<()> {
2133 this.update(&mut cx, |this, cx| {
2134 this.leader_state
2135 .followers
2136 .remove(&envelope.original_sender_id()?);
2137 cx.notify();
2138 Ok(())
2139 })?
2140 }
2141
2142 async fn handle_update_followers(
2143 this: WeakViewHandle<Self>,
2144 envelope: TypedEnvelope<proto::UpdateFollowers>,
2145 _: Arc<Client>,
2146 cx: AsyncAppContext,
2147 ) -> Result<()> {
2148 let leader_id = envelope.original_sender_id()?;
2149 this.read_with(&cx, |this, _| {
2150 this.leader_updates_tx
2151 .unbounded_send((leader_id, envelope.payload))
2152 })??;
2153 Ok(())
2154 }
2155
2156 async fn process_leader_update(
2157 this: &WeakViewHandle<Self>,
2158 leader_id: PeerId,
2159 update: proto::UpdateFollowers,
2160 cx: &mut AsyncAppContext,
2161 ) -> Result<()> {
2162 match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
2163 proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
2164 this.update(cx, |this, _| {
2165 if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
2166 for state in state.values_mut() {
2167 state.active_view_id =
2168 if let Some(active_view_id) = update_active_view.id.clone() {
2169 Some(ViewId::from_proto(active_view_id)?)
2170 } else {
2171 None
2172 };
2173 }
2174 }
2175 anyhow::Ok(())
2176 })??;
2177 }
2178 proto::update_followers::Variant::UpdateView(update_view) => {
2179 let variant = update_view
2180 .variant
2181 .ok_or_else(|| anyhow!("missing update view variant"))?;
2182 let id = update_view
2183 .id
2184 .ok_or_else(|| anyhow!("missing update view id"))?;
2185 let mut tasks = Vec::new();
2186 this.update(cx, |this, cx| {
2187 let project = this.project.clone();
2188 if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
2189 for state in state.values_mut() {
2190 let view_id = ViewId::from_proto(id.clone())?;
2191 if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
2192 tasks.push(item.apply_update_proto(&project, variant.clone(), cx));
2193 }
2194 }
2195 }
2196 anyhow::Ok(())
2197 })??;
2198 try_join_all(tasks).await.log_err();
2199 }
2200 proto::update_followers::Variant::CreateView(view) => {
2201 let panes = this.read_with(cx, |this, _| {
2202 this.follower_states_by_leader
2203 .get(&leader_id)
2204 .into_iter()
2205 .flat_map(|states_by_pane| states_by_pane.keys())
2206 .cloned()
2207 .collect()
2208 })?;
2209 Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], cx).await?;
2210 }
2211 }
2212 this.update(cx, |this, cx| this.leader_updated(leader_id, cx))?;
2213 Ok(())
2214 }
2215
2216 async fn add_views_from_leader(
2217 this: WeakViewHandle<Self>,
2218 leader_id: PeerId,
2219 panes: Vec<ViewHandle<Pane>>,
2220 views: Vec<proto::View>,
2221 cx: &mut AsyncAppContext,
2222 ) -> Result<()> {
2223 let project = this.read_with(cx, |this, _| this.project.clone())?;
2224 let replica_id = project
2225 .read_with(cx, |project, _| {
2226 project
2227 .collaborators()
2228 .get(&leader_id)
2229 .map(|c| c.replica_id)
2230 })
2231 .ok_or_else(|| anyhow!("no such collaborator {}", leader_id))?;
2232
2233 let item_builders = cx.update(|cx| {
2234 cx.default_global::<FollowableItemBuilders>()
2235 .values()
2236 .map(|b| b.0)
2237 .collect::<Vec<_>>()
2238 });
2239
2240 let mut item_tasks_by_pane = HashMap::default();
2241 for pane in panes {
2242 let mut item_tasks = Vec::new();
2243 let mut leader_view_ids = Vec::new();
2244 for view in &views {
2245 let Some(id) = &view.id else { continue };
2246 let id = ViewId::from_proto(id.clone())?;
2247 let mut variant = view.variant.clone();
2248 if variant.is_none() {
2249 Err(anyhow!("missing variant"))?;
2250 }
2251 for build_item in &item_builders {
2252 let task = cx.update(|cx| {
2253 build_item(pane.clone(), project.clone(), id, &mut variant, cx)
2254 });
2255 if let Some(task) = task {
2256 item_tasks.push(task);
2257 leader_view_ids.push(id);
2258 break;
2259 } else {
2260 assert!(variant.is_some());
2261 }
2262 }
2263 }
2264
2265 item_tasks_by_pane.insert(pane, (item_tasks, leader_view_ids));
2266 }
2267
2268 for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane {
2269 let items = futures::future::try_join_all(item_tasks).await?;
2270 this.update(cx, |this, cx| {
2271 let state = this
2272 .follower_states_by_leader
2273 .get_mut(&leader_id)?
2274 .get_mut(&pane)?;
2275
2276 for (id, item) in leader_view_ids.into_iter().zip(items) {
2277 item.set_leader_replica_id(Some(replica_id), cx);
2278 state.items_by_leader_view_id.insert(id, item);
2279 }
2280
2281 Some(())
2282 })?;
2283 }
2284 Ok(())
2285 }
2286
2287 fn update_followers(
2288 &self,
2289 update: proto::update_followers::Variant,
2290 cx: &AppContext,
2291 ) -> Option<()> {
2292 let project_id = self.project.read(cx).remote_id()?;
2293 if !self.leader_state.followers.is_empty() {
2294 self.app_state
2295 .client
2296 .send(proto::UpdateFollowers {
2297 project_id,
2298 follower_ids: self.leader_state.followers.iter().copied().collect(),
2299 variant: Some(update),
2300 })
2301 .log_err();
2302 }
2303 None
2304 }
2305
2306 pub fn leader_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<PeerId> {
2307 self.follower_states_by_leader
2308 .iter()
2309 .find_map(|(leader_id, state)| {
2310 if state.contains_key(pane) {
2311 Some(*leader_id)
2312 } else {
2313 None
2314 }
2315 })
2316 }
2317
2318 fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
2319 cx.notify();
2320
2321 let call = self.active_call()?;
2322 let room = call.read(cx).room()?.read(cx);
2323 let participant = room.remote_participant_for_peer_id(leader_id)?;
2324 let mut items_to_activate = Vec::new();
2325 match participant.location {
2326 call::ParticipantLocation::SharedProject { project_id } => {
2327 if Some(project_id) == self.project.read(cx).remote_id() {
2328 for (pane, state) in self.follower_states_by_leader.get(&leader_id)? {
2329 if let Some(item) = state
2330 .active_view_id
2331 .and_then(|id| state.items_by_leader_view_id.get(&id))
2332 {
2333 items_to_activate.push((pane.clone(), item.boxed_clone()));
2334 } else {
2335 if let Some(shared_screen) =
2336 self.shared_screen_for_peer(leader_id, pane, cx)
2337 {
2338 items_to_activate.push((pane.clone(), Box::new(shared_screen)));
2339 }
2340 }
2341 }
2342 }
2343 }
2344 call::ParticipantLocation::UnsharedProject => {}
2345 call::ParticipantLocation::External => {
2346 for (pane, _) in self.follower_states_by_leader.get(&leader_id)? {
2347 if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
2348 items_to_activate.push((pane.clone(), Box::new(shared_screen)));
2349 }
2350 }
2351 }
2352 }
2353
2354 for (pane, item) in items_to_activate {
2355 let pane_was_focused = pane.read(cx).has_focus();
2356 if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) {
2357 pane.update(cx, |pane, cx| pane.activate_item(index, false, false, cx));
2358 } else {
2359 Pane::add_item(self, &pane, item.boxed_clone(), false, false, None, cx);
2360 }
2361
2362 if pane_was_focused {
2363 pane.update(cx, |pane, cx| pane.focus_active_item(cx));
2364 }
2365 }
2366
2367 None
2368 }
2369
2370 fn shared_screen_for_peer(
2371 &self,
2372 peer_id: PeerId,
2373 pane: &ViewHandle<Pane>,
2374 cx: &mut ViewContext<Self>,
2375 ) -> Option<ViewHandle<SharedScreen>> {
2376 let call = self.active_call()?;
2377 let room = call.read(cx).room()?.read(cx);
2378 let participant = room.remote_participant_for_peer_id(peer_id)?;
2379 let track = participant.tracks.values().next()?.clone();
2380 let user = participant.user.clone();
2381
2382 for item in pane.read(cx).items_of_type::<SharedScreen>() {
2383 if item.read(cx).peer_id == peer_id {
2384 return Some(item);
2385 }
2386 }
2387
2388 Some(cx.add_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx)))
2389 }
2390
2391 pub fn on_window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
2392 if active {
2393 cx.background()
2394 .spawn(persistence::DB.update_timestamp(self.database_id()))
2395 .detach();
2396 } else {
2397 for pane in &self.panes {
2398 pane.update(cx, |pane, cx| {
2399 if let Some(item) = pane.active_item() {
2400 item.workspace_deactivated(cx);
2401 }
2402 if matches!(
2403 cx.global::<Settings>().autosave,
2404 Autosave::OnWindowChange | Autosave::OnFocusChange
2405 ) {
2406 for item in pane.items() {
2407 Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
2408 .detach_and_log_err(cx);
2409 }
2410 }
2411 });
2412 }
2413 }
2414 }
2415
2416 fn active_call(&self) -> Option<&ModelHandle<ActiveCall>> {
2417 self.active_call.as_ref().map(|(call, _)| call)
2418 }
2419
2420 fn on_active_call_event(
2421 &mut self,
2422 _: ModelHandle<ActiveCall>,
2423 event: &call::room::Event,
2424 cx: &mut ViewContext<Self>,
2425 ) {
2426 match event {
2427 call::room::Event::ParticipantLocationChanged { participant_id }
2428 | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
2429 self.leader_updated(*participant_id, cx);
2430 }
2431 _ => {}
2432 }
2433 }
2434
2435 pub fn database_id(&self) -> WorkspaceId {
2436 self.database_id
2437 }
2438
2439 fn location(&self, cx: &AppContext) -> Option<WorkspaceLocation> {
2440 let project = self.project().read(cx);
2441
2442 if project.is_local() {
2443 Some(
2444 project
2445 .visible_worktrees(cx)
2446 .map(|worktree| worktree.read(cx).abs_path())
2447 .collect::<Vec<_>>()
2448 .into(),
2449 )
2450 } else {
2451 None
2452 }
2453 }
2454
2455 fn remove_panes(&mut self, member: Member, cx: &mut ViewContext<Workspace>) {
2456 match member {
2457 Member::Axis(PaneAxis { members, .. }) => {
2458 for child in members.iter() {
2459 self.remove_panes(child.clone(), cx)
2460 }
2461 }
2462 Member::Pane(pane) => self.remove_pane(pane.clone(), cx),
2463 }
2464 }
2465
2466 fn serialize_workspace(&self, cx: &AppContext) {
2467 fn serialize_pane_handle(
2468 pane_handle: &ViewHandle<Pane>,
2469 cx: &AppContext,
2470 ) -> SerializedPane {
2471 let (items, active) = {
2472 let pane = pane_handle.read(cx);
2473 let active_item_id = pane.active_item().map(|item| item.id());
2474 (
2475 pane.items()
2476 .filter_map(|item_handle| {
2477 Some(SerializedItem {
2478 kind: Arc::from(item_handle.serialized_item_kind()?),
2479 item_id: item_handle.id(),
2480 active: Some(item_handle.id()) == active_item_id,
2481 })
2482 })
2483 .collect::<Vec<_>>(),
2484 pane.is_active(),
2485 )
2486 };
2487
2488 SerializedPane::new(items, active)
2489 }
2490
2491 fn build_serialized_pane_group(
2492 pane_group: &Member,
2493 cx: &AppContext,
2494 ) -> SerializedPaneGroup {
2495 match pane_group {
2496 Member::Axis(PaneAxis { axis, members }) => SerializedPaneGroup::Group {
2497 axis: *axis,
2498 children: members
2499 .iter()
2500 .map(|member| build_serialized_pane_group(member, cx))
2501 .collect::<Vec<_>>(),
2502 },
2503 Member::Pane(pane_handle) => {
2504 SerializedPaneGroup::Pane(serialize_pane_handle(&pane_handle, cx))
2505 }
2506 }
2507 }
2508
2509 if let Some(location) = self.location(cx) {
2510 // Load bearing special case:
2511 // - with_local_workspace() relies on this to not have other stuff open
2512 // when you open your log
2513 if !location.paths().is_empty() {
2514 let dock_pane = serialize_pane_handle(self.dock.pane(), cx);
2515 let center_group = build_serialized_pane_group(&self.center.root, cx);
2516
2517 let serialized_workspace = SerializedWorkspace {
2518 id: self.database_id,
2519 location,
2520 dock_position: self.dock.position(),
2521 dock_pane,
2522 center_group,
2523 left_sidebar_open: self.left_sidebar.read(cx).is_open(),
2524 bounds: Default::default(),
2525 display: Default::default(),
2526 };
2527
2528 cx.background()
2529 .spawn(persistence::DB.save_workspace(serialized_workspace))
2530 .detach();
2531 }
2532 }
2533 }
2534
2535 fn load_from_serialized_workspace(
2536 workspace: WeakViewHandle<Workspace>,
2537 serialized_workspace: SerializedWorkspace,
2538 cx: &mut AppContext,
2539 ) {
2540 cx.spawn(|mut cx| async move {
2541 let (project, dock_pane_handle, old_center_pane) =
2542 workspace.read_with(&cx, |workspace, _| {
2543 (
2544 workspace.project().clone(),
2545 workspace.dock_pane().downgrade(),
2546 workspace.last_active_center_pane.clone(),
2547 )
2548 })?;
2549
2550 serialized_workspace
2551 .dock_pane
2552 .deserialize_to(
2553 &project,
2554 &dock_pane_handle,
2555 serialized_workspace.id,
2556 &workspace,
2557 &mut cx,
2558 )
2559 .await?;
2560
2561 // Traverse the splits tree and add to things
2562 let center_group = serialized_workspace
2563 .center_group
2564 .deserialize(&project, serialized_workspace.id, &workspace, &mut cx)
2565 .await;
2566
2567 // Remove old panes from workspace panes list
2568 workspace.update(&mut cx, |workspace, cx| {
2569 if let Some((center_group, active_pane)) = center_group {
2570 workspace.remove_panes(workspace.center.root.clone(), cx);
2571
2572 // Swap workspace center group
2573 workspace.center = PaneGroup::with_root(center_group);
2574
2575 // Change the focus to the workspace first so that we retrigger focus in on the pane.
2576 cx.focus_self();
2577
2578 if let Some(active_pane) = active_pane {
2579 cx.focus(&active_pane);
2580 } else {
2581 cx.focus(workspace.panes.last().unwrap());
2582 }
2583 } else {
2584 let old_center_handle = old_center_pane.and_then(|weak| weak.upgrade(cx));
2585 if let Some(old_center_handle) = old_center_handle {
2586 cx.focus(&old_center_handle)
2587 } else {
2588 cx.focus_self()
2589 }
2590 }
2591
2592 if workspace.left_sidebar().read(cx).is_open()
2593 != serialized_workspace.left_sidebar_open
2594 {
2595 workspace.toggle_sidebar(SidebarSide::Left, cx);
2596 }
2597
2598 // Note that without after_window, the focus_self() and
2599 // the focus the dock generates start generating alternating
2600 // focus due to the deferred execution each triggering each other
2601 cx.after_window_update(move |workspace, cx| {
2602 Dock::set_dock_position(
2603 workspace,
2604 serialized_workspace.dock_position,
2605 true,
2606 cx,
2607 );
2608 });
2609
2610 cx.notify();
2611 })?;
2612
2613 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
2614 workspace.read_with(&cx, |workspace, cx| workspace.serialize_workspace(cx))?;
2615 anyhow::Ok(())
2616 })
2617 .detach_and_log_err(cx);
2618 }
2619
2620 #[cfg(any(test, feature = "test-support"))]
2621 pub fn test_new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
2622 let app_state = Arc::new(AppState {
2623 languages: project.read(cx).languages().clone(),
2624 themes: ThemeRegistry::new((), cx.font_cache().clone()),
2625 client: project.read(cx).client(),
2626 user_store: project.read(cx).user_store(),
2627 fs: project.read(cx).fs().clone(),
2628 build_window_options: |_, _, _| Default::default(),
2629 initialize_workspace: |_, _, _| {},
2630 dock_default_item_factory: |_, _| None,
2631 background_actions: || &[],
2632 });
2633 Self::new(None, 0, project, app_state, cx)
2634 }
2635}
2636
2637fn notify_if_database_failed(workspace: &WeakViewHandle<Workspace>, cx: &mut AsyncAppContext) {
2638 const REPORT_ISSUE_URL: &str ="https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";
2639
2640 workspace
2641 .update(cx, |workspace, cx| {
2642 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
2643 workspace.show_notification_once(0, cx, |cx| {
2644 cx.add_view(|_| {
2645 MessageNotification::new("Failed to load any database file.")
2646 .with_click_message("Click to let us know about this error")
2647 .on_click(|cx| cx.platform().open_url(REPORT_ISSUE_URL))
2648 })
2649 });
2650 } else {
2651 let backup_path = (*db::BACKUP_DB_PATH).read();
2652 if let Some(backup_path) = backup_path.clone() {
2653 workspace.show_notification_once(0, cx, move |cx| {
2654 cx.add_view(move |_| {
2655 MessageNotification::new(format!(
2656 "Database file was corrupted. Old database backed up to {}",
2657 backup_path.display()
2658 ))
2659 .with_click_message("Click to show old database in finder")
2660 .on_click(move |cx| {
2661 cx.platform().open_url(&backup_path.to_string_lossy())
2662 })
2663 })
2664 });
2665 }
2666 }
2667 })
2668 .log_err();
2669}
2670
2671impl Entity for Workspace {
2672 type Event = Event;
2673}
2674
2675impl View for Workspace {
2676 fn ui_name() -> &'static str {
2677 "Workspace"
2678 }
2679
2680 fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
2681 let theme = cx.global::<Settings>().theme.clone();
2682 Stack::new()
2683 .with_child(
2684 Flex::column()
2685 .with_child(self.render_titlebar(&theme, cx))
2686 .with_child(
2687 Stack::new()
2688 .with_child({
2689 let project = self.project.clone();
2690 Flex::row()
2691 .with_children(
2692 if self.left_sidebar.read(cx).active_item().is_some() {
2693 Some(
2694 ChildView::new(&self.left_sidebar, cx)
2695 .constrained()
2696 .dynamically(|constraint, _, cx| {
2697 SizeConstraint::new(
2698 Vector2F::new(20., constraint.min.y()),
2699 Vector2F::new(
2700 cx.window_size().x() * 0.8,
2701 constraint.max.y(),
2702 ),
2703 )
2704 }),
2705 )
2706 } else {
2707 None
2708 },
2709 )
2710 .with_child(
2711 FlexItem::new(
2712 Flex::column()
2713 .with_child(
2714 FlexItem::new(self.center.render(
2715 &project,
2716 &theme,
2717 &self.follower_states_by_leader,
2718 self.active_call(),
2719 self.active_pane(),
2720 &self.app_state,
2721 cx,
2722 ))
2723 .flex(1., true),
2724 )
2725 .with_children(self.dock.render(
2726 &theme,
2727 DockAnchor::Bottom,
2728 cx,
2729 )),
2730 )
2731 .flex(1., true),
2732 )
2733 .with_children(self.dock.render(&theme, DockAnchor::Right, cx))
2734 .with_children(
2735 if self.right_sidebar.read(cx).active_item().is_some() {
2736 Some(
2737 ChildView::new(&self.right_sidebar, cx)
2738 .constrained()
2739 .dynamically(|constraint, _, cx| {
2740 SizeConstraint::new(
2741 Vector2F::new(20., constraint.min.y()),
2742 Vector2F::new(
2743 cx.window_size().x() * 0.8,
2744 constraint.max.y(),
2745 ),
2746 )
2747 }),
2748 )
2749 } else {
2750 None
2751 },
2752 )
2753 })
2754 .with_child(Overlay::new(
2755 Stack::new()
2756 .with_children(self.dock.render(
2757 &theme,
2758 DockAnchor::Expanded,
2759 cx,
2760 ))
2761 .with_children(self.modal.as_ref().map(|modal| {
2762 ChildView::new(modal, cx)
2763 .contained()
2764 .with_style(theme.workspace.modal)
2765 .aligned()
2766 .top()
2767 }))
2768 .with_children(self.render_notifications(&theme.workspace, cx)),
2769 ))
2770 .flex(1.0, true),
2771 )
2772 .with_child(ChildView::new(&self.status_bar, cx))
2773 .contained()
2774 .with_background_color(theme.workspace.background),
2775 )
2776 .with_children(DragAndDrop::render(cx))
2777 .with_children(self.render_disconnected_overlay(cx))
2778 .into_any_named("workspace")
2779 }
2780
2781 fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
2782 if cx.is_self_focused() {
2783 cx.focus(&self.active_pane);
2784 }
2785 }
2786}
2787
2788impl ViewId {
2789 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
2790 Ok(Self {
2791 creator: message
2792 .creator
2793 .ok_or_else(|| anyhow!("creator is missing"))?,
2794 id: message.id,
2795 })
2796 }
2797
2798 pub(crate) fn to_proto(&self) -> proto::ViewId {
2799 proto::ViewId {
2800 creator: Some(self.creator),
2801 id: self.id,
2802 }
2803 }
2804}
2805
2806pub trait WorkspaceHandle {
2807 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
2808}
2809
2810impl WorkspaceHandle for ViewHandle<Workspace> {
2811 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
2812 self.read(cx)
2813 .worktrees(cx)
2814 .flat_map(|worktree| {
2815 let worktree_id = worktree.read(cx).id();
2816 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
2817 worktree_id,
2818 path: f.path.clone(),
2819 })
2820 })
2821 .collect::<Vec<_>>()
2822 }
2823}
2824
2825impl std::fmt::Debug for OpenPaths {
2826 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2827 f.debug_struct("OpenPaths")
2828 .field("paths", &self.paths)
2829 .finish()
2830 }
2831}
2832
2833pub struct WorkspaceCreated(WeakViewHandle<Workspace>);
2834
2835pub fn activate_workspace_for_project(
2836 cx: &mut AsyncAppContext,
2837 predicate: impl Fn(&mut Project, &mut ModelContext<Project>) -> bool,
2838) -> Option<WeakViewHandle<Workspace>> {
2839 for window_id in cx.window_ids() {
2840 let handle = cx
2841 .update_window(window_id, |cx| {
2842 if let Some(workspace_handle) = cx.root_view().clone().downcast::<Workspace>() {
2843 let project = workspace_handle.read(cx).project.clone();
2844 if project.update(cx, &predicate) {
2845 cx.activate_window();
2846 return Some(workspace_handle.clone());
2847 }
2848 }
2849 None
2850 })
2851 .flatten();
2852
2853 if let Some(handle) = handle {
2854 return Some(handle.downgrade());
2855 }
2856 }
2857 None
2858}
2859
2860pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
2861 DB.last_workspace().await.log_err().flatten()
2862}
2863
2864#[allow(clippy::type_complexity)]
2865pub fn open_paths(
2866 abs_paths: &[PathBuf],
2867 app_state: &Arc<AppState>,
2868 requesting_window_id: Option<usize>,
2869 cx: &mut AppContext,
2870) -> Task<
2871 Result<(
2872 WeakViewHandle<Workspace>,
2873 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
2874 )>,
2875> {
2876 log::info!("open paths {:?}", abs_paths);
2877
2878 let app_state = app_state.clone();
2879 let abs_paths = abs_paths.to_vec();
2880 cx.spawn(|mut cx| async move {
2881 // Open paths in existing workspace if possible
2882 let existing = activate_workspace_for_project(&mut cx, |project, cx| {
2883 project.contains_paths(&abs_paths, cx)
2884 });
2885
2886 if let Some(existing) = existing {
2887 Ok((
2888 existing.clone(),
2889 existing
2890 .update(&mut cx, |workspace, cx| {
2891 workspace.open_paths(abs_paths, true, cx)
2892 })?
2893 .await,
2894 ))
2895 } else {
2896 let contains_directory =
2897 futures::future::join_all(abs_paths.iter().map(|path| app_state.fs.is_file(path)))
2898 .await
2899 .contains(&false);
2900
2901 cx.update(|cx| {
2902 let task =
2903 Workspace::new_local(abs_paths, app_state.clone(), requesting_window_id, cx);
2904
2905 cx.spawn(|mut cx| async move {
2906 let (workspace, items) = task.await;
2907
2908 workspace.update(&mut cx, |workspace, cx| {
2909 if contains_directory {
2910 workspace.toggle_sidebar(SidebarSide::Left, cx);
2911 }
2912 })?;
2913
2914 anyhow::Ok((workspace, items))
2915 })
2916 })
2917 .await
2918 }
2919 })
2920}
2921
2922pub fn open_new(
2923 app_state: &Arc<AppState>,
2924 cx: &mut AppContext,
2925 init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static,
2926) -> Task<()> {
2927 let task = Workspace::new_local(Vec::new(), app_state.clone(), None, cx);
2928 cx.spawn(|mut cx| async move {
2929 let (workspace, opened_paths) = task.await;
2930
2931 workspace
2932 .update(&mut cx, |workspace, cx| {
2933 if opened_paths.is_empty() {
2934 init(workspace, cx)
2935 }
2936 })
2937 .log_err();
2938 })
2939}
2940
2941pub fn create_and_open_local_file(
2942 path: &'static Path,
2943 cx: &mut ViewContext<Workspace>,
2944 default_content: impl 'static + Send + FnOnce() -> Rope,
2945) -> Task<Result<Box<dyn ItemHandle>>> {
2946 cx.spawn(|workspace, mut cx| async move {
2947 let fs = workspace.read_with(&cx, |workspace, _| workspace.app_state().fs.clone())?;
2948 if !fs.is_file(path).await {
2949 fs.create_file(path, Default::default()).await?;
2950 fs.save(path, &default_content(), Default::default())
2951 .await?;
2952 }
2953
2954 let mut items = workspace
2955 .update(&mut cx, |workspace, cx| {
2956 workspace.with_local_workspace(cx, |workspace, cx| {
2957 workspace.open_paths(vec![path.to_path_buf()], false, cx)
2958 })
2959 })?
2960 .await?
2961 .await;
2962
2963 let item = items.pop().flatten();
2964 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
2965 })
2966}
2967
2968pub fn join_remote_project(
2969 project_id: u64,
2970 follow_user_id: u64,
2971 app_state: Arc<AppState>,
2972 cx: &mut AppContext,
2973) -> Task<Result<()>> {
2974 cx.spawn(|mut cx| async move {
2975 let existing_workspace = cx
2976 .window_ids()
2977 .into_iter()
2978 .filter_map(|window_id| cx.root_view(window_id)?.clone().downcast::<Workspace>())
2979 .find(|workspace| {
2980 cx.read_window(workspace.window_id(), |cx| {
2981 workspace.read(cx).project().read(cx).remote_id() == Some(project_id)
2982 })
2983 .unwrap_or(false)
2984 });
2985
2986 let workspace = if let Some(existing_workspace) = existing_workspace {
2987 existing_workspace.downgrade()
2988 } else {
2989 let active_call = cx.read(ActiveCall::global);
2990 let room = active_call
2991 .read_with(&cx, |call, _| call.room().cloned())
2992 .ok_or_else(|| anyhow!("not in a call"))?;
2993 let project = room
2994 .update(&mut cx, |room, cx| {
2995 room.join_project(
2996 project_id,
2997 app_state.languages.clone(),
2998 app_state.fs.clone(),
2999 cx,
3000 )
3001 })
3002 .await?;
3003
3004 let (_, workspace) = cx.add_window(
3005 (app_state.build_window_options)(None, None, cx.platform().as_ref()),
3006 |cx| {
3007 let mut workspace =
3008 Workspace::new(Default::default(), 0, project, app_state.clone(), cx);
3009 (app_state.initialize_workspace)(&mut workspace, &app_state, cx);
3010 workspace
3011 },
3012 );
3013 workspace.downgrade()
3014 };
3015
3016 cx.activate_window(workspace.window_id());
3017 cx.platform().activate(true);
3018
3019 workspace.update(&mut cx, |workspace, cx| {
3020 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
3021 let follow_peer_id = room
3022 .read(cx)
3023 .remote_participants()
3024 .iter()
3025 .find(|(_, participant)| participant.user.id == follow_user_id)
3026 .map(|(_, p)| p.peer_id)
3027 .or_else(|| {
3028 // If we couldn't follow the given user, follow the host instead.
3029 let collaborator = workspace
3030 .project()
3031 .read(cx)
3032 .collaborators()
3033 .values()
3034 .find(|collaborator| collaborator.replica_id == 0)?;
3035 Some(collaborator.peer_id)
3036 });
3037
3038 if let Some(follow_peer_id) = follow_peer_id {
3039 if !workspace.is_being_followed(follow_peer_id) {
3040 workspace
3041 .toggle_follow(follow_peer_id, cx)
3042 .map(|follow| follow.detach_and_log_err(cx));
3043 }
3044 }
3045 }
3046 })?;
3047
3048 anyhow::Ok(())
3049 })
3050}
3051
3052pub fn restart(_: &Restart, cx: &mut AppContext) {
3053 let should_confirm = cx.global::<Settings>().confirm_quit;
3054 cx.spawn(|mut cx| async move {
3055 let mut workspaces = cx
3056 .window_ids()
3057 .into_iter()
3058 .filter_map(|window_id| {
3059 Some(
3060 cx.root_view(window_id)?
3061 .clone()
3062 .downcast::<Workspace>()?
3063 .downgrade(),
3064 )
3065 })
3066 .collect::<Vec<_>>();
3067
3068 // If multiple windows have unsaved changes, and need a save prompt,
3069 // prompt in the active window before switching to a different window.
3070 workspaces.sort_by_key(|workspace| !cx.window_is_active(workspace.window_id()));
3071
3072 if let (true, Some(workspace)) = (should_confirm, workspaces.first()) {
3073 let answer = cx.prompt(
3074 workspace.window_id(),
3075 PromptLevel::Info,
3076 "Are you sure you want to restart?",
3077 &["Restart", "Cancel"],
3078 );
3079
3080 if let Some(mut answer) = answer {
3081 let answer = answer.next().await;
3082 if answer != Some(0) {
3083 return Ok(());
3084 }
3085 }
3086 }
3087
3088 // If the user cancels any save prompt, then keep the app open.
3089 for workspace in workspaces {
3090 if !workspace
3091 .update(&mut cx, |workspace, cx| {
3092 workspace.prepare_to_close(true, cx)
3093 })?
3094 .await?
3095 {
3096 return Ok(());
3097 }
3098 }
3099 cx.platform().restart();
3100 anyhow::Ok(())
3101 })
3102 .detach_and_log_err(cx);
3103}
3104
3105fn parse_pixel_position_env_var(value: &str) -> Option<Vector2F> {
3106 let mut parts = value.split(',');
3107 let width: usize = parts.next()?.parse().ok()?;
3108 let height: usize = parts.next()?.parse().ok()?;
3109 Some(vec2f(width as f32, height as f32))
3110}
3111
3112#[cfg(test)]
3113mod tests {
3114 use std::{cell::RefCell, rc::Rc};
3115
3116 use crate::item::test::{TestItem, TestItemEvent, TestProjectItem};
3117
3118 use super::*;
3119 use fs::FakeFs;
3120 use gpui::{executor::Deterministic, TestAppContext};
3121 use project::{Project, ProjectEntryId};
3122 use serde_json::json;
3123
3124 #[gpui::test]
3125 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
3126 cx.foreground().forbid_parking();
3127 Settings::test_async(cx);
3128
3129 let fs = FakeFs::new(cx.background());
3130 let project = Project::test(fs, [], cx).await;
3131 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
3132
3133 // Adding an item with no ambiguity renders the tab without detail.
3134 let item1 = cx.add_view(window_id, |_| {
3135 let mut item = TestItem::new();
3136 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
3137 item
3138 });
3139 workspace.update(cx, |workspace, cx| {
3140 workspace.add_item(Box::new(item1.clone()), cx);
3141 });
3142 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), None));
3143
3144 // Adding an item that creates ambiguity increases the level of detail on
3145 // both tabs.
3146 let item2 = cx.add_view(window_id, |_| {
3147 let mut item = TestItem::new();
3148 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
3149 item
3150 });
3151 workspace.update(cx, |workspace, cx| {
3152 workspace.add_item(Box::new(item2.clone()), cx);
3153 });
3154 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
3155 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
3156
3157 // Adding an item that creates ambiguity increases the level of detail only
3158 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
3159 // we stop at the highest detail available.
3160 let item3 = cx.add_view(window_id, |_| {
3161 let mut item = TestItem::new();
3162 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
3163 item
3164 });
3165 workspace.update(cx, |workspace, cx| {
3166 workspace.add_item(Box::new(item3.clone()), cx);
3167 });
3168 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
3169 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
3170 item3.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
3171 }
3172
3173 #[gpui::test]
3174 async fn test_tracking_active_path(cx: &mut TestAppContext) {
3175 cx.foreground().forbid_parking();
3176 Settings::test_async(cx);
3177 let fs = FakeFs::new(cx.background());
3178 fs.insert_tree(
3179 "/root1",
3180 json!({
3181 "one.txt": "",
3182 "two.txt": "",
3183 }),
3184 )
3185 .await;
3186 fs.insert_tree(
3187 "/root2",
3188 json!({
3189 "three.txt": "",
3190 }),
3191 )
3192 .await;
3193
3194 let project = Project::test(fs, ["root1".as_ref()], cx).await;
3195 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
3196 let worktree_id = project.read_with(cx, |project, cx| {
3197 project.worktrees(cx).next().unwrap().read(cx).id()
3198 });
3199
3200 let item1 = cx.add_view(window_id, |cx| {
3201 TestItem::new().with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
3202 });
3203 let item2 = cx.add_view(window_id, |cx| {
3204 TestItem::new().with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
3205 });
3206
3207 // Add an item to an empty pane
3208 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx));
3209 project.read_with(cx, |project, cx| {
3210 assert_eq!(
3211 project.active_entry(),
3212 project
3213 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
3214 .map(|e| e.id)
3215 );
3216 });
3217 assert_eq!(
3218 cx.current_window_title(window_id).as_deref(),
3219 Some("one.txt — root1")
3220 );
3221
3222 // Add a second item to a non-empty pane
3223 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
3224 assert_eq!(
3225 cx.current_window_title(window_id).as_deref(),
3226 Some("two.txt — root1")
3227 );
3228 project.read_with(cx, |project, cx| {
3229 assert_eq!(
3230 project.active_entry(),
3231 project
3232 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
3233 .map(|e| e.id)
3234 );
3235 });
3236
3237 // Close the active item
3238 workspace
3239 .update(cx, |workspace, cx| {
3240 Pane::close_active_item(workspace, &Default::default(), cx).unwrap()
3241 })
3242 .await
3243 .unwrap();
3244 assert_eq!(
3245 cx.current_window_title(window_id).as_deref(),
3246 Some("one.txt — root1")
3247 );
3248 project.read_with(cx, |project, cx| {
3249 assert_eq!(
3250 project.active_entry(),
3251 project
3252 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
3253 .map(|e| e.id)
3254 );
3255 });
3256
3257 // Add a project folder
3258 project
3259 .update(cx, |project, cx| {
3260 project.find_or_create_local_worktree("/root2", true, cx)
3261 })
3262 .await
3263 .unwrap();
3264 assert_eq!(
3265 cx.current_window_title(window_id).as_deref(),
3266 Some("one.txt — root1, root2")
3267 );
3268
3269 // Remove a project folder
3270 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
3271 assert_eq!(
3272 cx.current_window_title(window_id).as_deref(),
3273 Some("one.txt — root2")
3274 );
3275 }
3276
3277 #[gpui::test]
3278 async fn test_close_window(cx: &mut TestAppContext) {
3279 cx.foreground().forbid_parking();
3280 Settings::test_async(cx);
3281 let fs = FakeFs::new(cx.background());
3282 fs.insert_tree("/root", json!({ "one": "" })).await;
3283
3284 let project = Project::test(fs, ["root".as_ref()], cx).await;
3285 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
3286
3287 // When there are no dirty items, there's nothing to do.
3288 let item1 = cx.add_view(window_id, |_| TestItem::new());
3289 workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
3290 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
3291 assert!(task.await.unwrap());
3292
3293 // When there are dirty untitled items, prompt to save each one. If the user
3294 // cancels any prompt, then abort.
3295 let item2 = cx.add_view(window_id, |_| TestItem::new().with_dirty(true));
3296 let item3 = cx.add_view(window_id, |cx| {
3297 TestItem::new()
3298 .with_dirty(true)
3299 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3300 });
3301 workspace.update(cx, |w, cx| {
3302 w.add_item(Box::new(item2.clone()), cx);
3303 w.add_item(Box::new(item3.clone()), cx);
3304 });
3305 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
3306 cx.foreground().run_until_parked();
3307 cx.simulate_prompt_answer(window_id, 2 /* cancel */);
3308 cx.foreground().run_until_parked();
3309 assert!(!cx.has_pending_prompt(window_id));
3310 assert!(!task.await.unwrap());
3311 }
3312
3313 #[gpui::test]
3314 async fn test_close_pane_items(cx: &mut TestAppContext) {
3315 cx.foreground().forbid_parking();
3316 Settings::test_async(cx);
3317 let fs = FakeFs::new(cx.background());
3318
3319 let project = Project::test(fs, None, cx).await;
3320 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
3321
3322 let item1 = cx.add_view(window_id, |cx| {
3323 TestItem::new()
3324 .with_dirty(true)
3325 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3326 });
3327 let item2 = cx.add_view(window_id, |cx| {
3328 TestItem::new()
3329 .with_dirty(true)
3330 .with_conflict(true)
3331 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
3332 });
3333 let item3 = cx.add_view(window_id, |cx| {
3334 TestItem::new()
3335 .with_dirty(true)
3336 .with_conflict(true)
3337 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
3338 });
3339 let item4 = cx.add_view(window_id, |cx| {
3340 TestItem::new()
3341 .with_dirty(true)
3342 .with_project_items(&[TestProjectItem::new_untitled(cx)])
3343 });
3344 let pane = workspace.update(cx, |workspace, cx| {
3345 workspace.add_item(Box::new(item1.clone()), cx);
3346 workspace.add_item(Box::new(item2.clone()), cx);
3347 workspace.add_item(Box::new(item3.clone()), cx);
3348 workspace.add_item(Box::new(item4.clone()), cx);
3349 workspace.active_pane().clone()
3350 });
3351
3352 let close_items = workspace.update(cx, |workspace, cx| {
3353 pane.update(cx, |pane, cx| {
3354 pane.activate_item(1, true, true, cx);
3355 assert_eq!(pane.active_item().unwrap().id(), item2.id());
3356 });
3357
3358 let item1_id = item1.id();
3359 let item3_id = item3.id();
3360 let item4_id = item4.id();
3361 Pane::close_items(workspace, pane.clone(), cx, move |id| {
3362 [item1_id, item3_id, item4_id].contains(&id)
3363 })
3364 });
3365 cx.foreground().run_until_parked();
3366
3367 // There's a prompt to save item 1.
3368 pane.read_with(cx, |pane, _| {
3369 assert_eq!(pane.items_len(), 4);
3370 assert_eq!(pane.active_item().unwrap().id(), item1.id());
3371 });
3372 assert!(cx.has_pending_prompt(window_id));
3373
3374 // Confirm saving item 1.
3375 cx.simulate_prompt_answer(window_id, 0);
3376 cx.foreground().run_until_parked();
3377
3378 // Item 1 is saved. There's a prompt to save item 3.
3379 pane.read_with(cx, |pane, cx| {
3380 assert_eq!(item1.read(cx).save_count, 1);
3381 assert_eq!(item1.read(cx).save_as_count, 0);
3382 assert_eq!(item1.read(cx).reload_count, 0);
3383 assert_eq!(pane.items_len(), 3);
3384 assert_eq!(pane.active_item().unwrap().id(), item3.id());
3385 });
3386 assert!(cx.has_pending_prompt(window_id));
3387
3388 // Cancel saving item 3.
3389 cx.simulate_prompt_answer(window_id, 1);
3390 cx.foreground().run_until_parked();
3391
3392 // Item 3 is reloaded. There's a prompt to save item 4.
3393 pane.read_with(cx, |pane, cx| {
3394 assert_eq!(item3.read(cx).save_count, 0);
3395 assert_eq!(item3.read(cx).save_as_count, 0);
3396 assert_eq!(item3.read(cx).reload_count, 1);
3397 assert_eq!(pane.items_len(), 2);
3398 assert_eq!(pane.active_item().unwrap().id(), item4.id());
3399 });
3400 assert!(cx.has_pending_prompt(window_id));
3401
3402 // Confirm saving item 4.
3403 cx.simulate_prompt_answer(window_id, 0);
3404 cx.foreground().run_until_parked();
3405
3406 // There's a prompt for a path for item 4.
3407 cx.simulate_new_path_selection(|_| Some(Default::default()));
3408 close_items.await.unwrap();
3409
3410 // The requested items are closed.
3411 pane.read_with(cx, |pane, cx| {
3412 assert_eq!(item4.read(cx).save_count, 0);
3413 assert_eq!(item4.read(cx).save_as_count, 1);
3414 assert_eq!(item4.read(cx).reload_count, 0);
3415 assert_eq!(pane.items_len(), 1);
3416 assert_eq!(pane.active_item().unwrap().id(), item2.id());
3417 });
3418 }
3419
3420 #[gpui::test]
3421 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
3422 cx.foreground().forbid_parking();
3423 Settings::test_async(cx);
3424 let fs = FakeFs::new(cx.background());
3425
3426 let project = Project::test(fs, [], cx).await;
3427 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
3428
3429 // Create several workspace items with single project entries, and two
3430 // workspace items with multiple project entries.
3431 let single_entry_items = (0..=4)
3432 .map(|project_entry_id| {
3433 cx.add_view(window_id, |cx| {
3434 TestItem::new()
3435 .with_dirty(true)
3436 .with_project_items(&[TestProjectItem::new(
3437 project_entry_id,
3438 &format!("{project_entry_id}.txt"),
3439 cx,
3440 )])
3441 })
3442 })
3443 .collect::<Vec<_>>();
3444 let item_2_3 = cx.add_view(window_id, |cx| {
3445 TestItem::new()
3446 .with_dirty(true)
3447 .with_singleton(false)
3448 .with_project_items(&[
3449 single_entry_items[2].read(cx).project_items[0].clone(),
3450 single_entry_items[3].read(cx).project_items[0].clone(),
3451 ])
3452 });
3453 let item_3_4 = cx.add_view(window_id, |cx| {
3454 TestItem::new()
3455 .with_dirty(true)
3456 .with_singleton(false)
3457 .with_project_items(&[
3458 single_entry_items[3].read(cx).project_items[0].clone(),
3459 single_entry_items[4].read(cx).project_items[0].clone(),
3460 ])
3461 });
3462
3463 // Create two panes that contain the following project entries:
3464 // left pane:
3465 // multi-entry items: (2, 3)
3466 // single-entry items: 0, 1, 2, 3, 4
3467 // right pane:
3468 // single-entry items: 1
3469 // multi-entry items: (3, 4)
3470 let left_pane = workspace.update(cx, |workspace, cx| {
3471 let left_pane = workspace.active_pane().clone();
3472 workspace.add_item(Box::new(item_2_3.clone()), cx);
3473 for item in single_entry_items {
3474 workspace.add_item(Box::new(item), cx);
3475 }
3476 left_pane.update(cx, |pane, cx| {
3477 pane.activate_item(2, true, true, cx);
3478 });
3479
3480 workspace
3481 .split_pane(left_pane.clone(), SplitDirection::Right, cx)
3482 .unwrap();
3483
3484 left_pane
3485 });
3486
3487 //Need to cause an effect flush in order to respect new focus
3488 workspace.update(cx, |workspace, cx| {
3489 workspace.add_item(Box::new(item_3_4.clone()), cx);
3490 cx.focus(&left_pane);
3491 });
3492
3493 // When closing all of the items in the left pane, we should be prompted twice:
3494 // once for project entry 0, and once for project entry 2. After those two
3495 // prompts, the task should complete.
3496
3497 let close = workspace.update(cx, |workspace, cx| {
3498 Pane::close_items(workspace, left_pane.clone(), cx, |_| true)
3499 });
3500
3501 cx.foreground().run_until_parked();
3502 left_pane.read_with(cx, |pane, cx| {
3503 assert_eq!(
3504 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
3505 &[ProjectEntryId::from_proto(0)]
3506 );
3507 });
3508 cx.simulate_prompt_answer(window_id, 0);
3509
3510 cx.foreground().run_until_parked();
3511 left_pane.read_with(cx, |pane, cx| {
3512 assert_eq!(
3513 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
3514 &[ProjectEntryId::from_proto(2)]
3515 );
3516 });
3517 cx.simulate_prompt_answer(window_id, 0);
3518
3519 cx.foreground().run_until_parked();
3520 close.await.unwrap();
3521 left_pane.read_with(cx, |pane, _| {
3522 assert_eq!(pane.items_len(), 0);
3523 });
3524 }
3525
3526 #[gpui::test]
3527 async fn test_autosave(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
3528 deterministic.forbid_parking();
3529
3530 Settings::test_async(cx);
3531 let fs = FakeFs::new(cx.background());
3532
3533 let project = Project::test(fs, [], cx).await;
3534 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
3535
3536 let item = cx.add_view(window_id, |cx| {
3537 TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3538 });
3539 let item_id = item.id();
3540 workspace.update(cx, |workspace, cx| {
3541 workspace.add_item(Box::new(item.clone()), cx);
3542 });
3543
3544 // Autosave on window change.
3545 item.update(cx, |item, cx| {
3546 cx.update_global(|settings: &mut Settings, _| {
3547 settings.autosave = Autosave::OnWindowChange;
3548 });
3549 item.is_dirty = true;
3550 });
3551
3552 // Deactivating the window saves the file.
3553 cx.simulate_window_activation(None);
3554 deterministic.run_until_parked();
3555 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
3556
3557 // Autosave on focus change.
3558 item.update(cx, |item, cx| {
3559 cx.focus_self();
3560 cx.update_global(|settings: &mut Settings, _| {
3561 settings.autosave = Autosave::OnFocusChange;
3562 });
3563 item.is_dirty = true;
3564 });
3565
3566 // Blurring the item saves the file.
3567 item.update(cx, |_, cx| cx.blur());
3568 deterministic.run_until_parked();
3569 item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
3570
3571 // Deactivating the window still saves the file.
3572 cx.simulate_window_activation(Some(window_id));
3573 item.update(cx, |item, cx| {
3574 cx.focus_self();
3575 item.is_dirty = true;
3576 });
3577 cx.simulate_window_activation(None);
3578
3579 deterministic.run_until_parked();
3580 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
3581
3582 // Autosave after delay.
3583 item.update(cx, |item, cx| {
3584 cx.update_global(|settings: &mut Settings, _| {
3585 settings.autosave = Autosave::AfterDelay { milliseconds: 500 };
3586 });
3587 item.is_dirty = true;
3588 cx.emit(TestItemEvent::Edit);
3589 });
3590
3591 // Delay hasn't fully expired, so the file is still dirty and unsaved.
3592 deterministic.advance_clock(Duration::from_millis(250));
3593 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
3594
3595 // After delay expires, the file is saved.
3596 deterministic.advance_clock(Duration::from_millis(250));
3597 item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
3598
3599 // Autosave on focus change, ensuring closing the tab counts as such.
3600 item.update(cx, |item, cx| {
3601 cx.update_global(|settings: &mut Settings, _| {
3602 settings.autosave = Autosave::OnFocusChange;
3603 });
3604 item.is_dirty = true;
3605 });
3606
3607 workspace
3608 .update(cx, |workspace, cx| {
3609 let pane = workspace.active_pane().clone();
3610 Pane::close_items(workspace, pane, cx, move |id| id == item_id)
3611 })
3612 .await
3613 .unwrap();
3614 assert!(!cx.has_pending_prompt(window_id));
3615 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
3616
3617 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
3618 workspace.update(cx, |workspace, cx| {
3619 workspace.add_item(Box::new(item.clone()), cx);
3620 });
3621 item.update(cx, |item, cx| {
3622 item.project_items[0].update(cx, |item, _| {
3623 item.entry_id = None;
3624 });
3625 item.is_dirty = true;
3626 cx.blur();
3627 });
3628 deterministic.run_until_parked();
3629 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
3630
3631 // Ensure autosave is prevented for deleted files also when closing the buffer.
3632 let _close_items = workspace.update(cx, |workspace, cx| {
3633 let pane = workspace.active_pane().clone();
3634 Pane::close_items(workspace, pane, cx, move |id| id == item_id)
3635 });
3636 deterministic.run_until_parked();
3637 assert!(cx.has_pending_prompt(window_id));
3638 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
3639 }
3640
3641 #[gpui::test]
3642 async fn test_pane_navigation(
3643 deterministic: Arc<Deterministic>,
3644 cx: &mut gpui::TestAppContext,
3645 ) {
3646 deterministic.forbid_parking();
3647 Settings::test_async(cx);
3648 let fs = FakeFs::new(cx.background());
3649
3650 let project = Project::test(fs, [], cx).await;
3651 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
3652
3653 let item = cx.add_view(window_id, |cx| {
3654 TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3655 });
3656 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3657 let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone());
3658 let toolbar_notify_count = Rc::new(RefCell::new(0));
3659
3660 workspace.update(cx, |workspace, cx| {
3661 workspace.add_item(Box::new(item.clone()), cx);
3662 let toolbar_notification_count = toolbar_notify_count.clone();
3663 cx.observe(&toolbar, move |_, _, _| {
3664 *toolbar_notification_count.borrow_mut() += 1
3665 })
3666 .detach();
3667 });
3668
3669 pane.read_with(cx, |pane, _| {
3670 assert!(!pane.can_navigate_backward());
3671 assert!(!pane.can_navigate_forward());
3672 });
3673
3674 item.update(cx, |item, cx| {
3675 item.set_state("one".to_string(), cx);
3676 });
3677
3678 // Toolbar must be notified to re-render the navigation buttons
3679 assert_eq!(*toolbar_notify_count.borrow(), 1);
3680
3681 pane.read_with(cx, |pane, _| {
3682 assert!(pane.can_navigate_backward());
3683 assert!(!pane.can_navigate_forward());
3684 });
3685
3686 workspace
3687 .update(cx, |workspace, cx| {
3688 Pane::go_back(workspace, Some(pane.downgrade()), cx)
3689 })
3690 .await
3691 .unwrap();
3692
3693 assert_eq!(*toolbar_notify_count.borrow(), 3);
3694 pane.read_with(cx, |pane, _| {
3695 assert!(!pane.can_navigate_backward());
3696 assert!(pane.can_navigate_forward());
3697 });
3698 }
3699}