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