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