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