1pub mod lsp_status;
2pub mod menu;
3pub mod pane;
4pub mod pane_group;
5pub mod sidebar;
6mod status_bar;
7mod toolbar;
8mod waiting_room;
9
10use anyhow::{anyhow, Context, Result};
11use client::{
12 proto, Authenticate, Client, Contact, PeerId, Subscription, TypedEnvelope, User, UserStore,
13};
14use clock::ReplicaId;
15use collections::{hash_map, HashMap, HashSet};
16use gpui::{
17 actions,
18 color::Color,
19 elements::*,
20 geometry::{rect::RectF, vector::vec2f, PathBuilder},
21 impl_internal_actions,
22 json::{self, ToJson},
23 platform::{CursorStyle, WindowOptions},
24 AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Border, Entity, ImageData,
25 ModelHandle, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, View,
26 ViewContext, ViewHandle, WeakViewHandle,
27};
28use language::LanguageRegistry;
29use log::error;
30pub use pane::*;
31pub use pane_group::*;
32use postage::prelude::Stream;
33use project::{fs, Fs, Project, ProjectEntryId, ProjectPath, Worktree};
34use settings::Settings;
35use sidebar::{Side, Sidebar, SidebarButtons, ToggleSidebarItem, ToggleSidebarItemFocus};
36use smallvec::SmallVec;
37use status_bar::StatusBar;
38pub use status_bar::StatusItemView;
39use std::{
40 any::{Any, TypeId},
41 borrow::Cow,
42 cell::RefCell,
43 fmt,
44 future::Future,
45 path::{Path, PathBuf},
46 rc::Rc,
47 sync::{
48 atomic::{AtomicBool, Ordering::SeqCst},
49 Arc,
50 },
51};
52use theme::{Theme, ThemeRegistry};
53pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
54use util::ResultExt;
55use waiting_room::WaitingRoom;
56
57type ProjectItemBuilders = HashMap<
58 TypeId,
59 fn(usize, ModelHandle<Project>, AnyModelHandle, &mut MutableAppContext) -> Box<dyn ItemHandle>,
60>;
61
62type FollowableItemBuilder = fn(
63 ViewHandle<Pane>,
64 ModelHandle<Project>,
65 &mut Option<proto::view::Variant>,
66 &mut MutableAppContext,
67) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>;
68type FollowableItemBuilders = HashMap<
69 TypeId,
70 (
71 FollowableItemBuilder,
72 fn(AnyViewHandle) -> Box<dyn FollowableItemHandle>,
73 ),
74>;
75
76actions!(
77 workspace,
78 [
79 Open,
80 NewFile,
81 NewWindow,
82 CloseWindow,
83 AddFolderToProject,
84 Unfollow,
85 Save,
86 SaveAs,
87 SaveAll,
88 ActivatePreviousPane,
89 ActivateNextPane,
90 FollowNextCollaborator,
91 ]
92);
93
94#[derive(Clone)]
95pub struct OpenPaths {
96 pub paths: Vec<PathBuf>,
97}
98
99#[derive(Clone)]
100pub struct ToggleFollow(pub PeerId);
101
102#[derive(Clone)]
103pub struct JoinProject {
104 pub contact: Arc<Contact>,
105 pub project_index: usize,
106}
107
108impl_internal_actions!(workspace, [OpenPaths, ToggleFollow, JoinProject]);
109
110pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
111 pane::init(cx);
112
113 cx.add_global_action(open);
114 cx.add_global_action({
115 let app_state = Arc::downgrade(&app_state);
116 move |action: &OpenPaths, cx: &mut MutableAppContext| {
117 if let Some(app_state) = app_state.upgrade() {
118 open_paths(&action.paths, &app_state, cx).detach();
119 }
120 }
121 });
122 cx.add_global_action({
123 let app_state = Arc::downgrade(&app_state);
124 move |_: &NewFile, cx: &mut MutableAppContext| {
125 if let Some(app_state) = app_state.upgrade() {
126 open_new(&app_state, cx)
127 }
128 }
129 });
130 cx.add_global_action({
131 let app_state = Arc::downgrade(&app_state);
132 move |_: &NewWindow, cx: &mut MutableAppContext| {
133 if let Some(app_state) = app_state.upgrade() {
134 open_new(&app_state, cx)
135 }
136 }
137 });
138 cx.add_global_action({
139 let app_state = Arc::downgrade(&app_state);
140 move |action: &JoinProject, cx: &mut MutableAppContext| {
141 if let Some(app_state) = app_state.upgrade() {
142 join_project(action.contact.clone(), action.project_index, &app_state, cx);
143 }
144 }
145 });
146
147 cx.add_async_action(Workspace::toggle_follow);
148 cx.add_async_action(Workspace::follow_next_collaborator);
149 cx.add_async_action(Workspace::close);
150 cx.add_async_action(Workspace::save_all);
151 cx.add_action(Workspace::add_folder_to_project);
152 cx.add_action(
153 |workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext<Workspace>| {
154 let pane = workspace.active_pane().clone();
155 workspace.unfollow(&pane, cx);
156 },
157 );
158 cx.add_action(
159 |workspace: &mut Workspace, _: &Save, cx: &mut ViewContext<Workspace>| {
160 workspace.save_active_item(false, cx).detach_and_log_err(cx);
161 },
162 );
163 cx.add_action(
164 |workspace: &mut Workspace, _: &SaveAs, cx: &mut ViewContext<Workspace>| {
165 workspace.save_active_item(true, cx).detach_and_log_err(cx);
166 },
167 );
168 cx.add_action(Workspace::toggle_sidebar_item);
169 cx.add_action(Workspace::toggle_sidebar_item_focus);
170 cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| {
171 workspace.activate_previous_pane(cx)
172 });
173 cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| {
174 workspace.activate_next_pane(cx)
175 });
176
177 let client = &app_state.client;
178 client.add_view_request_handler(Workspace::handle_follow);
179 client.add_view_message_handler(Workspace::handle_unfollow);
180 client.add_view_message_handler(Workspace::handle_update_followers);
181}
182
183pub fn register_project_item<I: ProjectItem>(cx: &mut MutableAppContext) {
184 cx.update_default_global(|builders: &mut ProjectItemBuilders, _| {
185 builders.insert(TypeId::of::<I::Item>(), |window_id, project, model, cx| {
186 let item = model.downcast::<I::Item>().unwrap();
187 Box::new(cx.add_view(window_id, |cx| I::for_project_item(project, item, cx)))
188 });
189 });
190}
191
192pub fn register_followable_item<I: FollowableItem>(cx: &mut MutableAppContext) {
193 cx.update_default_global(|builders: &mut FollowableItemBuilders, _| {
194 builders.insert(
195 TypeId::of::<I>(),
196 (
197 |pane, project, state, cx| {
198 I::from_state_proto(pane, project, state, cx).map(|task| {
199 cx.foreground()
200 .spawn(async move { Ok(Box::new(task.await?) as Box<_>) })
201 })
202 },
203 |this| Box::new(this.downcast::<I>().unwrap()),
204 ),
205 );
206 });
207}
208
209pub struct AppState {
210 pub languages: Arc<LanguageRegistry>,
211 pub themes: Arc<ThemeRegistry>,
212 pub client: Arc<client::Client>,
213 pub user_store: ModelHandle<client::UserStore>,
214 pub fs: Arc<dyn fs::Fs>,
215 pub build_window_options: fn() -> WindowOptions<'static>,
216 pub initialize_workspace: fn(&mut Workspace, &Arc<AppState>, &mut ViewContext<Workspace>),
217}
218
219pub trait Item: View {
220 fn deactivated(&mut self, _: &mut ViewContext<Self>) {}
221 fn navigate(&mut self, _: Box<dyn Any>, _: &mut ViewContext<Self>) -> bool {
222 false
223 }
224 fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox;
225 fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
226 fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]>;
227 fn is_singleton(&self, cx: &AppContext) -> bool;
228 fn set_nav_history(&mut self, _: ItemNavHistory, _: &mut ViewContext<Self>);
229 fn clone_on_split(&self, _: &mut ViewContext<Self>) -> Option<Self>
230 where
231 Self: Sized,
232 {
233 None
234 }
235 fn is_dirty(&self, _: &AppContext) -> bool {
236 false
237 }
238 fn has_conflict(&self, _: &AppContext) -> bool {
239 false
240 }
241 fn can_save(&self, cx: &AppContext) -> bool;
242 fn save(
243 &mut self,
244 project: ModelHandle<Project>,
245 cx: &mut ViewContext<Self>,
246 ) -> Task<Result<()>>;
247 fn save_as(
248 &mut self,
249 project: ModelHandle<Project>,
250 abs_path: PathBuf,
251 cx: &mut ViewContext<Self>,
252 ) -> Task<Result<()>>;
253 fn reload(
254 &mut self,
255 project: ModelHandle<Project>,
256 cx: &mut ViewContext<Self>,
257 ) -> Task<Result<()>>;
258 fn should_activate_item_on_event(_: &Self::Event) -> bool {
259 false
260 }
261 fn should_close_item_on_event(_: &Self::Event) -> bool {
262 false
263 }
264 fn should_update_tab_on_event(_: &Self::Event) -> bool {
265 false
266 }
267 fn act_as_type(
268 &self,
269 type_id: TypeId,
270 self_handle: &ViewHandle<Self>,
271 _: &AppContext,
272 ) -> Option<AnyViewHandle> {
273 if TypeId::of::<Self>() == type_id {
274 Some(self_handle.into())
275 } else {
276 None
277 }
278 }
279}
280
281pub trait ProjectItem: Item {
282 type Item: project::Item;
283
284 fn for_project_item(
285 project: ModelHandle<Project>,
286 item: ModelHandle<Self::Item>,
287 cx: &mut ViewContext<Self>,
288 ) -> Self;
289}
290
291pub trait FollowableItem: Item {
292 fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant>;
293 fn from_state_proto(
294 pane: ViewHandle<Pane>,
295 project: ModelHandle<Project>,
296 state: &mut Option<proto::view::Variant>,
297 cx: &mut MutableAppContext,
298 ) -> Option<Task<Result<ViewHandle<Self>>>>;
299 fn add_event_to_update_proto(
300 &self,
301 event: &Self::Event,
302 update: &mut Option<proto::update_view::Variant>,
303 cx: &AppContext,
304 ) -> bool;
305 fn apply_update_proto(
306 &mut self,
307 message: proto::update_view::Variant,
308 cx: &mut ViewContext<Self>,
309 ) -> Result<()>;
310
311 fn set_leader_replica_id(&mut self, leader_replica_id: Option<u16>, cx: &mut ViewContext<Self>);
312 fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool;
313}
314
315pub trait FollowableItemHandle: ItemHandle {
316 fn set_leader_replica_id(&self, leader_replica_id: Option<u16>, cx: &mut MutableAppContext);
317 fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant>;
318 fn add_event_to_update_proto(
319 &self,
320 event: &dyn Any,
321 update: &mut Option<proto::update_view::Variant>,
322 cx: &AppContext,
323 ) -> bool;
324 fn apply_update_proto(
325 &self,
326 message: proto::update_view::Variant,
327 cx: &mut MutableAppContext,
328 ) -> Result<()>;
329 fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool;
330}
331
332impl<T: FollowableItem> FollowableItemHandle for ViewHandle<T> {
333 fn set_leader_replica_id(&self, leader_replica_id: Option<u16>, cx: &mut MutableAppContext) {
334 self.update(cx, |this, cx| {
335 this.set_leader_replica_id(leader_replica_id, cx)
336 })
337 }
338
339 fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
340 self.read(cx).to_state_proto(cx)
341 }
342
343 fn add_event_to_update_proto(
344 &self,
345 event: &dyn Any,
346 update: &mut Option<proto::update_view::Variant>,
347 cx: &AppContext,
348 ) -> bool {
349 if let Some(event) = event.downcast_ref() {
350 self.read(cx).add_event_to_update_proto(event, update, cx)
351 } else {
352 false
353 }
354 }
355
356 fn apply_update_proto(
357 &self,
358 message: proto::update_view::Variant,
359 cx: &mut MutableAppContext,
360 ) -> Result<()> {
361 self.update(cx, |this, cx| this.apply_update_proto(message, cx))
362 }
363
364 fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool {
365 if let Some(event) = event.downcast_ref() {
366 T::should_unfollow_on_event(event, cx)
367 } else {
368 false
369 }
370 }
371}
372
373pub trait ItemHandle: 'static + fmt::Debug {
374 fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox;
375 fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
376 fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]>;
377 fn is_singleton(&self, cx: &AppContext) -> bool;
378 fn boxed_clone(&self) -> Box<dyn ItemHandle>;
379 fn set_nav_history(&self, nav_history: Rc<RefCell<NavHistory>>, cx: &mut MutableAppContext);
380 fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemHandle>>;
381 fn added_to_pane(
382 &self,
383 workspace: &mut Workspace,
384 pane: ViewHandle<Pane>,
385 cx: &mut ViewContext<Workspace>,
386 );
387 fn deactivated(&self, cx: &mut MutableAppContext);
388 fn navigate(&self, data: Box<dyn Any>, cx: &mut MutableAppContext) -> bool;
389 fn id(&self) -> usize;
390 fn to_any(&self) -> AnyViewHandle;
391 fn is_dirty(&self, cx: &AppContext) -> bool;
392 fn has_conflict(&self, cx: &AppContext) -> bool;
393 fn can_save(&self, cx: &AppContext) -> bool;
394 fn save(&self, project: ModelHandle<Project>, cx: &mut MutableAppContext) -> Task<Result<()>>;
395 fn save_as(
396 &self,
397 project: ModelHandle<Project>,
398 abs_path: PathBuf,
399 cx: &mut MutableAppContext,
400 ) -> Task<Result<()>>;
401 fn reload(&self, project: ModelHandle<Project>, cx: &mut MutableAppContext)
402 -> Task<Result<()>>;
403 fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option<AnyViewHandle>;
404 fn to_followable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn FollowableItemHandle>>;
405 fn on_release(
406 &self,
407 cx: &mut MutableAppContext,
408 callback: Box<dyn FnOnce(&mut MutableAppContext)>,
409 ) -> gpui::Subscription;
410}
411
412pub trait WeakItemHandle {
413 fn id(&self) -> usize;
414 fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>>;
415}
416
417impl dyn ItemHandle {
418 pub fn downcast<T: View>(&self) -> Option<ViewHandle<T>> {
419 self.to_any().downcast()
420 }
421
422 pub fn act_as<T: View>(&self, cx: &AppContext) -> Option<ViewHandle<T>> {
423 self.act_as_type(TypeId::of::<T>(), cx)
424 .and_then(|t| t.downcast())
425 }
426}
427
428impl<T: Item> ItemHandle for ViewHandle<T> {
429 fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox {
430 self.read(cx).tab_content(style, cx)
431 }
432
433 fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
434 self.read(cx).project_path(cx)
435 }
436
437 fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]> {
438 self.read(cx).project_entry_ids(cx)
439 }
440
441 fn is_singleton(&self, cx: &AppContext) -> bool {
442 self.read(cx).is_singleton(cx)
443 }
444
445 fn boxed_clone(&self) -> Box<dyn ItemHandle> {
446 Box::new(self.clone())
447 }
448
449 fn set_nav_history(&self, nav_history: Rc<RefCell<NavHistory>>, cx: &mut MutableAppContext) {
450 self.update(cx, |item, cx| {
451 item.set_nav_history(ItemNavHistory::new(nav_history, &cx.handle()), cx);
452 })
453 }
454
455 fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemHandle>> {
456 self.update(cx, |item, cx| {
457 cx.add_option_view(|cx| item.clone_on_split(cx))
458 })
459 .map(|handle| Box::new(handle) as Box<dyn ItemHandle>)
460 }
461
462 fn added_to_pane(
463 &self,
464 workspace: &mut Workspace,
465 pane: ViewHandle<Pane>,
466 cx: &mut ViewContext<Workspace>,
467 ) {
468 if let Some(followed_item) = self.to_followable_item_handle(cx) {
469 if let Some(message) = followed_item.to_state_proto(cx) {
470 workspace.update_followers(
471 proto::update_followers::Variant::CreateView(proto::View {
472 id: followed_item.id() as u64,
473 variant: Some(message),
474 leader_id: workspace.leader_for_pane(&pane).map(|id| id.0),
475 }),
476 cx,
477 );
478 }
479 }
480
481 let pending_update = Rc::new(RefCell::new(None));
482 let pending_update_scheduled = Rc::new(AtomicBool::new(false));
483 let pane = pane.downgrade();
484 cx.subscribe(self, move |workspace, item, event, cx| {
485 let pane = if let Some(pane) = pane.upgrade(cx) {
486 pane
487 } else {
488 log::error!("unexpected item event after pane was dropped");
489 return;
490 };
491
492 if let Some(item) = item.to_followable_item_handle(cx) {
493 let leader_id = workspace.leader_for_pane(&pane);
494
495 if leader_id.is_some() && item.should_unfollow_on_event(event, cx) {
496 workspace.unfollow(&pane, cx);
497 }
498
499 if item.add_event_to_update_proto(event, &mut *pending_update.borrow_mut(), cx)
500 && !pending_update_scheduled.load(SeqCst)
501 {
502 pending_update_scheduled.store(true, SeqCst);
503 cx.after_window_update({
504 let pending_update = pending_update.clone();
505 let pending_update_scheduled = pending_update_scheduled.clone();
506 move |this, cx| {
507 pending_update_scheduled.store(false, SeqCst);
508 this.update_followers(
509 proto::update_followers::Variant::UpdateView(proto::UpdateView {
510 id: item.id() as u64,
511 variant: pending_update.borrow_mut().take(),
512 leader_id: leader_id.map(|id| id.0),
513 }),
514 cx,
515 );
516 }
517 });
518 }
519 }
520
521 if T::should_close_item_on_event(event) {
522 Pane::close_item(workspace, pane, item.id(), cx).detach_and_log_err(cx);
523 return;
524 }
525
526 if T::should_activate_item_on_event(event) {
527 pane.update(cx, |pane, cx| {
528 if let Some(ix) = pane.index_for_item(&item) {
529 pane.activate_item(ix, true, true, cx);
530 pane.activate(cx);
531 }
532 });
533 }
534
535 if T::should_update_tab_on_event(event) {
536 pane.update(cx, |_, cx| {
537 cx.emit(pane::Event::ChangeItemTitle);
538 cx.notify();
539 });
540 }
541 })
542 .detach();
543 }
544
545 fn deactivated(&self, cx: &mut MutableAppContext) {
546 self.update(cx, |this, cx| this.deactivated(cx));
547 }
548
549 fn navigate(&self, data: Box<dyn Any>, cx: &mut MutableAppContext) -> bool {
550 self.update(cx, |this, cx| this.navigate(data, cx))
551 }
552
553 fn id(&self) -> usize {
554 self.id()
555 }
556
557 fn to_any(&self) -> AnyViewHandle {
558 self.into()
559 }
560
561 fn is_dirty(&self, cx: &AppContext) -> bool {
562 self.read(cx).is_dirty(cx)
563 }
564
565 fn has_conflict(&self, cx: &AppContext) -> bool {
566 self.read(cx).has_conflict(cx)
567 }
568
569 fn can_save(&self, cx: &AppContext) -> bool {
570 self.read(cx).can_save(cx)
571 }
572
573 fn save(&self, project: ModelHandle<Project>, cx: &mut MutableAppContext) -> Task<Result<()>> {
574 self.update(cx, |item, cx| item.save(project, cx))
575 }
576
577 fn save_as(
578 &self,
579 project: ModelHandle<Project>,
580 abs_path: PathBuf,
581 cx: &mut MutableAppContext,
582 ) -> Task<anyhow::Result<()>> {
583 self.update(cx, |item, cx| item.save_as(project, abs_path, cx))
584 }
585
586 fn reload(
587 &self,
588 project: ModelHandle<Project>,
589 cx: &mut MutableAppContext,
590 ) -> Task<Result<()>> {
591 self.update(cx, |item, cx| item.reload(project, cx))
592 }
593
594 fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option<AnyViewHandle> {
595 self.read(cx).act_as_type(type_id, self, cx)
596 }
597
598 fn to_followable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn FollowableItemHandle>> {
599 if cx.has_global::<FollowableItemBuilders>() {
600 let builders = cx.global::<FollowableItemBuilders>();
601 let item = self.to_any();
602 Some(builders.get(&item.view_type())?.1(item))
603 } else {
604 None
605 }
606 }
607
608 fn on_release(
609 &self,
610 cx: &mut MutableAppContext,
611 callback: Box<dyn FnOnce(&mut MutableAppContext)>,
612 ) -> gpui::Subscription {
613 cx.observe_release(self, move |_, cx| callback(cx))
614 }
615}
616
617impl Into<AnyViewHandle> for Box<dyn ItemHandle> {
618 fn into(self) -> AnyViewHandle {
619 self.to_any()
620 }
621}
622
623impl Clone for Box<dyn ItemHandle> {
624 fn clone(&self) -> Box<dyn ItemHandle> {
625 self.boxed_clone()
626 }
627}
628
629impl<T: Item> WeakItemHandle for WeakViewHandle<T> {
630 fn id(&self) -> usize {
631 self.id()
632 }
633
634 fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
635 self.upgrade(cx).map(|v| Box::new(v) as Box<dyn ItemHandle>)
636 }
637}
638
639pub trait Notification: View {
640 fn should_dismiss_notification_on_event(&self, event: &<Self as Entity>::Event) -> bool;
641}
642
643pub trait NotificationHandle {
644 fn id(&self) -> usize;
645 fn to_any(&self) -> AnyViewHandle;
646}
647
648impl<T: Notification> NotificationHandle for ViewHandle<T> {
649 fn id(&self) -> usize {
650 self.id()
651 }
652
653 fn to_any(&self) -> AnyViewHandle {
654 self.into()
655 }
656}
657
658impl Into<AnyViewHandle> for &dyn NotificationHandle {
659 fn into(self) -> AnyViewHandle {
660 self.to_any()
661 }
662}
663
664impl AppState {
665 #[cfg(any(test, feature = "test-support"))]
666 pub fn test(cx: &mut MutableAppContext) -> Arc<Self> {
667 let settings = Settings::test(cx);
668 cx.set_global(settings);
669
670 let fs = project::FakeFs::new(cx.background().clone());
671 let languages = Arc::new(LanguageRegistry::test());
672 let http_client = client::test::FakeHttpClient::with_404_response();
673 let client = Client::new(http_client.clone());
674 let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
675 let themes = ThemeRegistry::new((), cx.font_cache().clone());
676 Arc::new(Self {
677 client,
678 themes,
679 fs,
680 languages,
681 user_store,
682 initialize_workspace: |_, _, _| {},
683 build_window_options: || Default::default(),
684 })
685 }
686}
687
688pub enum Event {
689 PaneAdded(ViewHandle<Pane>),
690 ContactRequestedJoin(u64),
691}
692
693pub struct Workspace {
694 weak_self: WeakViewHandle<Self>,
695 client: Arc<Client>,
696 user_store: ModelHandle<client::UserStore>,
697 remote_entity_subscription: Option<Subscription>,
698 fs: Arc<dyn Fs>,
699 modal: Option<AnyViewHandle>,
700 center: PaneGroup,
701 left_sidebar: ViewHandle<Sidebar>,
702 right_sidebar: ViewHandle<Sidebar>,
703 panes: Vec<ViewHandle<Pane>>,
704 active_pane: ViewHandle<Pane>,
705 status_bar: ViewHandle<StatusBar>,
706 notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
707 project: ModelHandle<Project>,
708 leader_state: LeaderState,
709 follower_states_by_leader: FollowerStatesByLeader,
710 last_leaders_by_pane: HashMap<WeakViewHandle<Pane>, PeerId>,
711 _observe_current_user: Task<()>,
712}
713
714#[derive(Default)]
715struct LeaderState {
716 followers: HashSet<PeerId>,
717}
718
719type FollowerStatesByLeader = HashMap<PeerId, HashMap<ViewHandle<Pane>, FollowerState>>;
720
721#[derive(Default)]
722struct FollowerState {
723 active_view_id: Option<u64>,
724 items_by_leader_view_id: HashMap<u64, FollowerItem>,
725}
726
727#[derive(Debug)]
728enum FollowerItem {
729 Loading(Vec<proto::update_view::Variant>),
730 Loaded(Box<dyn FollowableItemHandle>),
731}
732
733impl Workspace {
734 pub fn new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
735 cx.observe(&project, |_, project, cx| {
736 if project.read(cx).is_read_only() {
737 cx.blur();
738 }
739 cx.notify()
740 })
741 .detach();
742
743 cx.subscribe(&project, move |this, project, event, cx| {
744 match event {
745 project::Event::RemoteIdChanged(remote_id) => {
746 this.project_remote_id_changed(*remote_id, cx);
747 }
748 project::Event::CollaboratorLeft(peer_id) => {
749 this.collaborator_left(*peer_id, cx);
750 }
751 project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded => {
752 this.update_window_title(cx);
753 }
754 _ => {}
755 }
756 if project.read(cx).is_read_only() {
757 cx.blur();
758 }
759 cx.notify()
760 })
761 .detach();
762
763 let pane = cx.add_view(|cx| Pane::new(cx));
764 let pane_id = pane.id();
765 cx.subscribe(&pane, move |this, _, event, cx| {
766 this.handle_pane_event(pane_id, event, cx)
767 })
768 .detach();
769 cx.focus(&pane);
770 cx.emit(Event::PaneAdded(pane.clone()));
771
772 let fs = project.read(cx).fs().clone();
773 let user_store = project.read(cx).user_store();
774 let client = project.read(cx).client();
775 let mut current_user = user_store.read(cx).watch_current_user().clone();
776 let mut connection_status = client.status().clone();
777 let _observe_current_user = cx.spawn_weak(|this, mut cx| async move {
778 current_user.recv().await;
779 connection_status.recv().await;
780 let mut stream =
781 Stream::map(current_user, drop).merge(Stream::map(connection_status, drop));
782
783 while stream.recv().await.is_some() {
784 cx.update(|cx| {
785 if let Some(this) = this.upgrade(cx) {
786 this.update(cx, |_, cx| cx.notify());
787 }
788 })
789 }
790 });
791
792 let weak_self = cx.weak_handle();
793
794 cx.emit_global(WorkspaceCreated(weak_self.clone()));
795
796 let left_sidebar = cx.add_view(|_| Sidebar::new(Side::Left));
797 let right_sidebar = cx.add_view(|_| Sidebar::new(Side::Right));
798 let left_sidebar_buttons = cx.add_view(|cx| SidebarButtons::new(left_sidebar.clone(), cx));
799 let right_sidebar_buttons =
800 cx.add_view(|cx| SidebarButtons::new(right_sidebar.clone(), cx));
801 let status_bar = cx.add_view(|cx| {
802 let mut status_bar = StatusBar::new(&pane.clone(), cx);
803 status_bar.add_left_item(left_sidebar_buttons, cx);
804 status_bar.add_right_item(right_sidebar_buttons, cx);
805 status_bar
806 });
807
808 let mut this = Workspace {
809 modal: None,
810 weak_self,
811 center: PaneGroup::new(pane.clone()),
812 panes: vec![pane.clone()],
813 active_pane: pane.clone(),
814 status_bar,
815 notifications: Default::default(),
816 client,
817 remote_entity_subscription: None,
818 user_store,
819 fs,
820 left_sidebar,
821 right_sidebar,
822 project,
823 leader_state: Default::default(),
824 follower_states_by_leader: Default::default(),
825 last_leaders_by_pane: Default::default(),
826 _observe_current_user,
827 };
828 this.project_remote_id_changed(this.project.read(cx).remote_id(), cx);
829
830 cx.defer(|this, cx| {
831 this.update_window_title(cx);
832 });
833
834 this
835 }
836
837 pub fn weak_handle(&self) -> WeakViewHandle<Self> {
838 self.weak_self.clone()
839 }
840
841 pub fn left_sidebar(&self) -> &ViewHandle<Sidebar> {
842 &self.left_sidebar
843 }
844
845 pub fn right_sidebar(&self) -> &ViewHandle<Sidebar> {
846 &self.right_sidebar
847 }
848
849 pub fn status_bar(&self) -> &ViewHandle<StatusBar> {
850 &self.status_bar
851 }
852
853 pub fn user_store(&self) -> &ModelHandle<UserStore> {
854 &self.user_store
855 }
856
857 pub fn project(&self) -> &ModelHandle<Project> {
858 &self.project
859 }
860
861 pub fn worktrees<'a>(
862 &self,
863 cx: &'a AppContext,
864 ) -> impl 'a + Iterator<Item = ModelHandle<Worktree>> {
865 self.project.read(cx).worktrees(cx)
866 }
867
868 pub fn contains_paths(&self, paths: &[PathBuf], cx: &AppContext) -> bool {
869 paths.iter().all(|path| self.contains_path(&path, cx))
870 }
871
872 pub fn contains_path(&self, path: &Path, cx: &AppContext) -> bool {
873 for worktree in self.worktrees(cx) {
874 let worktree = worktree.read(cx).as_local();
875 if worktree.map_or(false, |w| w.contains_abs_path(path)) {
876 return true;
877 }
878 }
879 false
880 }
881
882 pub fn worktree_scans_complete(&self, cx: &AppContext) -> impl Future<Output = ()> + 'static {
883 let futures = self
884 .worktrees(cx)
885 .filter_map(|worktree| worktree.read(cx).as_local())
886 .map(|worktree| worktree.scan_complete())
887 .collect::<Vec<_>>();
888 async move {
889 for future in futures {
890 future.await;
891 }
892 }
893 }
894
895 fn close(&mut self, _: &CloseWindow, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
896 let prepare = self.prepare_to_close(cx);
897 Some(cx.spawn(|this, mut cx| async move {
898 if prepare.await? {
899 this.update(&mut cx, |_, cx| {
900 let window_id = cx.window_id();
901 cx.remove_window(window_id);
902 });
903 }
904 Ok(())
905 }))
906 }
907
908 fn prepare_to_close(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<bool>> {
909 self.save_all_internal(true, cx)
910 }
911
912 fn save_all(&mut self, _: &SaveAll, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
913 let save_all = self.save_all_internal(false, cx);
914 Some(cx.foreground().spawn(async move {
915 save_all.await?;
916 Ok(())
917 }))
918 }
919
920 fn save_all_internal(
921 &mut self,
922 should_prompt_to_save: bool,
923 cx: &mut ViewContext<Self>,
924 ) -> Task<Result<bool>> {
925 let dirty_items = self
926 .panes
927 .iter()
928 .flat_map(|pane| {
929 pane.read(cx).items().filter_map(|item| {
930 if item.is_dirty(cx) {
931 Some((pane.clone(), item.boxed_clone()))
932 } else {
933 None
934 }
935 })
936 })
937 .collect::<Vec<_>>();
938
939 let project = self.project.clone();
940 cx.spawn_weak(|_, mut cx| async move {
941 // let mut saved_project_entry_ids = HashSet::default();
942 for (pane, item) in dirty_items {
943 let (is_singl, project_entry_ids) =
944 cx.read(|cx| (item.is_singleton(cx), item.project_entry_ids(cx)));
945 if is_singl || !project_entry_ids.is_empty() {
946 if let Some(ix) =
947 pane.read_with(&cx, |pane, _| pane.index_for_item(item.as_ref()))
948 {
949 if !Pane::save_item(
950 project.clone(),
951 &pane,
952 ix,
953 &item,
954 should_prompt_to_save,
955 &mut cx,
956 )
957 .await?
958 {
959 return Ok(false);
960 }
961 }
962 }
963 }
964 Ok(true)
965 })
966 }
967
968 pub fn open_paths(
969 &mut self,
970 mut abs_paths: Vec<PathBuf>,
971 cx: &mut ViewContext<Self>,
972 ) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, Arc<anyhow::Error>>>>> {
973 let fs = self.fs.clone();
974
975 // Sort the paths to ensure we add worktrees for parents before their children.
976 abs_paths.sort_unstable();
977 cx.spawn(|this, mut cx| async move {
978 let mut entries = Vec::new();
979 for path in &abs_paths {
980 entries.push(
981 this.update(&mut cx, |this, cx| this.project_path_for_path(path, cx))
982 .await
983 .ok(),
984 );
985 }
986
987 let tasks = abs_paths
988 .iter()
989 .cloned()
990 .zip(entries.into_iter())
991 .map(|(abs_path, project_path)| {
992 let this = this.clone();
993 cx.spawn(|mut cx| {
994 let fs = fs.clone();
995 async move {
996 let project_path = project_path?;
997 if fs.is_file(&abs_path).await {
998 Some(
999 this.update(&mut cx, |this, cx| {
1000 this.open_path(project_path, true, cx)
1001 })
1002 .await,
1003 )
1004 } else {
1005 None
1006 }
1007 }
1008 })
1009 })
1010 .collect::<Vec<_>>();
1011
1012 futures::future::join_all(tasks).await
1013 })
1014 }
1015
1016 fn add_folder_to_project(&mut self, _: &AddFolderToProject, cx: &mut ViewContext<Self>) {
1017 let mut paths = cx.prompt_for_paths(PathPromptOptions {
1018 files: false,
1019 directories: true,
1020 multiple: true,
1021 });
1022 cx.spawn(|this, mut cx| async move {
1023 if let Some(paths) = paths.recv().await.flatten() {
1024 let results = this
1025 .update(&mut cx, |this, cx| this.open_paths(paths, cx))
1026 .await;
1027 for result in results {
1028 if let Some(result) = result {
1029 result.log_err();
1030 }
1031 }
1032 }
1033 })
1034 .detach();
1035 }
1036
1037 fn project_path_for_path(
1038 &self,
1039 abs_path: &Path,
1040 cx: &mut ViewContext<Self>,
1041 ) -> Task<Result<ProjectPath>> {
1042 let entry = self.project().update(cx, |project, cx| {
1043 project.find_or_create_local_worktree(abs_path, true, cx)
1044 });
1045 cx.spawn(|_, cx| async move {
1046 let (worktree, path) = entry.await?;
1047 Ok(ProjectPath {
1048 worktree_id: worktree.read_with(&cx, |t, _| t.id()),
1049 path: path.into(),
1050 })
1051 })
1052 }
1053
1054 /// Returns the modal that was toggled closed if it was open.
1055 pub fn toggle_modal<V, F>(
1056 &mut self,
1057 cx: &mut ViewContext<Self>,
1058 add_view: F,
1059 ) -> Option<ViewHandle<V>>
1060 where
1061 V: 'static + View,
1062 F: FnOnce(&mut Self, &mut ViewContext<Self>) -> ViewHandle<V>,
1063 {
1064 cx.notify();
1065 // Whatever modal was visible is getting clobbered. If its the same type as V, then return
1066 // it. Otherwise, create a new modal and set it as active.
1067 let already_open_modal = self.modal.take().and_then(|modal| modal.downcast::<V>());
1068 if let Some(already_open_modal) = already_open_modal {
1069 cx.focus_self();
1070 Some(already_open_modal)
1071 } else {
1072 let modal = add_view(self, cx);
1073 cx.focus(&modal);
1074 self.modal = Some(modal.into());
1075 None
1076 }
1077 }
1078
1079 pub fn modal(&self) -> Option<&AnyViewHandle> {
1080 self.modal.as_ref()
1081 }
1082
1083 pub fn dismiss_modal(&mut self, cx: &mut ViewContext<Self>) {
1084 if self.modal.take().is_some() {
1085 cx.focus(&self.active_pane);
1086 cx.notify();
1087 }
1088 }
1089
1090 pub fn show_notification<V: Notification>(
1091 &mut self,
1092 id: usize,
1093 cx: &mut ViewContext<Self>,
1094 build_notification: impl FnOnce(&mut ViewContext<Self>) -> ViewHandle<V>,
1095 ) {
1096 let type_id = TypeId::of::<V>();
1097 if self
1098 .notifications
1099 .iter()
1100 .all(|(existing_type_id, existing_id, _)| {
1101 (*existing_type_id, *existing_id) != (type_id, id)
1102 })
1103 {
1104 let notification = build_notification(cx);
1105 cx.subscribe(¬ification, move |this, handle, event, cx| {
1106 if handle.read(cx).should_dismiss_notification_on_event(event) {
1107 this.dismiss_notification(type_id, id, cx);
1108 }
1109 })
1110 .detach();
1111 self.notifications
1112 .push((type_id, id, Box::new(notification)));
1113 cx.notify();
1114 }
1115 }
1116
1117 fn dismiss_notification(&mut self, type_id: TypeId, id: usize, cx: &mut ViewContext<Self>) {
1118 self.notifications
1119 .retain(|(existing_type_id, existing_id, _)| {
1120 if (*existing_type_id, *existing_id) == (type_id, id) {
1121 cx.notify();
1122 false
1123 } else {
1124 true
1125 }
1126 });
1127 }
1128
1129 pub fn items<'a>(
1130 &'a self,
1131 cx: &'a AppContext,
1132 ) -> impl 'a + Iterator<Item = &Box<dyn ItemHandle>> {
1133 self.panes.iter().flat_map(|pane| pane.read(cx).items())
1134 }
1135
1136 pub fn item_of_type<T: Item>(&self, cx: &AppContext) -> Option<ViewHandle<T>> {
1137 self.items_of_type(cx).max_by_key(|item| item.id())
1138 }
1139
1140 pub fn items_of_type<'a, T: Item>(
1141 &'a self,
1142 cx: &'a AppContext,
1143 ) -> impl 'a + Iterator<Item = ViewHandle<T>> {
1144 self.panes
1145 .iter()
1146 .flat_map(|pane| pane.read(cx).items_of_type())
1147 }
1148
1149 pub fn active_item(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
1150 self.active_pane().read(cx).active_item()
1151 }
1152
1153 fn active_project_path(&self, cx: &ViewContext<Self>) -> Option<ProjectPath> {
1154 self.active_item(cx).and_then(|item| item.project_path(cx))
1155 }
1156
1157 pub fn save_active_item(
1158 &mut self,
1159 force_name_change: bool,
1160 cx: &mut ViewContext<Self>,
1161 ) -> Task<Result<()>> {
1162 let project = self.project.clone();
1163 if let Some(item) = self.active_item(cx) {
1164 if !force_name_change && item.can_save(cx) {
1165 if item.has_conflict(cx.as_ref()) {
1166 const CONFLICT_MESSAGE: &'static str = "This file has changed on disk since you started editing it. Do you want to overwrite it?";
1167
1168 let mut answer = cx.prompt(
1169 PromptLevel::Warning,
1170 CONFLICT_MESSAGE,
1171 &["Overwrite", "Cancel"],
1172 );
1173 cx.spawn(|_, mut cx| async move {
1174 let answer = answer.recv().await;
1175 if answer == Some(0) {
1176 cx.update(|cx| item.save(project, cx)).await?;
1177 }
1178 Ok(())
1179 })
1180 } else {
1181 item.save(project, cx)
1182 }
1183 } else if item.is_singleton(cx) {
1184 let worktree = self.worktrees(cx).next();
1185 let start_abs_path = worktree
1186 .and_then(|w| w.read(cx).as_local())
1187 .map_or(Path::new(""), |w| w.abs_path())
1188 .to_path_buf();
1189 let mut abs_path = cx.prompt_for_new_path(&start_abs_path);
1190 cx.spawn(|_, mut cx| async move {
1191 if let Some(abs_path) = abs_path.recv().await.flatten() {
1192 cx.update(|cx| item.save_as(project, abs_path, cx)).await?;
1193 }
1194 Ok(())
1195 })
1196 } else {
1197 Task::ready(Ok(()))
1198 }
1199 } else {
1200 Task::ready(Ok(()))
1201 }
1202 }
1203
1204 pub fn toggle_sidebar_item(&mut self, action: &ToggleSidebarItem, cx: &mut ViewContext<Self>) {
1205 let sidebar = match action.side {
1206 Side::Left => &mut self.left_sidebar,
1207 Side::Right => &mut self.right_sidebar,
1208 };
1209 let active_item = sidebar.update(cx, |sidebar, cx| {
1210 sidebar.toggle_item(action.item_index, cx);
1211 sidebar.active_item().map(|item| item.to_any())
1212 });
1213 if let Some(active_item) = active_item {
1214 cx.focus(active_item);
1215 } else {
1216 cx.focus_self();
1217 }
1218 cx.notify();
1219 }
1220
1221 pub fn toggle_sidebar_item_focus(
1222 &mut self,
1223 action: &ToggleSidebarItemFocus,
1224 cx: &mut ViewContext<Self>,
1225 ) {
1226 let sidebar = match action.side {
1227 Side::Left => &mut self.left_sidebar,
1228 Side::Right => &mut self.right_sidebar,
1229 };
1230 let active_item = sidebar.update(cx, |sidebar, cx| {
1231 sidebar.activate_item(action.item_index, cx);
1232 sidebar.active_item().cloned()
1233 });
1234 if let Some(active_item) = active_item {
1235 if active_item.is_focused(cx) {
1236 cx.focus_self();
1237 } else {
1238 cx.focus(active_item.to_any());
1239 }
1240 }
1241 cx.notify();
1242 }
1243
1244 fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
1245 let pane = cx.add_view(|cx| Pane::new(cx));
1246 let pane_id = pane.id();
1247 cx.subscribe(&pane, move |this, _, event, cx| {
1248 this.handle_pane_event(pane_id, event, cx)
1249 })
1250 .detach();
1251 self.panes.push(pane.clone());
1252 self.activate_pane(pane.clone(), cx);
1253 cx.emit(Event::PaneAdded(pane.clone()));
1254 pane
1255 }
1256
1257 pub fn add_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
1258 let pane = self.active_pane().clone();
1259 Pane::add_item(self, pane, item, true, true, cx);
1260 }
1261
1262 pub fn open_path(
1263 &mut self,
1264 path: impl Into<ProjectPath>,
1265 focus_item: bool,
1266 cx: &mut ViewContext<Self>,
1267 ) -> Task<Result<Box<dyn ItemHandle>, Arc<anyhow::Error>>> {
1268 let pane = self.active_pane().downgrade();
1269 let task = self.load_path(path.into(), cx);
1270 cx.spawn(|this, mut cx| async move {
1271 let (project_entry_id, build_item) = task.await?;
1272 let pane = pane
1273 .upgrade(&cx)
1274 .ok_or_else(|| anyhow!("pane was closed"))?;
1275 this.update(&mut cx, |this, cx| {
1276 Ok(Pane::open_item(
1277 this,
1278 pane,
1279 project_entry_id,
1280 focus_item,
1281 cx,
1282 build_item,
1283 ))
1284 })
1285 })
1286 }
1287
1288 pub(crate) fn load_path(
1289 &mut self,
1290 path: ProjectPath,
1291 cx: &mut ViewContext<Self>,
1292 ) -> Task<
1293 Result<(
1294 ProjectEntryId,
1295 impl 'static + FnOnce(&mut MutableAppContext) -> Box<dyn ItemHandle>,
1296 )>,
1297 > {
1298 let project = self.project().clone();
1299 let project_item = project.update(cx, |project, cx| project.open_path(path, cx));
1300 let window_id = cx.window_id();
1301 cx.as_mut().spawn(|mut cx| async move {
1302 let (project_entry_id, project_item) = project_item.await?;
1303 let build_item = cx.update(|cx| {
1304 cx.default_global::<ProjectItemBuilders>()
1305 .get(&project_item.model_type())
1306 .ok_or_else(|| anyhow!("no item builder for project item"))
1307 .cloned()
1308 })?;
1309 let build_item =
1310 move |cx: &mut MutableAppContext| build_item(window_id, project, project_item, cx);
1311 Ok((project_entry_id, build_item))
1312 })
1313 }
1314
1315 pub fn open_project_item<T>(
1316 &mut self,
1317 project_item: ModelHandle<T::Item>,
1318 cx: &mut ViewContext<Self>,
1319 ) -> ViewHandle<T>
1320 where
1321 T: ProjectItem,
1322 {
1323 use project::Item as _;
1324
1325 let entry_id = project_item.read(cx).entry_id(cx);
1326 if let Some(item) = entry_id
1327 .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id, cx))
1328 .and_then(|item| item.downcast())
1329 {
1330 self.activate_item(&item, cx);
1331 return item;
1332 }
1333
1334 let item = cx.add_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
1335 self.add_item(Box::new(item.clone()), cx);
1336 item
1337 }
1338
1339 pub fn activate_item(&mut self, item: &dyn ItemHandle, cx: &mut ViewContext<Self>) -> bool {
1340 let result = self.panes.iter().find_map(|pane| {
1341 if let Some(ix) = pane.read(cx).index_for_item(item) {
1342 Some((pane.clone(), ix))
1343 } else {
1344 None
1345 }
1346 });
1347 if let Some((pane, ix)) = result {
1348 self.activate_pane(pane.clone(), cx);
1349 pane.update(cx, |pane, cx| pane.activate_item(ix, true, true, cx));
1350 true
1351 } else {
1352 false
1353 }
1354 }
1355
1356 pub fn activate_next_pane(&mut self, cx: &mut ViewContext<Self>) {
1357 let next_pane = {
1358 let panes = self.center.panes();
1359 let ix = panes
1360 .iter()
1361 .position(|pane| **pane == self.active_pane)
1362 .unwrap();
1363 let next_ix = (ix + 1) % panes.len();
1364 panes[next_ix].clone()
1365 };
1366 self.activate_pane(next_pane, cx);
1367 }
1368
1369 pub fn activate_previous_pane(&mut self, cx: &mut ViewContext<Self>) {
1370 let prev_pane = {
1371 let panes = self.center.panes();
1372 let ix = panes
1373 .iter()
1374 .position(|pane| **pane == self.active_pane)
1375 .unwrap();
1376 let prev_ix = if ix == 0 { panes.len() - 1 } else { ix - 1 };
1377 panes[prev_ix].clone()
1378 };
1379 self.activate_pane(prev_pane, cx);
1380 }
1381
1382 fn activate_pane(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
1383 if self.active_pane != pane {
1384 self.active_pane = pane.clone();
1385 self.status_bar.update(cx, |status_bar, cx| {
1386 status_bar.set_active_pane(&self.active_pane, cx);
1387 });
1388 self.active_item_path_changed(cx);
1389 cx.focus(&self.active_pane);
1390 cx.notify();
1391 }
1392
1393 self.update_followers(
1394 proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView {
1395 id: self.active_item(cx).map(|item| item.id() as u64),
1396 leader_id: self.leader_for_pane(&pane).map(|id| id.0),
1397 }),
1398 cx,
1399 );
1400 }
1401
1402 fn handle_pane_event(
1403 &mut self,
1404 pane_id: usize,
1405 event: &pane::Event,
1406 cx: &mut ViewContext<Self>,
1407 ) {
1408 if let Some(pane) = self.pane(pane_id) {
1409 match event {
1410 pane::Event::Split(direction) => {
1411 self.split_pane(pane, *direction, cx);
1412 }
1413 pane::Event::Remove => {
1414 self.remove_pane(pane, cx);
1415 }
1416 pane::Event::Activate => {
1417 self.activate_pane(pane, cx);
1418 }
1419 pane::Event::ActivateItem { local } => {
1420 if *local {
1421 self.unfollow(&pane, cx);
1422 }
1423 if pane == self.active_pane {
1424 self.active_item_path_changed(cx);
1425 }
1426 }
1427 pane::Event::ChangeItemTitle => {
1428 if pane == self.active_pane {
1429 self.active_item_path_changed(cx);
1430 }
1431 }
1432 }
1433 } else {
1434 error!("pane {} not found", pane_id);
1435 }
1436 }
1437
1438 pub fn split_pane(
1439 &mut self,
1440 pane: ViewHandle<Pane>,
1441 direction: SplitDirection,
1442 cx: &mut ViewContext<Self>,
1443 ) -> ViewHandle<Pane> {
1444 let new_pane = self.add_pane(cx);
1445 self.activate_pane(new_pane.clone(), cx);
1446 if let Some(item) = pane.read(cx).active_item() {
1447 if let Some(clone) = item.clone_on_split(cx.as_mut()) {
1448 Pane::add_item(self, new_pane.clone(), clone, true, true, cx);
1449 }
1450 }
1451 self.center.split(&pane, &new_pane, direction).unwrap();
1452 cx.notify();
1453 new_pane
1454 }
1455
1456 fn remove_pane(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
1457 if self.center.remove(&pane).unwrap() {
1458 self.panes.retain(|p| p != &pane);
1459 self.activate_pane(self.panes.last().unwrap().clone(), cx);
1460 self.unfollow(&pane, cx);
1461 self.last_leaders_by_pane.remove(&pane.downgrade());
1462 cx.notify();
1463 } else {
1464 self.active_item_path_changed(cx);
1465 }
1466 }
1467
1468 pub fn panes(&self) -> &[ViewHandle<Pane>] {
1469 &self.panes
1470 }
1471
1472 fn pane(&self, pane_id: usize) -> Option<ViewHandle<Pane>> {
1473 self.panes.iter().find(|pane| pane.id() == pane_id).cloned()
1474 }
1475
1476 pub fn active_pane(&self) -> &ViewHandle<Pane> {
1477 &self.active_pane
1478 }
1479
1480 fn project_remote_id_changed(&mut self, remote_id: Option<u64>, cx: &mut ViewContext<Self>) {
1481 if let Some(remote_id) = remote_id {
1482 self.remote_entity_subscription =
1483 Some(self.client.add_view_for_remote_entity(remote_id, cx));
1484 } else {
1485 self.remote_entity_subscription.take();
1486 }
1487 }
1488
1489 fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
1490 self.leader_state.followers.remove(&peer_id);
1491 if let Some(states_by_pane) = self.follower_states_by_leader.remove(&peer_id) {
1492 for state in states_by_pane.into_values() {
1493 for item in state.items_by_leader_view_id.into_values() {
1494 if let FollowerItem::Loaded(item) = item {
1495 item.set_leader_replica_id(None, cx);
1496 }
1497 }
1498 }
1499 }
1500 cx.notify();
1501 }
1502
1503 pub fn toggle_follow(
1504 &mut self,
1505 ToggleFollow(leader_id): &ToggleFollow,
1506 cx: &mut ViewContext<Self>,
1507 ) -> Option<Task<Result<()>>> {
1508 let leader_id = *leader_id;
1509 let pane = self.active_pane().clone();
1510
1511 if let Some(prev_leader_id) = self.unfollow(&pane, cx) {
1512 if leader_id == prev_leader_id {
1513 return None;
1514 }
1515 }
1516
1517 self.last_leaders_by_pane
1518 .insert(pane.downgrade(), leader_id);
1519 self.follower_states_by_leader
1520 .entry(leader_id)
1521 .or_default()
1522 .insert(pane.clone(), Default::default());
1523 cx.notify();
1524
1525 let project_id = self.project.read(cx).remote_id()?;
1526 let request = self.client.request(proto::Follow {
1527 project_id,
1528 leader_id: leader_id.0,
1529 });
1530 Some(cx.spawn_weak(|this, mut cx| async move {
1531 let response = request.await?;
1532 if let Some(this) = this.upgrade(&cx) {
1533 this.update(&mut cx, |this, _| {
1534 let state = this
1535 .follower_states_by_leader
1536 .get_mut(&leader_id)
1537 .and_then(|states_by_pane| states_by_pane.get_mut(&pane))
1538 .ok_or_else(|| anyhow!("following interrupted"))?;
1539 state.active_view_id = response.active_view_id;
1540 Ok::<_, anyhow::Error>(())
1541 })?;
1542 Self::add_views_from_leader(this, leader_id, vec![pane], response.views, &mut cx)
1543 .await?;
1544 }
1545 Ok(())
1546 }))
1547 }
1548
1549 pub fn follow_next_collaborator(
1550 &mut self,
1551 _: &FollowNextCollaborator,
1552 cx: &mut ViewContext<Self>,
1553 ) -> Option<Task<Result<()>>> {
1554 let collaborators = self.project.read(cx).collaborators();
1555 let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
1556 let mut collaborators = collaborators.keys().copied();
1557 while let Some(peer_id) = collaborators.next() {
1558 if peer_id == leader_id {
1559 break;
1560 }
1561 }
1562 collaborators.next()
1563 } else if let Some(last_leader_id) =
1564 self.last_leaders_by_pane.get(&self.active_pane.downgrade())
1565 {
1566 if collaborators.contains_key(last_leader_id) {
1567 Some(*last_leader_id)
1568 } else {
1569 None
1570 }
1571 } else {
1572 None
1573 };
1574
1575 next_leader_id
1576 .or_else(|| collaborators.keys().copied().next())
1577 .and_then(|leader_id| self.toggle_follow(&ToggleFollow(leader_id), cx))
1578 }
1579
1580 pub fn unfollow(
1581 &mut self,
1582 pane: &ViewHandle<Pane>,
1583 cx: &mut ViewContext<Self>,
1584 ) -> Option<PeerId> {
1585 for (leader_id, states_by_pane) in &mut self.follower_states_by_leader {
1586 let leader_id = *leader_id;
1587 if let Some(state) = states_by_pane.remove(&pane) {
1588 for (_, item) in state.items_by_leader_view_id {
1589 if let FollowerItem::Loaded(item) = item {
1590 item.set_leader_replica_id(None, cx);
1591 }
1592 }
1593
1594 if states_by_pane.is_empty() {
1595 self.follower_states_by_leader.remove(&leader_id);
1596 if let Some(project_id) = self.project.read(cx).remote_id() {
1597 self.client
1598 .send(proto::Unfollow {
1599 project_id,
1600 leader_id: leader_id.0,
1601 })
1602 .log_err();
1603 }
1604 }
1605
1606 cx.notify();
1607 return Some(leader_id);
1608 }
1609 }
1610 None
1611 }
1612
1613 fn render_connection_status(&self, cx: &mut RenderContext<Self>) -> Option<ElementBox> {
1614 let theme = &cx.global::<Settings>().theme;
1615 match &*self.client.status().borrow() {
1616 client::Status::ConnectionError
1617 | client::Status::ConnectionLost
1618 | client::Status::Reauthenticating
1619 | client::Status::Reconnecting { .. }
1620 | client::Status::ReconnectionError { .. } => Some(
1621 Container::new(
1622 Align::new(
1623 ConstrainedBox::new(
1624 Svg::new("icons/offline-14.svg")
1625 .with_color(theme.workspace.titlebar.offline_icon.color)
1626 .boxed(),
1627 )
1628 .with_width(theme.workspace.titlebar.offline_icon.width)
1629 .boxed(),
1630 )
1631 .boxed(),
1632 )
1633 .with_style(theme.workspace.titlebar.offline_icon.container)
1634 .boxed(),
1635 ),
1636 client::Status::UpgradeRequired => Some(
1637 Label::new(
1638 "Please update Zed to collaborate".to_string(),
1639 theme.workspace.titlebar.outdated_warning.text.clone(),
1640 )
1641 .contained()
1642 .with_style(theme.workspace.titlebar.outdated_warning.container)
1643 .aligned()
1644 .boxed(),
1645 ),
1646 _ => None,
1647 }
1648 }
1649
1650 fn render_titlebar(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
1651 let mut worktree_root_names = String::new();
1652 self.worktree_root_names(&mut worktree_root_names, cx);
1653
1654 ConstrainedBox::new(
1655 Container::new(
1656 Stack::new()
1657 .with_child(
1658 Label::new(worktree_root_names, theme.workspace.titlebar.title.clone())
1659 .aligned()
1660 .left()
1661 .boxed(),
1662 )
1663 .with_child(
1664 Align::new(
1665 Flex::row()
1666 .with_children(self.render_collaborators(theme, cx))
1667 .with_children(self.render_current_user(
1668 self.user_store.read(cx).current_user().as_ref(),
1669 self.project.read(cx).replica_id(),
1670 theme,
1671 cx,
1672 ))
1673 .with_children(self.render_connection_status(cx))
1674 .boxed(),
1675 )
1676 .right()
1677 .boxed(),
1678 )
1679 .boxed(),
1680 )
1681 .with_style(theme.workspace.titlebar.container)
1682 .boxed(),
1683 )
1684 .with_height(theme.workspace.titlebar.height)
1685 .named("titlebar")
1686 }
1687
1688 fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
1689 let active_entry = self.active_project_path(cx);
1690 self.project
1691 .update(cx, |project, cx| project.set_active_path(active_entry, cx));
1692 self.update_window_title(cx);
1693 }
1694
1695 fn update_window_title(&mut self, cx: &mut ViewContext<Self>) {
1696 let mut title = String::new();
1697 if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
1698 let filename = path
1699 .path
1700 .file_name()
1701 .map(|s| s.to_string_lossy())
1702 .or_else(|| {
1703 Some(Cow::Borrowed(
1704 self.project()
1705 .read(cx)
1706 .worktree_for_id(path.worktree_id, cx)?
1707 .read(cx)
1708 .root_name(),
1709 ))
1710 });
1711 if let Some(filename) = filename {
1712 title.push_str(filename.as_ref());
1713 title.push_str(" — ");
1714 }
1715 }
1716 self.worktree_root_names(&mut title, cx);
1717 if title.is_empty() {
1718 title = "empty project".to_string();
1719 }
1720 cx.set_window_title(&title);
1721 }
1722
1723 fn worktree_root_names(&self, string: &mut String, cx: &mut MutableAppContext) {
1724 for (i, worktree) in self.project.read(cx).visible_worktrees(cx).enumerate() {
1725 if i != 0 {
1726 string.push_str(", ");
1727 }
1728 string.push_str(worktree.read(cx).root_name());
1729 }
1730 }
1731
1732 fn render_collaborators(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> Vec<ElementBox> {
1733 let mut collaborators = self
1734 .project
1735 .read(cx)
1736 .collaborators()
1737 .values()
1738 .cloned()
1739 .collect::<Vec<_>>();
1740 collaborators.sort_unstable_by_key(|collaborator| collaborator.replica_id);
1741 collaborators
1742 .into_iter()
1743 .filter_map(|collaborator| {
1744 Some(self.render_avatar(
1745 collaborator.user.avatar.clone()?,
1746 collaborator.replica_id,
1747 Some(collaborator.peer_id),
1748 theme,
1749 cx,
1750 ))
1751 })
1752 .collect()
1753 }
1754
1755 fn render_current_user(
1756 &self,
1757 user: Option<&Arc<User>>,
1758 replica_id: ReplicaId,
1759 theme: &Theme,
1760 cx: &mut RenderContext<Self>,
1761 ) -> Option<ElementBox> {
1762 let status = *self.client.status().borrow();
1763 if let Some(avatar) = user.and_then(|user| user.avatar.clone()) {
1764 Some(self.render_avatar(avatar, replica_id, None, theme, cx))
1765 } else if matches!(status, client::Status::UpgradeRequired) {
1766 None
1767 } else {
1768 Some(
1769 MouseEventHandler::new::<Authenticate, _, _>(0, cx, |state, _| {
1770 let style = theme
1771 .workspace
1772 .titlebar
1773 .sign_in_prompt
1774 .style_for(state, false);
1775 Label::new("Sign in".to_string(), style.text.clone())
1776 .contained()
1777 .with_style(style.container)
1778 .boxed()
1779 })
1780 .on_click(|_, cx| cx.dispatch_action(Authenticate))
1781 .with_cursor_style(CursorStyle::PointingHand)
1782 .aligned()
1783 .boxed(),
1784 )
1785 }
1786 }
1787
1788 fn render_avatar(
1789 &self,
1790 avatar: Arc<ImageData>,
1791 replica_id: ReplicaId,
1792 peer_id: Option<PeerId>,
1793 theme: &Theme,
1794 cx: &mut RenderContext<Self>,
1795 ) -> ElementBox {
1796 let replica_color = theme.editor.replica_selection_style(replica_id).cursor;
1797 let is_followed = peer_id.map_or(false, |peer_id| {
1798 self.follower_states_by_leader.contains_key(&peer_id)
1799 });
1800 let mut avatar_style = theme.workspace.titlebar.avatar;
1801 if is_followed {
1802 avatar_style.border = Border::all(1.0, replica_color);
1803 }
1804 let content = Stack::new()
1805 .with_child(
1806 Image::new(avatar)
1807 .with_style(avatar_style)
1808 .constrained()
1809 .with_width(theme.workspace.titlebar.avatar_width)
1810 .aligned()
1811 .boxed(),
1812 )
1813 .with_child(
1814 AvatarRibbon::new(replica_color)
1815 .constrained()
1816 .with_width(theme.workspace.titlebar.avatar_ribbon.width)
1817 .with_height(theme.workspace.titlebar.avatar_ribbon.height)
1818 .aligned()
1819 .bottom()
1820 .boxed(),
1821 )
1822 .constrained()
1823 .with_width(theme.workspace.titlebar.avatar_width)
1824 .contained()
1825 .with_margin_left(theme.workspace.titlebar.avatar_margin)
1826 .boxed();
1827
1828 if let Some(peer_id) = peer_id {
1829 MouseEventHandler::new::<ToggleFollow, _, _>(replica_id.into(), cx, move |_, _| content)
1830 .with_cursor_style(CursorStyle::PointingHand)
1831 .on_click(move |_, cx| cx.dispatch_action(ToggleFollow(peer_id)))
1832 .boxed()
1833 } else {
1834 content
1835 }
1836 }
1837
1838 fn render_disconnected_overlay(&self, cx: &AppContext) -> Option<ElementBox> {
1839 if self.project.read(cx).is_read_only() {
1840 let theme = &cx.global::<Settings>().theme;
1841 Some(
1842 EventHandler::new(
1843 Label::new(
1844 "Your connection to the remote project has been lost.".to_string(),
1845 theme.workspace.disconnected_overlay.text.clone(),
1846 )
1847 .aligned()
1848 .contained()
1849 .with_style(theme.workspace.disconnected_overlay.container)
1850 .boxed(),
1851 )
1852 .capture(|_, _, _| true)
1853 .boxed(),
1854 )
1855 } else {
1856 None
1857 }
1858 }
1859
1860 fn render_notifications(&self, theme: &theme::Workspace) -> Option<ElementBox> {
1861 if self.notifications.is_empty() {
1862 None
1863 } else {
1864 Some(
1865 Flex::column()
1866 .with_children(self.notifications.iter().map(|(_, _, notification)| {
1867 ChildView::new(notification.as_ref())
1868 .contained()
1869 .with_style(theme.notification)
1870 .boxed()
1871 }))
1872 .constrained()
1873 .with_width(theme.notifications.width)
1874 .contained()
1875 .with_style(theme.notifications.container)
1876 .aligned()
1877 .bottom()
1878 .right()
1879 .boxed(),
1880 )
1881 }
1882 }
1883
1884 // RPC handlers
1885
1886 async fn handle_follow(
1887 this: ViewHandle<Self>,
1888 envelope: TypedEnvelope<proto::Follow>,
1889 _: Arc<Client>,
1890 mut cx: AsyncAppContext,
1891 ) -> Result<proto::FollowResponse> {
1892 this.update(&mut cx, |this, cx| {
1893 this.leader_state
1894 .followers
1895 .insert(envelope.original_sender_id()?);
1896
1897 let active_view_id = this
1898 .active_item(cx)
1899 .and_then(|i| i.to_followable_item_handle(cx))
1900 .map(|i| i.id() as u64);
1901 Ok(proto::FollowResponse {
1902 active_view_id,
1903 views: this
1904 .panes()
1905 .iter()
1906 .flat_map(|pane| {
1907 let leader_id = this.leader_for_pane(pane).map(|id| id.0);
1908 pane.read(cx).items().filter_map({
1909 let cx = &cx;
1910 move |item| {
1911 let id = item.id() as u64;
1912 let item = item.to_followable_item_handle(cx)?;
1913 let variant = item.to_state_proto(cx)?;
1914 Some(proto::View {
1915 id,
1916 leader_id,
1917 variant: Some(variant),
1918 })
1919 }
1920 })
1921 })
1922 .collect(),
1923 })
1924 })
1925 }
1926
1927 async fn handle_unfollow(
1928 this: ViewHandle<Self>,
1929 envelope: TypedEnvelope<proto::Unfollow>,
1930 _: Arc<Client>,
1931 mut cx: AsyncAppContext,
1932 ) -> Result<()> {
1933 this.update(&mut cx, |this, _| {
1934 this.leader_state
1935 .followers
1936 .remove(&envelope.original_sender_id()?);
1937 Ok(())
1938 })
1939 }
1940
1941 async fn handle_update_followers(
1942 this: ViewHandle<Self>,
1943 envelope: TypedEnvelope<proto::UpdateFollowers>,
1944 _: Arc<Client>,
1945 mut cx: AsyncAppContext,
1946 ) -> Result<()> {
1947 let leader_id = envelope.original_sender_id()?;
1948 match envelope
1949 .payload
1950 .variant
1951 .ok_or_else(|| anyhow!("invalid update"))?
1952 {
1953 proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
1954 this.update(&mut cx, |this, cx| {
1955 this.update_leader_state(leader_id, cx, |state, _| {
1956 state.active_view_id = update_active_view.id;
1957 });
1958 Ok::<_, anyhow::Error>(())
1959 })
1960 }
1961 proto::update_followers::Variant::UpdateView(update_view) => {
1962 this.update(&mut cx, |this, cx| {
1963 let variant = update_view
1964 .variant
1965 .ok_or_else(|| anyhow!("missing update view variant"))?;
1966 this.update_leader_state(leader_id, cx, |state, cx| {
1967 let variant = variant.clone();
1968 match state
1969 .items_by_leader_view_id
1970 .entry(update_view.id)
1971 .or_insert(FollowerItem::Loading(Vec::new()))
1972 {
1973 FollowerItem::Loaded(item) => {
1974 item.apply_update_proto(variant, cx).log_err();
1975 }
1976 FollowerItem::Loading(updates) => updates.push(variant),
1977 }
1978 });
1979 Ok(())
1980 })
1981 }
1982 proto::update_followers::Variant::CreateView(view) => {
1983 let panes = this.read_with(&cx, |this, _| {
1984 this.follower_states_by_leader
1985 .get(&leader_id)
1986 .into_iter()
1987 .flat_map(|states_by_pane| states_by_pane.keys())
1988 .cloned()
1989 .collect()
1990 });
1991 Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], &mut cx)
1992 .await?;
1993 Ok(())
1994 }
1995 }
1996 .log_err();
1997
1998 Ok(())
1999 }
2000
2001 async fn add_views_from_leader(
2002 this: ViewHandle<Self>,
2003 leader_id: PeerId,
2004 panes: Vec<ViewHandle<Pane>>,
2005 views: Vec<proto::View>,
2006 cx: &mut AsyncAppContext,
2007 ) -> Result<()> {
2008 let project = this.read_with(cx, |this, _| this.project.clone());
2009 let replica_id = project
2010 .read_with(cx, |project, _| {
2011 project
2012 .collaborators()
2013 .get(&leader_id)
2014 .map(|c| c.replica_id)
2015 })
2016 .ok_or_else(|| anyhow!("no such collaborator {}", leader_id))?;
2017
2018 let item_builders = cx.update(|cx| {
2019 cx.default_global::<FollowableItemBuilders>()
2020 .values()
2021 .map(|b| b.0)
2022 .collect::<Vec<_>>()
2023 .clone()
2024 });
2025
2026 let mut item_tasks_by_pane = HashMap::default();
2027 for pane in panes {
2028 let mut item_tasks = Vec::new();
2029 let mut leader_view_ids = Vec::new();
2030 for view in &views {
2031 let mut variant = view.variant.clone();
2032 if variant.is_none() {
2033 Err(anyhow!("missing variant"))?;
2034 }
2035 for build_item in &item_builders {
2036 let task =
2037 cx.update(|cx| build_item(pane.clone(), project.clone(), &mut variant, cx));
2038 if let Some(task) = task {
2039 item_tasks.push(task);
2040 leader_view_ids.push(view.id);
2041 break;
2042 } else {
2043 assert!(variant.is_some());
2044 }
2045 }
2046 }
2047
2048 item_tasks_by_pane.insert(pane, (item_tasks, leader_view_ids));
2049 }
2050
2051 for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane {
2052 let items = futures::future::try_join_all(item_tasks).await?;
2053 this.update(cx, |this, cx| {
2054 let state = this
2055 .follower_states_by_leader
2056 .get_mut(&leader_id)?
2057 .get_mut(&pane)?;
2058
2059 for (id, item) in leader_view_ids.into_iter().zip(items) {
2060 item.set_leader_replica_id(Some(replica_id), cx);
2061 match state.items_by_leader_view_id.entry(id) {
2062 hash_map::Entry::Occupied(e) => {
2063 let e = e.into_mut();
2064 if let FollowerItem::Loading(updates) = e {
2065 for update in updates.drain(..) {
2066 item.apply_update_proto(update, cx)
2067 .context("failed to apply view update")
2068 .log_err();
2069 }
2070 }
2071 *e = FollowerItem::Loaded(item);
2072 }
2073 hash_map::Entry::Vacant(e) => {
2074 e.insert(FollowerItem::Loaded(item));
2075 }
2076 }
2077 }
2078
2079 Some(())
2080 });
2081 }
2082 this.update(cx, |this, cx| this.leader_updated(leader_id, cx));
2083
2084 Ok(())
2085 }
2086
2087 fn update_followers(
2088 &self,
2089 update: proto::update_followers::Variant,
2090 cx: &AppContext,
2091 ) -> Option<()> {
2092 let project_id = self.project.read(cx).remote_id()?;
2093 if !self.leader_state.followers.is_empty() {
2094 self.client
2095 .send(proto::UpdateFollowers {
2096 project_id,
2097 follower_ids: self.leader_state.followers.iter().map(|f| f.0).collect(),
2098 variant: Some(update),
2099 })
2100 .log_err();
2101 }
2102 None
2103 }
2104
2105 pub fn leader_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<PeerId> {
2106 self.follower_states_by_leader
2107 .iter()
2108 .find_map(|(leader_id, state)| {
2109 if state.contains_key(pane) {
2110 Some(*leader_id)
2111 } else {
2112 None
2113 }
2114 })
2115 }
2116
2117 fn update_leader_state(
2118 &mut self,
2119 leader_id: PeerId,
2120 cx: &mut ViewContext<Self>,
2121 mut update_fn: impl FnMut(&mut FollowerState, &mut ViewContext<Self>),
2122 ) {
2123 for (_, state) in self
2124 .follower_states_by_leader
2125 .get_mut(&leader_id)
2126 .into_iter()
2127 .flatten()
2128 {
2129 update_fn(state, cx);
2130 }
2131 self.leader_updated(leader_id, cx);
2132 }
2133
2134 fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
2135 let mut items_to_add = Vec::new();
2136 for (pane, state) in self.follower_states_by_leader.get(&leader_id)? {
2137 if let Some(active_item) = state
2138 .active_view_id
2139 .and_then(|id| state.items_by_leader_view_id.get(&id))
2140 {
2141 if let FollowerItem::Loaded(item) = active_item {
2142 items_to_add.push((pane.clone(), item.boxed_clone()));
2143 }
2144 }
2145 }
2146
2147 for (pane, item) in items_to_add {
2148 Pane::add_item(self, pane.clone(), item.boxed_clone(), false, false, cx);
2149 if pane == self.active_pane {
2150 pane.update(cx, |pane, cx| pane.focus_active_item(cx));
2151 }
2152 cx.notify();
2153 }
2154 None
2155 }
2156}
2157
2158impl Entity for Workspace {
2159 type Event = Event;
2160}
2161
2162impl View for Workspace {
2163 fn ui_name() -> &'static str {
2164 "Workspace"
2165 }
2166
2167 fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
2168 let theme = cx.global::<Settings>().theme.clone();
2169 Stack::new()
2170 .with_child(
2171 Flex::column()
2172 .with_child(self.render_titlebar(&theme, cx))
2173 .with_child(
2174 Stack::new()
2175 .with_child({
2176 Flex::row()
2177 .with_children(
2178 if self.left_sidebar.read(cx).active_item().is_some() {
2179 Some(
2180 ChildView::new(&self.left_sidebar)
2181 .flex(0.8, false)
2182 .boxed(),
2183 )
2184 } else {
2185 None
2186 },
2187 )
2188 .with_child(
2189 FlexItem::new(self.center.render(
2190 &theme,
2191 &self.follower_states_by_leader,
2192 self.project.read(cx).collaborators(),
2193 ))
2194 .flex(1., true)
2195 .boxed(),
2196 )
2197 .with_children(
2198 if self.right_sidebar.read(cx).active_item().is_some() {
2199 Some(
2200 ChildView::new(&self.right_sidebar)
2201 .flex(0.8, false)
2202 .boxed(),
2203 )
2204 } else {
2205 None
2206 },
2207 )
2208 .boxed()
2209 })
2210 .with_children(self.modal.as_ref().map(|m| {
2211 ChildView::new(m)
2212 .contained()
2213 .with_style(theme.workspace.modal)
2214 .aligned()
2215 .top()
2216 .boxed()
2217 }))
2218 .with_children(self.render_notifications(&theme.workspace))
2219 .flex(1.0, true)
2220 .boxed(),
2221 )
2222 .with_child(ChildView::new(&self.status_bar).boxed())
2223 .contained()
2224 .with_background_color(theme.workspace.background)
2225 .boxed(),
2226 )
2227 .with_children(self.render_disconnected_overlay(cx))
2228 .named("workspace")
2229 }
2230
2231 fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
2232 cx.focus(&self.active_pane);
2233 }
2234}
2235
2236pub trait WorkspaceHandle {
2237 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
2238}
2239
2240impl WorkspaceHandle for ViewHandle<Workspace> {
2241 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
2242 self.read(cx)
2243 .worktrees(cx)
2244 .flat_map(|worktree| {
2245 let worktree_id = worktree.read(cx).id();
2246 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
2247 worktree_id,
2248 path: f.path.clone(),
2249 })
2250 })
2251 .collect::<Vec<_>>()
2252 }
2253}
2254
2255pub struct AvatarRibbon {
2256 color: Color,
2257}
2258
2259impl AvatarRibbon {
2260 pub fn new(color: Color) -> AvatarRibbon {
2261 AvatarRibbon { color }
2262 }
2263}
2264
2265impl Element for AvatarRibbon {
2266 type LayoutState = ();
2267
2268 type PaintState = ();
2269
2270 fn layout(
2271 &mut self,
2272 constraint: gpui::SizeConstraint,
2273 _: &mut gpui::LayoutContext,
2274 ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
2275 (constraint.max, ())
2276 }
2277
2278 fn paint(
2279 &mut self,
2280 bounds: gpui::geometry::rect::RectF,
2281 _: gpui::geometry::rect::RectF,
2282 _: &mut Self::LayoutState,
2283 cx: &mut gpui::PaintContext,
2284 ) -> Self::PaintState {
2285 let mut path = PathBuilder::new();
2286 path.reset(bounds.lower_left());
2287 path.curve_to(
2288 bounds.origin() + vec2f(bounds.height(), 0.),
2289 bounds.origin(),
2290 );
2291 path.line_to(bounds.upper_right() - vec2f(bounds.height(), 0.));
2292 path.curve_to(bounds.lower_right(), bounds.upper_right());
2293 path.line_to(bounds.lower_left());
2294 cx.scene.push_path(path.build(self.color, None));
2295 }
2296
2297 fn dispatch_event(
2298 &mut self,
2299 _: &gpui::Event,
2300 _: RectF,
2301 _: RectF,
2302 _: &mut Self::LayoutState,
2303 _: &mut Self::PaintState,
2304 _: &mut gpui::EventContext,
2305 ) -> bool {
2306 false
2307 }
2308
2309 fn debug(
2310 &self,
2311 bounds: gpui::geometry::rect::RectF,
2312 _: &Self::LayoutState,
2313 _: &Self::PaintState,
2314 _: &gpui::DebugContext,
2315 ) -> gpui::json::Value {
2316 json::json!({
2317 "type": "AvatarRibbon",
2318 "bounds": bounds.to_json(),
2319 "color": self.color.to_json(),
2320 })
2321 }
2322}
2323
2324impl std::fmt::Debug for OpenPaths {
2325 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2326 f.debug_struct("OpenPaths")
2327 .field("paths", &self.paths)
2328 .finish()
2329 }
2330}
2331
2332fn open(_: &Open, cx: &mut MutableAppContext) {
2333 let mut paths = cx.prompt_for_paths(PathPromptOptions {
2334 files: true,
2335 directories: true,
2336 multiple: true,
2337 });
2338 cx.spawn(|mut cx| async move {
2339 if let Some(paths) = paths.recv().await.flatten() {
2340 cx.update(|cx| cx.dispatch_global_action(OpenPaths { paths }));
2341 }
2342 })
2343 .detach();
2344}
2345
2346pub struct WorkspaceCreated(WeakViewHandle<Workspace>);
2347
2348pub fn open_paths(
2349 abs_paths: &[PathBuf],
2350 app_state: &Arc<AppState>,
2351 cx: &mut MutableAppContext,
2352) -> Task<(
2353 ViewHandle<Workspace>,
2354 Vec<Option<Result<Box<dyn ItemHandle>, Arc<anyhow::Error>>>>,
2355)> {
2356 log::info!("open paths {:?}", abs_paths);
2357
2358 // Open paths in existing workspace if possible
2359 let mut existing = None;
2360 for window_id in cx.window_ids().collect::<Vec<_>>() {
2361 if let Some(workspace_handle) = cx.root_view::<Workspace>(window_id) {
2362 if workspace_handle.update(cx, |workspace, cx| {
2363 if workspace.contains_paths(abs_paths, cx.as_ref()) {
2364 cx.activate_window(window_id);
2365 existing = Some(workspace_handle.clone());
2366 true
2367 } else {
2368 false
2369 }
2370 }) {
2371 break;
2372 }
2373 }
2374 }
2375
2376 let app_state = app_state.clone();
2377 let abs_paths = abs_paths.to_vec();
2378 cx.spawn(|mut cx| async move {
2379 let workspace = if let Some(existing) = existing {
2380 existing
2381 } else {
2382 let contains_directory =
2383 futures::future::join_all(abs_paths.iter().map(|path| app_state.fs.is_file(path)))
2384 .await
2385 .contains(&false);
2386
2387 cx.add_window((app_state.build_window_options)(), |cx| {
2388 let mut workspace = Workspace::new(
2389 Project::local(
2390 app_state.client.clone(),
2391 app_state.user_store.clone(),
2392 app_state.languages.clone(),
2393 app_state.fs.clone(),
2394 cx,
2395 ),
2396 cx,
2397 );
2398 (app_state.initialize_workspace)(&mut workspace, &app_state, cx);
2399 if contains_directory {
2400 workspace.toggle_sidebar_item(
2401 &ToggleSidebarItem {
2402 side: Side::Left,
2403 item_index: 0,
2404 },
2405 cx,
2406 );
2407 }
2408 workspace
2409 })
2410 .1
2411 };
2412
2413 let items = workspace
2414 .update(&mut cx, |workspace, cx| workspace.open_paths(abs_paths, cx))
2415 .await;
2416 (workspace, items)
2417 })
2418}
2419
2420pub fn join_project(
2421 contact: Arc<Contact>,
2422 project_index: usize,
2423 app_state: &Arc<AppState>,
2424 cx: &mut MutableAppContext,
2425) {
2426 let project_id = contact.projects[project_index].id;
2427
2428 for window_id in cx.window_ids().collect::<Vec<_>>() {
2429 if let Some(workspace) = cx.root_view::<Workspace>(window_id) {
2430 if workspace.read(cx).project().read(cx).remote_id() == Some(project_id) {
2431 cx.activate_window(window_id);
2432 return;
2433 }
2434 }
2435 }
2436
2437 cx.add_window((app_state.build_window_options)(), |cx| {
2438 WaitingRoom::new(contact, project_index, app_state.clone(), cx)
2439 });
2440}
2441
2442fn open_new(app_state: &Arc<AppState>, cx: &mut MutableAppContext) {
2443 let (window_id, workspace) = cx.add_window((app_state.build_window_options)(), |cx| {
2444 let mut workspace = Workspace::new(
2445 Project::local(
2446 app_state.client.clone(),
2447 app_state.user_store.clone(),
2448 app_state.languages.clone(),
2449 app_state.fs.clone(),
2450 cx,
2451 ),
2452 cx,
2453 );
2454 (app_state.initialize_workspace)(&mut workspace, app_state, cx);
2455 workspace
2456 });
2457 cx.dispatch_action(window_id, vec![workspace.id()], &NewFile);
2458}
2459
2460#[cfg(test)]
2461mod tests {
2462 use super::*;
2463 use gpui::{ModelHandle, TestAppContext, ViewContext};
2464 use project::{FakeFs, Project, ProjectEntryId};
2465 use serde_json::json;
2466
2467 #[gpui::test]
2468 async fn test_tracking_active_path(cx: &mut TestAppContext) {
2469 cx.foreground().forbid_parking();
2470 Settings::test_async(cx);
2471 let fs = FakeFs::new(cx.background());
2472 fs.insert_tree(
2473 "/root1",
2474 json!({
2475 "one.txt": "",
2476 "two.txt": "",
2477 }),
2478 )
2479 .await;
2480 fs.insert_tree(
2481 "/root2",
2482 json!({
2483 "three.txt": "",
2484 }),
2485 )
2486 .await;
2487
2488 let project = Project::test(fs, ["root1".as_ref()], cx).await;
2489 let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
2490 let worktree_id = project.read_with(cx, |project, cx| {
2491 project.worktrees(cx).next().unwrap().read(cx).id()
2492 });
2493
2494 let item1 = cx.add_view(window_id, |_| {
2495 let mut item = TestItem::new();
2496 item.project_path = Some((worktree_id, "one.txt").into());
2497 item
2498 });
2499 let item2 = cx.add_view(window_id, |_| {
2500 let mut item = TestItem::new();
2501 item.project_path = Some((worktree_id, "two.txt").into());
2502 item
2503 });
2504
2505 // Add an item to an empty pane
2506 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx));
2507 project.read_with(cx, |project, cx| {
2508 assert_eq!(
2509 project.active_entry(),
2510 project.entry_for_path(&(worktree_id, "one.txt").into(), cx)
2511 );
2512 });
2513 assert_eq!(
2514 cx.current_window_title(window_id).as_deref(),
2515 Some("one.txt — root1")
2516 );
2517
2518 // Add a second item to a non-empty pane
2519 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
2520 assert_eq!(
2521 cx.current_window_title(window_id).as_deref(),
2522 Some("two.txt — root1")
2523 );
2524 project.read_with(cx, |project, cx| {
2525 assert_eq!(
2526 project.active_entry(),
2527 project.entry_for_path(&(worktree_id, "two.txt").into(), cx)
2528 );
2529 });
2530
2531 // Close the active item
2532 workspace
2533 .update(cx, |workspace, cx| {
2534 Pane::close_active_item(workspace, &Default::default(), cx).unwrap()
2535 })
2536 .await
2537 .unwrap();
2538 assert_eq!(
2539 cx.current_window_title(window_id).as_deref(),
2540 Some("one.txt — root1")
2541 );
2542 project.read_with(cx, |project, cx| {
2543 assert_eq!(
2544 project.active_entry(),
2545 project.entry_for_path(&(worktree_id, "one.txt").into(), cx)
2546 );
2547 });
2548
2549 // Add a project folder
2550 project
2551 .update(cx, |project, cx| {
2552 project.find_or_create_local_worktree("/root2", true, cx)
2553 })
2554 .await
2555 .unwrap();
2556 assert_eq!(
2557 cx.current_window_title(window_id).as_deref(),
2558 Some("one.txt — root1, root2")
2559 );
2560
2561 // Remove a project folder
2562 project.update(cx, |project, cx| {
2563 project.remove_worktree(worktree_id, cx);
2564 });
2565 assert_eq!(
2566 cx.current_window_title(window_id).as_deref(),
2567 Some("one.txt — root2")
2568 );
2569 }
2570
2571 #[gpui::test]
2572 async fn test_close_window(cx: &mut TestAppContext) {
2573 cx.foreground().forbid_parking();
2574 Settings::test_async(cx);
2575 let fs = FakeFs::new(cx.background());
2576 fs.insert_tree("/root", json!({ "one": "" })).await;
2577
2578 let project = Project::test(fs, ["root".as_ref()], cx).await;
2579 let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
2580
2581 // When there are no dirty items, there's nothing to do.
2582 let item1 = cx.add_view(window_id, |_| TestItem::new());
2583 workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
2584 let task = workspace.update(cx, |w, cx| w.prepare_to_close(cx));
2585 assert_eq!(task.await.unwrap(), true);
2586
2587 // When there are dirty untitled items, prompt to save each one. If the user
2588 // cancels any prompt, then abort.
2589 let item2 = cx.add_view(window_id, |_| {
2590 let mut item = TestItem::new();
2591 item.is_dirty = true;
2592 item
2593 });
2594 let item3 = cx.add_view(window_id, |_| {
2595 let mut item = TestItem::new();
2596 item.is_dirty = true;
2597 item.project_entry_ids = vec![ProjectEntryId::from_proto(1)];
2598 item
2599 });
2600 workspace.update(cx, |w, cx| {
2601 w.add_item(Box::new(item2.clone()), cx);
2602 w.add_item(Box::new(item3.clone()), cx);
2603 });
2604 let task = workspace.update(cx, |w, cx| w.prepare_to_close(cx));
2605 cx.foreground().run_until_parked();
2606 cx.simulate_prompt_answer(window_id, 2 /* cancel */);
2607 cx.foreground().run_until_parked();
2608 assert!(!cx.has_pending_prompt(window_id));
2609 assert_eq!(task.await.unwrap(), false);
2610 }
2611
2612 #[gpui::test]
2613 async fn test_close_pane_items(cx: &mut TestAppContext) {
2614 cx.foreground().forbid_parking();
2615 Settings::test_async(cx);
2616 let fs = FakeFs::new(cx.background());
2617
2618 let project = Project::test(fs, None, cx).await;
2619 let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
2620
2621 let item1 = cx.add_view(window_id, |_| {
2622 let mut item = TestItem::new();
2623 item.is_dirty = true;
2624 item.project_entry_ids = vec![ProjectEntryId::from_proto(1)];
2625 item
2626 });
2627 let item2 = cx.add_view(window_id, |_| {
2628 let mut item = TestItem::new();
2629 item.is_dirty = true;
2630 item.has_conflict = true;
2631 item.project_entry_ids = vec![ProjectEntryId::from_proto(2)];
2632 item
2633 });
2634 let item3 = cx.add_view(window_id, |_| {
2635 let mut item = TestItem::new();
2636 item.is_dirty = true;
2637 item.has_conflict = true;
2638 item.project_entry_ids = vec![ProjectEntryId::from_proto(3)];
2639 item
2640 });
2641 let item4 = cx.add_view(window_id, |_| {
2642 let mut item = TestItem::new();
2643 item.is_dirty = true;
2644 item
2645 });
2646 let pane = workspace.update(cx, |workspace, cx| {
2647 workspace.add_item(Box::new(item1.clone()), cx);
2648 workspace.add_item(Box::new(item2.clone()), cx);
2649 workspace.add_item(Box::new(item3.clone()), cx);
2650 workspace.add_item(Box::new(item4.clone()), cx);
2651 workspace.active_pane().clone()
2652 });
2653
2654 let close_items = workspace.update(cx, |workspace, cx| {
2655 pane.update(cx, |pane, cx| {
2656 pane.activate_item(1, true, true, cx);
2657 assert_eq!(pane.active_item().unwrap().id(), item2.id());
2658 });
2659
2660 let item1_id = item1.id();
2661 let item3_id = item3.id();
2662 let item4_id = item4.id();
2663 Pane::close_items(workspace, pane.clone(), cx, move |id| {
2664 [item1_id, item3_id, item4_id].contains(&id)
2665 })
2666 });
2667
2668 cx.foreground().run_until_parked();
2669 pane.read_with(cx, |pane, _| {
2670 assert_eq!(pane.items().count(), 4);
2671 assert_eq!(pane.active_item().unwrap().id(), item1.id());
2672 });
2673
2674 cx.simulate_prompt_answer(window_id, 0);
2675 cx.foreground().run_until_parked();
2676 pane.read_with(cx, |pane, cx| {
2677 assert_eq!(item1.read(cx).save_count, 1);
2678 assert_eq!(item1.read(cx).save_as_count, 0);
2679 assert_eq!(item1.read(cx).reload_count, 0);
2680 assert_eq!(pane.items().count(), 3);
2681 assert_eq!(pane.active_item().unwrap().id(), item3.id());
2682 });
2683
2684 cx.simulate_prompt_answer(window_id, 1);
2685 cx.foreground().run_until_parked();
2686 pane.read_with(cx, |pane, cx| {
2687 assert_eq!(item3.read(cx).save_count, 0);
2688 assert_eq!(item3.read(cx).save_as_count, 0);
2689 assert_eq!(item3.read(cx).reload_count, 1);
2690 assert_eq!(pane.items().count(), 2);
2691 assert_eq!(pane.active_item().unwrap().id(), item4.id());
2692 });
2693
2694 cx.simulate_prompt_answer(window_id, 0);
2695 cx.foreground().run_until_parked();
2696 cx.simulate_new_path_selection(|_| Some(Default::default()));
2697 close_items.await.unwrap();
2698 pane.read_with(cx, |pane, cx| {
2699 assert_eq!(item4.read(cx).save_count, 0);
2700 assert_eq!(item4.read(cx).save_as_count, 1);
2701 assert_eq!(item4.read(cx).reload_count, 0);
2702 assert_eq!(pane.items().count(), 1);
2703 assert_eq!(pane.active_item().unwrap().id(), item2.id());
2704 });
2705 }
2706
2707 #[gpui::test]
2708 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
2709 cx.foreground().forbid_parking();
2710 Settings::test_async(cx);
2711 let fs = FakeFs::new(cx.background());
2712
2713 let project = Project::test(fs, [], cx).await;
2714 let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
2715
2716 // Create several workspace items with single project entries, and two
2717 // workspace items with multiple project entries.
2718 let single_entry_items = (0..=4)
2719 .map(|project_entry_id| {
2720 let mut item = TestItem::new();
2721 item.is_dirty = true;
2722 item.project_entry_ids = vec![ProjectEntryId::from_proto(project_entry_id)];
2723 item.is_singleton = true;
2724 item
2725 })
2726 .collect::<Vec<_>>();
2727 let item_2_3 = {
2728 let mut item = TestItem::new();
2729 item.is_dirty = true;
2730 item.is_singleton = false;
2731 item.project_entry_ids =
2732 vec![ProjectEntryId::from_proto(2), ProjectEntryId::from_proto(3)];
2733 item
2734 };
2735 let item_3_4 = {
2736 let mut item = TestItem::new();
2737 item.is_dirty = true;
2738 item.is_singleton = false;
2739 item.project_entry_ids =
2740 vec![ProjectEntryId::from_proto(3), ProjectEntryId::from_proto(4)];
2741 item
2742 };
2743
2744 // Create two panes that contain the following project entries:
2745 // left pane:
2746 // multi-entry items: (2, 3)
2747 // single-entry items: 0, 1, 2, 3, 4
2748 // right pane:
2749 // single-entry items: 1
2750 // multi-entry items: (3, 4)
2751 let left_pane = workspace.update(cx, |workspace, cx| {
2752 let left_pane = workspace.active_pane().clone();
2753 let right_pane = workspace.split_pane(left_pane.clone(), SplitDirection::Right, cx);
2754
2755 workspace.activate_pane(left_pane.clone(), cx);
2756 workspace.add_item(Box::new(cx.add_view(|_| item_2_3.clone())), cx);
2757 for item in &single_entry_items {
2758 workspace.add_item(Box::new(cx.add_view(|_| item.clone())), cx);
2759 }
2760
2761 workspace.activate_pane(right_pane.clone(), cx);
2762 workspace.add_item(Box::new(cx.add_view(|_| single_entry_items[1].clone())), cx);
2763 workspace.add_item(Box::new(cx.add_view(|_| item_3_4.clone())), cx);
2764
2765 left_pane
2766 });
2767
2768 // When closing all of the items in the left pane, we should be prompted twice:
2769 // once for project entry 0, and once for project entry 2. After those two
2770 // prompts, the task should complete.
2771 let close = workspace.update(cx, |workspace, cx| {
2772 workspace.activate_pane(left_pane.clone(), cx);
2773 Pane::close_items(workspace, left_pane.clone(), cx, |_| true)
2774 });
2775
2776 cx.foreground().run_until_parked();
2777 left_pane.read_with(cx, |pane, cx| {
2778 assert_eq!(
2779 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
2780 &[ProjectEntryId::from_proto(0)]
2781 );
2782 });
2783 cx.simulate_prompt_answer(window_id, 0);
2784
2785 cx.foreground().run_until_parked();
2786 left_pane.read_with(cx, |pane, cx| {
2787 assert_eq!(
2788 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
2789 &[ProjectEntryId::from_proto(2)]
2790 );
2791 });
2792 cx.simulate_prompt_answer(window_id, 0);
2793
2794 cx.foreground().run_until_parked();
2795 close.await.unwrap();
2796 left_pane.read_with(cx, |pane, _| {
2797 assert_eq!(pane.items().count(), 0);
2798 });
2799 }
2800
2801 #[derive(Clone)]
2802 struct TestItem {
2803 save_count: usize,
2804 save_as_count: usize,
2805 reload_count: usize,
2806 is_dirty: bool,
2807 has_conflict: bool,
2808 project_entry_ids: Vec<ProjectEntryId>,
2809 project_path: Option<ProjectPath>,
2810 is_singleton: bool,
2811 }
2812
2813 impl TestItem {
2814 fn new() -> Self {
2815 Self {
2816 save_count: 0,
2817 save_as_count: 0,
2818 reload_count: 0,
2819 is_dirty: false,
2820 has_conflict: false,
2821 project_entry_ids: Vec::new(),
2822 project_path: None,
2823 is_singleton: true,
2824 }
2825 }
2826 }
2827
2828 impl Entity for TestItem {
2829 type Event = ();
2830 }
2831
2832 impl View for TestItem {
2833 fn ui_name() -> &'static str {
2834 "TestItem"
2835 }
2836
2837 fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
2838 Empty::new().boxed()
2839 }
2840 }
2841
2842 impl Item for TestItem {
2843 fn tab_content(&self, _: &theme::Tab, _: &AppContext) -> ElementBox {
2844 Empty::new().boxed()
2845 }
2846
2847 fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
2848 self.project_path.clone()
2849 }
2850
2851 fn project_entry_ids(&self, _: &AppContext) -> SmallVec<[ProjectEntryId; 3]> {
2852 self.project_entry_ids.iter().copied().collect()
2853 }
2854
2855 fn is_singleton(&self, _: &AppContext) -> bool {
2856 self.is_singleton
2857 }
2858
2859 fn set_nav_history(&mut self, _: ItemNavHistory, _: &mut ViewContext<Self>) {}
2860
2861 fn clone_on_split(&self, _: &mut ViewContext<Self>) -> Option<Self>
2862 where
2863 Self: Sized,
2864 {
2865 Some(self.clone())
2866 }
2867
2868 fn is_dirty(&self, _: &AppContext) -> bool {
2869 self.is_dirty
2870 }
2871
2872 fn has_conflict(&self, _: &AppContext) -> bool {
2873 self.has_conflict
2874 }
2875
2876 fn can_save(&self, _: &AppContext) -> bool {
2877 self.project_entry_ids.len() > 0
2878 }
2879
2880 fn save(
2881 &mut self,
2882 _: ModelHandle<Project>,
2883 _: &mut ViewContext<Self>,
2884 ) -> Task<anyhow::Result<()>> {
2885 self.save_count += 1;
2886 Task::ready(Ok(()))
2887 }
2888
2889 fn save_as(
2890 &mut self,
2891 _: ModelHandle<Project>,
2892 _: std::path::PathBuf,
2893 _: &mut ViewContext<Self>,
2894 ) -> Task<anyhow::Result<()>> {
2895 self.save_as_count += 1;
2896 Task::ready(Ok(()))
2897 }
2898
2899 fn reload(
2900 &mut self,
2901 _: ModelHandle<Project>,
2902 _: &mut ViewContext<Self>,
2903 ) -> Task<anyhow::Result<()>> {
2904 self.reload_count += 1;
2905 Task::ready(Ok(()))
2906 }
2907
2908 fn should_update_tab_on_event(_: &Self::Event) -> bool {
2909 true
2910 }
2911 }
2912}