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