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