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