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_window_activation(Self::on_window_activation_changed)
809 .detach();
810 cx.observe(&project, |_, _, cx| cx.notify()).detach();
811 cx.subscribe(&project, move |this, _, event, cx| {
812 match event {
813 project::Event::RemoteIdChanged(remote_id) => {
814 this.project_remote_id_changed(*remote_id, cx);
815 }
816 project::Event::CollaboratorLeft(peer_id) => {
817 this.collaborator_left(*peer_id, cx);
818 }
819 project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded => {
820 this.update_window_title(cx);
821 }
822 project::Event::DisconnectedFromHost => {
823 this.update_window_edited(cx);
824 cx.blur();
825 }
826 _ => {}
827 }
828 cx.notify()
829 })
830 .detach();
831
832 let pane = cx.add_view(|cx| Pane::new(cx));
833 let pane_id = pane.id();
834 cx.subscribe(&pane, move |this, _, event, cx| {
835 this.handle_pane_event(pane_id, event, cx)
836 })
837 .detach();
838 cx.focus(&pane);
839 cx.emit(Event::PaneAdded(pane.clone()));
840
841 let fs = project.read(cx).fs().clone();
842 let user_store = project.read(cx).user_store();
843 let client = project.read(cx).client();
844 let mut current_user = user_store.read(cx).watch_current_user().clone();
845 let mut connection_status = client.status().clone();
846 let _observe_current_user = cx.spawn_weak(|this, mut cx| async move {
847 current_user.recv().await;
848 connection_status.recv().await;
849 let mut stream =
850 Stream::map(current_user, drop).merge(Stream::map(connection_status, drop));
851
852 while stream.recv().await.is_some() {
853 cx.update(|cx| {
854 if let Some(this) = this.upgrade(cx) {
855 this.update(cx, |_, cx| cx.notify());
856 }
857 })
858 }
859 });
860
861 let weak_self = cx.weak_handle();
862
863 cx.emit_global(WorkspaceCreated(weak_self.clone()));
864
865 let left_sidebar = cx.add_view(|_| Sidebar::new(Side::Left));
866 let right_sidebar = cx.add_view(|_| Sidebar::new(Side::Right));
867 let left_sidebar_buttons = cx.add_view(|cx| SidebarButtons::new(left_sidebar.clone(), cx));
868 let right_sidebar_buttons =
869 cx.add_view(|cx| SidebarButtons::new(right_sidebar.clone(), cx));
870 let status_bar = cx.add_view(|cx| {
871 let mut status_bar = StatusBar::new(&pane.clone(), cx);
872 status_bar.add_left_item(left_sidebar_buttons, cx);
873 status_bar.add_right_item(right_sidebar_buttons, cx);
874 status_bar
875 });
876
877 let mut this = Workspace {
878 modal: None,
879 weak_self,
880 center: PaneGroup::new(pane.clone()),
881 panes: vec![pane.clone()],
882 active_pane: pane.clone(),
883 status_bar,
884 notifications: Default::default(),
885 client,
886 remote_entity_subscription: None,
887 user_store,
888 fs,
889 left_sidebar,
890 right_sidebar,
891 project,
892 leader_state: Default::default(),
893 follower_states_by_leader: Default::default(),
894 last_leaders_by_pane: Default::default(),
895 window_edited: false,
896 _observe_current_user,
897 };
898 this.project_remote_id_changed(this.project.read(cx).remote_id(), cx);
899 cx.defer(|this, cx| this.update_window_title(cx));
900
901 this
902 }
903
904 pub fn weak_handle(&self) -> WeakViewHandle<Self> {
905 self.weak_self.clone()
906 }
907
908 pub fn left_sidebar(&self) -> &ViewHandle<Sidebar> {
909 &self.left_sidebar
910 }
911
912 pub fn right_sidebar(&self) -> &ViewHandle<Sidebar> {
913 &self.right_sidebar
914 }
915
916 pub fn status_bar(&self) -> &ViewHandle<StatusBar> {
917 &self.status_bar
918 }
919
920 pub fn user_store(&self) -> &ModelHandle<UserStore> {
921 &self.user_store
922 }
923
924 pub fn project(&self) -> &ModelHandle<Project> {
925 &self.project
926 }
927
928 /// Call the given callback with a workspace whose project is local.
929 ///
930 /// If the given workspace has a local project, then it will be passed
931 /// to the callback. Otherwise, a new empty window will be created.
932 pub fn with_local_workspace<T, F>(
933 &mut self,
934 cx: &mut ViewContext<Self>,
935 app_state: Arc<AppState>,
936 mut callback: F,
937 ) -> T
938 where
939 T: 'static,
940 F: FnMut(&mut Workspace, &mut ViewContext<Workspace>) -> T,
941 {
942 if self.project.read(cx).is_local() {
943 callback(self, cx)
944 } else {
945 let (_, workspace) = cx.add_window((app_state.build_window_options)(), |cx| {
946 let mut workspace = Workspace::new(
947 Project::local(
948 false,
949 app_state.client.clone(),
950 app_state.user_store.clone(),
951 app_state.project_store.clone(),
952 app_state.languages.clone(),
953 app_state.fs.clone(),
954 cx,
955 ),
956 cx,
957 );
958 (app_state.initialize_workspace)(&mut workspace, &app_state, cx);
959 workspace
960 });
961 workspace.update(cx, callback)
962 }
963 }
964
965 pub fn worktrees<'a>(
966 &self,
967 cx: &'a AppContext,
968 ) -> impl 'a + Iterator<Item = ModelHandle<Worktree>> {
969 self.project.read(cx).worktrees(cx)
970 }
971
972 pub fn visible_worktrees<'a>(
973 &self,
974 cx: &'a AppContext,
975 ) -> impl 'a + Iterator<Item = ModelHandle<Worktree>> {
976 self.project.read(cx).visible_worktrees(cx)
977 }
978
979 pub fn worktree_scans_complete(&self, cx: &AppContext) -> impl Future<Output = ()> + 'static {
980 let futures = self
981 .worktrees(cx)
982 .filter_map(|worktree| worktree.read(cx).as_local())
983 .map(|worktree| worktree.scan_complete())
984 .collect::<Vec<_>>();
985 async move {
986 for future in futures {
987 future.await;
988 }
989 }
990 }
991
992 pub fn close(
993 &mut self,
994 _: &CloseWindow,
995 cx: &mut ViewContext<Self>,
996 ) -> Option<Task<Result<()>>> {
997 let prepare = self.prepare_to_close(cx);
998 Some(cx.spawn(|this, mut cx| async move {
999 if prepare.await? {
1000 this.update(&mut cx, |_, cx| {
1001 let window_id = cx.window_id();
1002 cx.remove_window(window_id);
1003 });
1004 }
1005 Ok(())
1006 }))
1007 }
1008
1009 pub fn prepare_to_close(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<bool>> {
1010 self.save_all_internal(true, cx)
1011 }
1012
1013 fn save_all(&mut self, _: &SaveAll, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
1014 let save_all = self.save_all_internal(false, cx);
1015 Some(cx.foreground().spawn(async move {
1016 save_all.await?;
1017 Ok(())
1018 }))
1019 }
1020
1021 fn save_all_internal(
1022 &mut self,
1023 should_prompt_to_save: bool,
1024 cx: &mut ViewContext<Self>,
1025 ) -> Task<Result<bool>> {
1026 if self.project.read(cx).is_read_only() {
1027 return Task::ready(Ok(true));
1028 }
1029
1030 let dirty_items = self
1031 .panes
1032 .iter()
1033 .flat_map(|pane| {
1034 pane.read(cx).items().filter_map(|item| {
1035 if item.is_dirty(cx) {
1036 Some((pane.clone(), item.boxed_clone()))
1037 } else {
1038 None
1039 }
1040 })
1041 })
1042 .collect::<Vec<_>>();
1043
1044 let project = self.project.clone();
1045 cx.spawn_weak(|_, mut cx| async move {
1046 for (pane, item) in dirty_items {
1047 let (singleton, project_entry_ids) =
1048 cx.read(|cx| (item.is_singleton(cx), item.project_entry_ids(cx)));
1049 if singleton || !project_entry_ids.is_empty() {
1050 if let Some(ix) =
1051 pane.read_with(&cx, |pane, _| pane.index_for_item(item.as_ref()))
1052 {
1053 if !Pane::save_item(
1054 project.clone(),
1055 &pane,
1056 ix,
1057 &item,
1058 should_prompt_to_save,
1059 &mut cx,
1060 )
1061 .await?
1062 {
1063 return Ok(false);
1064 }
1065 }
1066 }
1067 }
1068 Ok(true)
1069 })
1070 }
1071
1072 pub fn open_paths(
1073 &mut self,
1074 mut abs_paths: Vec<PathBuf>,
1075 visible: bool,
1076 cx: &mut ViewContext<Self>,
1077 ) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, Arc<anyhow::Error>>>>> {
1078 let fs = self.fs.clone();
1079
1080 // Sort the paths to ensure we add worktrees for parents before their children.
1081 abs_paths.sort_unstable();
1082 cx.spawn(|this, mut cx| async move {
1083 let mut project_paths = Vec::new();
1084 for path in &abs_paths {
1085 project_paths.push(
1086 this.update(&mut cx, |this, cx| {
1087 this.project_path_for_path(path, visible, cx)
1088 })
1089 .await
1090 .log_err(),
1091 );
1092 }
1093
1094 let tasks = abs_paths
1095 .iter()
1096 .cloned()
1097 .zip(project_paths.into_iter())
1098 .map(|(abs_path, project_path)| {
1099 let this = this.clone();
1100 cx.spawn(|mut cx| {
1101 let fs = fs.clone();
1102 async move {
1103 let (_worktree, project_path) = project_path?;
1104 if fs.is_file(&abs_path).await {
1105 Some(
1106 this.update(&mut cx, |this, cx| {
1107 this.open_path(project_path, true, cx)
1108 })
1109 .await,
1110 )
1111 } else {
1112 None
1113 }
1114 }
1115 })
1116 })
1117 .collect::<Vec<_>>();
1118
1119 futures::future::join_all(tasks).await
1120 })
1121 }
1122
1123 fn add_folder_to_project(&mut self, _: &AddFolderToProject, cx: &mut ViewContext<Self>) {
1124 let mut paths = cx.prompt_for_paths(PathPromptOptions {
1125 files: false,
1126 directories: true,
1127 multiple: true,
1128 });
1129 cx.spawn(|this, mut cx| async move {
1130 if let Some(paths) = paths.recv().await.flatten() {
1131 let results = this
1132 .update(&mut cx, |this, cx| this.open_paths(paths, true, cx))
1133 .await;
1134 for result in results {
1135 if let Some(result) = result {
1136 result.log_err();
1137 }
1138 }
1139 }
1140 })
1141 .detach();
1142 }
1143
1144 fn remove_folder_from_project(
1145 &mut self,
1146 RemoveWorktreeFromProject(worktree_id): &RemoveWorktreeFromProject,
1147 cx: &mut ViewContext<Self>,
1148 ) {
1149 self.project
1150 .update(cx, |project, cx| project.remove_worktree(*worktree_id, cx));
1151 }
1152
1153 fn toggle_project_online(&mut self, action: &ToggleProjectOnline, cx: &mut ViewContext<Self>) {
1154 let project = action
1155 .project
1156 .clone()
1157 .unwrap_or_else(|| self.project.clone());
1158 project.update(cx, |project, cx| {
1159 let public = !project.is_online();
1160 project.set_online(public, cx);
1161 });
1162 }
1163
1164 fn project_path_for_path(
1165 &self,
1166 abs_path: &Path,
1167 visible: bool,
1168 cx: &mut ViewContext<Self>,
1169 ) -> Task<Result<(ModelHandle<Worktree>, ProjectPath)>> {
1170 let entry = self.project().update(cx, |project, cx| {
1171 project.find_or_create_local_worktree(abs_path, visible, cx)
1172 });
1173 cx.spawn(|_, cx| async move {
1174 let (worktree, path) = entry.await?;
1175 let worktree_id = worktree.read_with(&cx, |t, _| t.id());
1176 Ok((
1177 worktree,
1178 ProjectPath {
1179 worktree_id,
1180 path: path.into(),
1181 },
1182 ))
1183 })
1184 }
1185
1186 /// Returns the modal that was toggled closed if it was open.
1187 pub fn toggle_modal<V, F>(
1188 &mut self,
1189 cx: &mut ViewContext<Self>,
1190 add_view: F,
1191 ) -> Option<ViewHandle<V>>
1192 where
1193 V: 'static + View,
1194 F: FnOnce(&mut Self, &mut ViewContext<Self>) -> ViewHandle<V>,
1195 {
1196 cx.notify();
1197 // Whatever modal was visible is getting clobbered. If its the same type as V, then return
1198 // it. Otherwise, create a new modal and set it as active.
1199 let already_open_modal = self.modal.take().and_then(|modal| modal.downcast::<V>());
1200 if let Some(already_open_modal) = already_open_modal {
1201 cx.focus_self();
1202 Some(already_open_modal)
1203 } else {
1204 let modal = add_view(self, cx);
1205 cx.focus(&modal);
1206 self.modal = Some(modal.into());
1207 None
1208 }
1209 }
1210
1211 pub fn modal(&self) -> Option<&AnyViewHandle> {
1212 self.modal.as_ref()
1213 }
1214
1215 pub fn dismiss_modal(&mut self, cx: &mut ViewContext<Self>) {
1216 if self.modal.take().is_some() {
1217 cx.focus(&self.active_pane);
1218 cx.notify();
1219 }
1220 }
1221
1222 pub fn show_notification<V: Notification>(
1223 &mut self,
1224 id: usize,
1225 cx: &mut ViewContext<Self>,
1226 build_notification: impl FnOnce(&mut ViewContext<Self>) -> ViewHandle<V>,
1227 ) {
1228 let type_id = TypeId::of::<V>();
1229 if self
1230 .notifications
1231 .iter()
1232 .all(|(existing_type_id, existing_id, _)| {
1233 (*existing_type_id, *existing_id) != (type_id, id)
1234 })
1235 {
1236 let notification = build_notification(cx);
1237 cx.subscribe(¬ification, move |this, handle, event, cx| {
1238 if handle.read(cx).should_dismiss_notification_on_event(event) {
1239 this.dismiss_notification(type_id, id, cx);
1240 }
1241 })
1242 .detach();
1243 self.notifications
1244 .push((type_id, id, Box::new(notification)));
1245 cx.notify();
1246 }
1247 }
1248
1249 fn dismiss_notification(&mut self, type_id: TypeId, id: usize, cx: &mut ViewContext<Self>) {
1250 self.notifications
1251 .retain(|(existing_type_id, existing_id, _)| {
1252 if (*existing_type_id, *existing_id) == (type_id, id) {
1253 cx.notify();
1254 false
1255 } else {
1256 true
1257 }
1258 });
1259 }
1260
1261 pub fn items<'a>(
1262 &'a self,
1263 cx: &'a AppContext,
1264 ) -> impl 'a + Iterator<Item = &Box<dyn ItemHandle>> {
1265 self.panes.iter().flat_map(|pane| pane.read(cx).items())
1266 }
1267
1268 pub fn item_of_type<T: Item>(&self, cx: &AppContext) -> Option<ViewHandle<T>> {
1269 self.items_of_type(cx).max_by_key(|item| item.id())
1270 }
1271
1272 pub fn items_of_type<'a, T: Item>(
1273 &'a self,
1274 cx: &'a AppContext,
1275 ) -> impl 'a + Iterator<Item = ViewHandle<T>> {
1276 self.panes
1277 .iter()
1278 .flat_map(|pane| pane.read(cx).items_of_type())
1279 }
1280
1281 pub fn active_item(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
1282 self.active_pane().read(cx).active_item()
1283 }
1284
1285 fn active_project_path(&self, cx: &ViewContext<Self>) -> Option<ProjectPath> {
1286 self.active_item(cx).and_then(|item| item.project_path(cx))
1287 }
1288
1289 pub fn save_active_item(
1290 &mut self,
1291 force_name_change: bool,
1292 cx: &mut ViewContext<Self>,
1293 ) -> Task<Result<()>> {
1294 let project = self.project.clone();
1295 if let Some(item) = self.active_item(cx) {
1296 if !force_name_change && item.can_save(cx) {
1297 if item.has_conflict(cx.as_ref()) {
1298 const CONFLICT_MESSAGE: &'static str = "This file has changed on disk since you started editing it. Do you want to overwrite it?";
1299
1300 let mut answer = cx.prompt(
1301 PromptLevel::Warning,
1302 CONFLICT_MESSAGE,
1303 &["Overwrite", "Cancel"],
1304 );
1305 cx.spawn(|_, mut cx| async move {
1306 let answer = answer.recv().await;
1307 if answer == Some(0) {
1308 cx.update(|cx| item.save(project, cx)).await?;
1309 }
1310 Ok(())
1311 })
1312 } else {
1313 item.save(project, cx)
1314 }
1315 } else if item.is_singleton(cx) {
1316 let worktree = self.worktrees(cx).next();
1317 let start_abs_path = worktree
1318 .and_then(|w| w.read(cx).as_local())
1319 .map_or(Path::new(""), |w| w.abs_path())
1320 .to_path_buf();
1321 let mut abs_path = cx.prompt_for_new_path(&start_abs_path);
1322 cx.spawn(|_, mut cx| async move {
1323 if let Some(abs_path) = abs_path.recv().await.flatten() {
1324 cx.update(|cx| item.save_as(project, abs_path, cx)).await?;
1325 }
1326 Ok(())
1327 })
1328 } else {
1329 Task::ready(Ok(()))
1330 }
1331 } else {
1332 Task::ready(Ok(()))
1333 }
1334 }
1335
1336 pub fn toggle_sidebar(&mut self, side: Side, cx: &mut ViewContext<Self>) {
1337 let sidebar = match side {
1338 Side::Left => &mut self.left_sidebar,
1339 Side::Right => &mut self.right_sidebar,
1340 };
1341 sidebar.update(cx, |sidebar, cx| {
1342 sidebar.set_open(!sidebar.is_open(), cx);
1343 });
1344 cx.focus_self();
1345 cx.notify();
1346 }
1347
1348 pub fn toggle_sidebar_item(&mut self, action: &ToggleSidebarItem, cx: &mut ViewContext<Self>) {
1349 let sidebar = match action.side {
1350 Side::Left => &mut self.left_sidebar,
1351 Side::Right => &mut self.right_sidebar,
1352 };
1353 let active_item = sidebar.update(cx, |sidebar, cx| {
1354 if sidebar.is_open() && sidebar.active_item_ix() == action.item_index {
1355 sidebar.set_open(false, cx);
1356 None
1357 } else {
1358 sidebar.set_open(true, cx);
1359 sidebar.activate_item(action.item_index, cx);
1360 sidebar.active_item().cloned()
1361 }
1362 });
1363 if let Some(active_item) = active_item {
1364 if active_item.is_focused(cx) {
1365 cx.focus_self();
1366 } else {
1367 cx.focus(active_item.to_any());
1368 }
1369 } else {
1370 cx.focus_self();
1371 }
1372 cx.notify();
1373 }
1374
1375 pub fn toggle_sidebar_item_focus(
1376 &mut self,
1377 side: Side,
1378 item_index: usize,
1379 cx: &mut ViewContext<Self>,
1380 ) {
1381 let sidebar = match side {
1382 Side::Left => &mut self.left_sidebar,
1383 Side::Right => &mut self.right_sidebar,
1384 };
1385 let active_item = sidebar.update(cx, |sidebar, cx| {
1386 sidebar.set_open(true, cx);
1387 sidebar.activate_item(item_index, cx);
1388 sidebar.active_item().cloned()
1389 });
1390 if let Some(active_item) = active_item {
1391 if active_item.is_focused(cx) {
1392 cx.focus_self();
1393 } else {
1394 cx.focus(active_item.to_any());
1395 }
1396 }
1397 cx.notify();
1398 }
1399
1400 pub fn focus_center(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
1401 cx.focus_self();
1402 cx.notify();
1403 }
1404
1405 fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
1406 let pane = cx.add_view(|cx| Pane::new(cx));
1407 let pane_id = pane.id();
1408 cx.subscribe(&pane, move |this, _, event, cx| {
1409 this.handle_pane_event(pane_id, event, cx)
1410 })
1411 .detach();
1412 self.panes.push(pane.clone());
1413 self.activate_pane(pane.clone(), cx);
1414 cx.emit(Event::PaneAdded(pane.clone()));
1415 pane
1416 }
1417
1418 pub fn add_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
1419 let pane = self.active_pane().clone();
1420 Pane::add_item(self, pane, item, true, true, cx);
1421 }
1422
1423 pub fn open_path(
1424 &mut self,
1425 path: impl Into<ProjectPath>,
1426 focus_item: bool,
1427 cx: &mut ViewContext<Self>,
1428 ) -> Task<Result<Box<dyn ItemHandle>, Arc<anyhow::Error>>> {
1429 let pane = self.active_pane().downgrade();
1430 let task = self.load_path(path.into(), cx);
1431 cx.spawn(|this, mut cx| async move {
1432 let (project_entry_id, build_item) = task.await?;
1433 let pane = pane
1434 .upgrade(&cx)
1435 .ok_or_else(|| anyhow!("pane was closed"))?;
1436 this.update(&mut cx, |this, cx| {
1437 Ok(Pane::open_item(
1438 this,
1439 pane,
1440 project_entry_id,
1441 focus_item,
1442 cx,
1443 build_item,
1444 ))
1445 })
1446 })
1447 }
1448
1449 pub(crate) fn load_path(
1450 &mut self,
1451 path: ProjectPath,
1452 cx: &mut ViewContext<Self>,
1453 ) -> Task<
1454 Result<(
1455 ProjectEntryId,
1456 impl 'static + FnOnce(&mut MutableAppContext) -> Box<dyn ItemHandle>,
1457 )>,
1458 > {
1459 let project = self.project().clone();
1460 let project_item = project.update(cx, |project, cx| project.open_path(path, cx));
1461 let window_id = cx.window_id();
1462 cx.as_mut().spawn(|mut cx| async move {
1463 let (project_entry_id, project_item) = project_item.await?;
1464 let build_item = cx.update(|cx| {
1465 cx.default_global::<ProjectItemBuilders>()
1466 .get(&project_item.model_type())
1467 .ok_or_else(|| anyhow!("no item builder for project item"))
1468 .cloned()
1469 })?;
1470 let build_item =
1471 move |cx: &mut MutableAppContext| build_item(window_id, project, project_item, cx);
1472 Ok((project_entry_id, build_item))
1473 })
1474 }
1475
1476 pub fn open_project_item<T>(
1477 &mut self,
1478 project_item: ModelHandle<T::Item>,
1479 cx: &mut ViewContext<Self>,
1480 ) -> ViewHandle<T>
1481 where
1482 T: ProjectItem,
1483 {
1484 use project::Item as _;
1485
1486 let entry_id = project_item.read(cx).entry_id(cx);
1487 if let Some(item) = entry_id
1488 .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id, cx))
1489 .and_then(|item| item.downcast())
1490 {
1491 self.activate_item(&item, cx);
1492 return item;
1493 }
1494
1495 let item = cx.add_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
1496 self.add_item(Box::new(item.clone()), cx);
1497 item
1498 }
1499
1500 pub fn activate_item(&mut self, item: &dyn ItemHandle, cx: &mut ViewContext<Self>) -> bool {
1501 let result = self.panes.iter().find_map(|pane| {
1502 if let Some(ix) = pane.read(cx).index_for_item(item) {
1503 Some((pane.clone(), ix))
1504 } else {
1505 None
1506 }
1507 });
1508 if let Some((pane, ix)) = result {
1509 self.activate_pane(pane.clone(), cx);
1510 pane.update(cx, |pane, cx| pane.activate_item(ix, true, true, cx));
1511 true
1512 } else {
1513 false
1514 }
1515 }
1516
1517 fn activate_pane_at_index(&mut self, action: &ActivatePane, cx: &mut ViewContext<Self>) {
1518 let panes = self.center.panes();
1519 if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
1520 self.activate_pane(pane, cx);
1521 } else {
1522 self.split_pane(self.active_pane.clone(), SplitDirection::Right, cx);
1523 }
1524 }
1525
1526 pub fn activate_next_pane(&mut self, cx: &mut ViewContext<Self>) {
1527 let next_pane = {
1528 let panes = self.center.panes();
1529 let ix = panes
1530 .iter()
1531 .position(|pane| **pane == self.active_pane)
1532 .unwrap();
1533 let next_ix = (ix + 1) % panes.len();
1534 panes[next_ix].clone()
1535 };
1536 self.activate_pane(next_pane, cx);
1537 }
1538
1539 pub fn activate_previous_pane(&mut self, cx: &mut ViewContext<Self>) {
1540 let prev_pane = {
1541 let panes = self.center.panes();
1542 let ix = panes
1543 .iter()
1544 .position(|pane| **pane == self.active_pane)
1545 .unwrap();
1546 let prev_ix = if ix == 0 { panes.len() - 1 } else { ix - 1 };
1547 panes[prev_ix].clone()
1548 };
1549 self.activate_pane(prev_pane, cx);
1550 }
1551
1552 fn activate_pane(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
1553 if self.active_pane != pane {
1554 self.active_pane = pane.clone();
1555 self.status_bar.update(cx, |status_bar, cx| {
1556 status_bar.set_active_pane(&self.active_pane, cx);
1557 });
1558 self.active_item_path_changed(cx);
1559 cx.focus(&self.active_pane);
1560 cx.notify();
1561 }
1562
1563 self.update_followers(
1564 proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView {
1565 id: self.active_item(cx).map(|item| item.id() as u64),
1566 leader_id: self.leader_for_pane(&pane).map(|id| id.0),
1567 }),
1568 cx,
1569 );
1570 }
1571
1572 fn handle_pane_event(
1573 &mut self,
1574 pane_id: usize,
1575 event: &pane::Event,
1576 cx: &mut ViewContext<Self>,
1577 ) {
1578 if let Some(pane) = self.pane(pane_id) {
1579 match event {
1580 pane::Event::Split(direction) => {
1581 self.split_pane(pane, *direction, cx);
1582 }
1583 pane::Event::Remove => {
1584 self.remove_pane(pane, cx);
1585 }
1586 pane::Event::Activate => {
1587 self.activate_pane(pane, cx);
1588 }
1589 pane::Event::ActivateItem { local } => {
1590 if *local {
1591 self.unfollow(&pane, cx);
1592 }
1593 if pane == self.active_pane {
1594 self.active_item_path_changed(cx);
1595 }
1596 }
1597 pane::Event::ChangeItemTitle => {
1598 if pane == self.active_pane {
1599 self.active_item_path_changed(cx);
1600 }
1601 self.update_window_edited(cx);
1602 }
1603 pane::Event::RemoveItem => {
1604 self.update_window_edited(cx);
1605 }
1606 }
1607 } else {
1608 error!("pane {} not found", pane_id);
1609 }
1610 }
1611
1612 pub fn split_pane(
1613 &mut self,
1614 pane: ViewHandle<Pane>,
1615 direction: SplitDirection,
1616 cx: &mut ViewContext<Self>,
1617 ) -> ViewHandle<Pane> {
1618 let new_pane = self.add_pane(cx);
1619 self.activate_pane(new_pane.clone(), cx);
1620 if let Some(item) = pane.read(cx).active_item() {
1621 if let Some(clone) = item.clone_on_split(cx.as_mut()) {
1622 Pane::add_item(self, new_pane.clone(), clone, true, true, cx);
1623 }
1624 }
1625 self.center.split(&pane, &new_pane, direction).unwrap();
1626 cx.notify();
1627 new_pane
1628 }
1629
1630 fn remove_pane(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
1631 if self.center.remove(&pane).unwrap() {
1632 self.panes.retain(|p| p != &pane);
1633 self.activate_pane(self.panes.last().unwrap().clone(), cx);
1634 self.unfollow(&pane, cx);
1635 self.last_leaders_by_pane.remove(&pane.downgrade());
1636 cx.notify();
1637 } else {
1638 self.active_item_path_changed(cx);
1639 }
1640 }
1641
1642 pub fn panes(&self) -> &[ViewHandle<Pane>] {
1643 &self.panes
1644 }
1645
1646 fn pane(&self, pane_id: usize) -> Option<ViewHandle<Pane>> {
1647 self.panes.iter().find(|pane| pane.id() == pane_id).cloned()
1648 }
1649
1650 pub fn active_pane(&self) -> &ViewHandle<Pane> {
1651 &self.active_pane
1652 }
1653
1654 fn project_remote_id_changed(&mut self, remote_id: Option<u64>, cx: &mut ViewContext<Self>) {
1655 if let Some(remote_id) = remote_id {
1656 self.remote_entity_subscription =
1657 Some(self.client.add_view_for_remote_entity(remote_id, cx));
1658 } else {
1659 self.remote_entity_subscription.take();
1660 }
1661 }
1662
1663 fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
1664 self.leader_state.followers.remove(&peer_id);
1665 if let Some(states_by_pane) = self.follower_states_by_leader.remove(&peer_id) {
1666 for state in states_by_pane.into_values() {
1667 for item in state.items_by_leader_view_id.into_values() {
1668 if let FollowerItem::Loaded(item) = item {
1669 item.set_leader_replica_id(None, cx);
1670 }
1671 }
1672 }
1673 }
1674 cx.notify();
1675 }
1676
1677 pub fn toggle_follow(
1678 &mut self,
1679 ToggleFollow(leader_id): &ToggleFollow,
1680 cx: &mut ViewContext<Self>,
1681 ) -> Option<Task<Result<()>>> {
1682 let leader_id = *leader_id;
1683 let pane = self.active_pane().clone();
1684
1685 if let Some(prev_leader_id) = self.unfollow(&pane, cx) {
1686 if leader_id == prev_leader_id {
1687 return None;
1688 }
1689 }
1690
1691 self.last_leaders_by_pane
1692 .insert(pane.downgrade(), leader_id);
1693 self.follower_states_by_leader
1694 .entry(leader_id)
1695 .or_default()
1696 .insert(pane.clone(), Default::default());
1697 cx.notify();
1698
1699 let project_id = self.project.read(cx).remote_id()?;
1700 let request = self.client.request(proto::Follow {
1701 project_id,
1702 leader_id: leader_id.0,
1703 });
1704 Some(cx.spawn_weak(|this, mut cx| async move {
1705 let response = request.await?;
1706 if let Some(this) = this.upgrade(&cx) {
1707 this.update(&mut cx, |this, _| {
1708 let state = this
1709 .follower_states_by_leader
1710 .get_mut(&leader_id)
1711 .and_then(|states_by_pane| states_by_pane.get_mut(&pane))
1712 .ok_or_else(|| anyhow!("following interrupted"))?;
1713 state.active_view_id = response.active_view_id;
1714 Ok::<_, anyhow::Error>(())
1715 })?;
1716 Self::add_views_from_leader(this, leader_id, vec![pane], response.views, &mut cx)
1717 .await?;
1718 }
1719 Ok(())
1720 }))
1721 }
1722
1723 pub fn follow_next_collaborator(
1724 &mut self,
1725 _: &FollowNextCollaborator,
1726 cx: &mut ViewContext<Self>,
1727 ) -> Option<Task<Result<()>>> {
1728 let collaborators = self.project.read(cx).collaborators();
1729 let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
1730 let mut collaborators = collaborators.keys().copied();
1731 while let Some(peer_id) = collaborators.next() {
1732 if peer_id == leader_id {
1733 break;
1734 }
1735 }
1736 collaborators.next()
1737 } else if let Some(last_leader_id) =
1738 self.last_leaders_by_pane.get(&self.active_pane.downgrade())
1739 {
1740 if collaborators.contains_key(last_leader_id) {
1741 Some(*last_leader_id)
1742 } else {
1743 None
1744 }
1745 } else {
1746 None
1747 };
1748
1749 next_leader_id
1750 .or_else(|| collaborators.keys().copied().next())
1751 .and_then(|leader_id| self.toggle_follow(&ToggleFollow(leader_id), cx))
1752 }
1753
1754 pub fn unfollow(
1755 &mut self,
1756 pane: &ViewHandle<Pane>,
1757 cx: &mut ViewContext<Self>,
1758 ) -> Option<PeerId> {
1759 for (leader_id, states_by_pane) in &mut self.follower_states_by_leader {
1760 let leader_id = *leader_id;
1761 if let Some(state) = states_by_pane.remove(&pane) {
1762 for (_, item) in state.items_by_leader_view_id {
1763 if let FollowerItem::Loaded(item) = item {
1764 item.set_leader_replica_id(None, cx);
1765 }
1766 }
1767
1768 if states_by_pane.is_empty() {
1769 self.follower_states_by_leader.remove(&leader_id);
1770 if let Some(project_id) = self.project.read(cx).remote_id() {
1771 self.client
1772 .send(proto::Unfollow {
1773 project_id,
1774 leader_id: leader_id.0,
1775 })
1776 .log_err();
1777 }
1778 }
1779
1780 cx.notify();
1781 return Some(leader_id);
1782 }
1783 }
1784 None
1785 }
1786
1787 fn render_connection_status(&self, cx: &mut RenderContext<Self>) -> Option<ElementBox> {
1788 let theme = &cx.global::<Settings>().theme;
1789 match &*self.client.status().borrow() {
1790 client::Status::ConnectionError
1791 | client::Status::ConnectionLost
1792 | client::Status::Reauthenticating
1793 | client::Status::Reconnecting { .. }
1794 | client::Status::ReconnectionError { .. } => Some(
1795 Container::new(
1796 Align::new(
1797 ConstrainedBox::new(
1798 Svg::new("icons/offline-14.svg")
1799 .with_color(theme.workspace.titlebar.offline_icon.color)
1800 .boxed(),
1801 )
1802 .with_width(theme.workspace.titlebar.offline_icon.width)
1803 .boxed(),
1804 )
1805 .boxed(),
1806 )
1807 .with_style(theme.workspace.titlebar.offline_icon.container)
1808 .boxed(),
1809 ),
1810 client::Status::UpgradeRequired => Some(
1811 Label::new(
1812 "Please update Zed to collaborate".to_string(),
1813 theme.workspace.titlebar.outdated_warning.text.clone(),
1814 )
1815 .contained()
1816 .with_style(theme.workspace.titlebar.outdated_warning.container)
1817 .aligned()
1818 .boxed(),
1819 ),
1820 _ => None,
1821 }
1822 }
1823
1824 fn render_titlebar(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
1825 let project = &self.project.read(cx);
1826 let replica_id = project.replica_id();
1827 let mut worktree_root_names = String::new();
1828 for (i, name) in project.worktree_root_names(cx).enumerate() {
1829 if i > 0 {
1830 worktree_root_names.push_str(", ");
1831 }
1832 worktree_root_names.push_str(name);
1833 }
1834
1835 ConstrainedBox::new(
1836 Container::new(
1837 Stack::new()
1838 .with_child(
1839 Label::new(worktree_root_names, theme.workspace.titlebar.title.clone())
1840 .aligned()
1841 .left()
1842 .boxed(),
1843 )
1844 .with_child(
1845 Align::new(
1846 Flex::row()
1847 .with_children(self.render_collaborators(theme, cx))
1848 .with_children(self.render_current_user(
1849 self.user_store.read(cx).current_user().as_ref(),
1850 replica_id,
1851 theme,
1852 cx,
1853 ))
1854 .with_children(self.render_connection_status(cx))
1855 .boxed(),
1856 )
1857 .right()
1858 .boxed(),
1859 )
1860 .boxed(),
1861 )
1862 .with_style(theme.workspace.titlebar.container)
1863 .boxed(),
1864 )
1865 .with_height(theme.workspace.titlebar.height)
1866 .named("titlebar")
1867 }
1868
1869 fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
1870 let active_entry = self.active_project_path(cx);
1871 self.project
1872 .update(cx, |project, cx| project.set_active_path(active_entry, cx));
1873 self.update_window_title(cx);
1874 }
1875
1876 fn update_window_title(&mut self, cx: &mut ViewContext<Self>) {
1877 let mut title = String::new();
1878 let project = self.project().read(cx);
1879 if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
1880 let filename = path
1881 .path
1882 .file_name()
1883 .map(|s| s.to_string_lossy())
1884 .or_else(|| {
1885 Some(Cow::Borrowed(
1886 project
1887 .worktree_for_id(path.worktree_id, cx)?
1888 .read(cx)
1889 .root_name(),
1890 ))
1891 });
1892 if let Some(filename) = filename {
1893 title.push_str(filename.as_ref());
1894 title.push_str(" — ");
1895 }
1896 }
1897 for (i, name) in project.worktree_root_names(cx).enumerate() {
1898 if i > 0 {
1899 title.push_str(", ");
1900 }
1901 title.push_str(name);
1902 }
1903 if title.is_empty() {
1904 title = "empty project".to_string();
1905 }
1906 cx.set_window_title(&title);
1907 }
1908
1909 fn update_window_edited(&mut self, cx: &mut ViewContext<Self>) {
1910 let is_edited = !self.project.read(cx).is_read_only()
1911 && self
1912 .items(cx)
1913 .any(|item| item.has_conflict(cx) || item.is_dirty(cx));
1914 if is_edited != self.window_edited {
1915 self.window_edited = is_edited;
1916 cx.set_window_edited(self.window_edited)
1917 }
1918 }
1919
1920 fn render_collaborators(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> Vec<ElementBox> {
1921 let mut collaborators = self
1922 .project
1923 .read(cx)
1924 .collaborators()
1925 .values()
1926 .cloned()
1927 .collect::<Vec<_>>();
1928 collaborators.sort_unstable_by_key(|collaborator| collaborator.replica_id);
1929 collaborators
1930 .into_iter()
1931 .filter_map(|collaborator| {
1932 Some(self.render_avatar(
1933 collaborator.user.avatar.clone()?,
1934 collaborator.replica_id,
1935 Some((collaborator.peer_id, &collaborator.user.github_login)),
1936 theme,
1937 cx,
1938 ))
1939 })
1940 .collect()
1941 }
1942
1943 fn render_current_user(
1944 &self,
1945 user: Option<&Arc<User>>,
1946 replica_id: ReplicaId,
1947 theme: &Theme,
1948 cx: &mut RenderContext<Self>,
1949 ) -> Option<ElementBox> {
1950 let status = *self.client.status().borrow();
1951 if let Some(avatar) = user.and_then(|user| user.avatar.clone()) {
1952 Some(self.render_avatar(avatar, replica_id, None, theme, cx))
1953 } else if matches!(status, client::Status::UpgradeRequired) {
1954 None
1955 } else {
1956 Some(
1957 MouseEventHandler::new::<Authenticate, _, _>(0, cx, |state, _| {
1958 let style = theme
1959 .workspace
1960 .titlebar
1961 .sign_in_prompt
1962 .style_for(state, false);
1963 Label::new("Sign in".to_string(), style.text.clone())
1964 .contained()
1965 .with_style(style.container)
1966 .boxed()
1967 })
1968 .on_click(|_, _, cx| cx.dispatch_action(Authenticate))
1969 .with_cursor_style(CursorStyle::PointingHand)
1970 .aligned()
1971 .boxed(),
1972 )
1973 }
1974 }
1975
1976 fn render_avatar(
1977 &self,
1978 avatar: Arc<ImageData>,
1979 replica_id: ReplicaId,
1980 peer: Option<(PeerId, &str)>,
1981 theme: &Theme,
1982 cx: &mut RenderContext<Self>,
1983 ) -> ElementBox {
1984 let replica_color = theme.editor.replica_selection_style(replica_id).cursor;
1985 let is_followed = peer.map_or(false, |(peer_id, _)| {
1986 self.follower_states_by_leader.contains_key(&peer_id)
1987 });
1988 let mut avatar_style = theme.workspace.titlebar.avatar;
1989 if is_followed {
1990 avatar_style.border = Border::all(1.0, replica_color);
1991 }
1992 let content = Stack::new()
1993 .with_child(
1994 Image::new(avatar)
1995 .with_style(avatar_style)
1996 .constrained()
1997 .with_width(theme.workspace.titlebar.avatar_width)
1998 .aligned()
1999 .boxed(),
2000 )
2001 .with_child(
2002 AvatarRibbon::new(replica_color)
2003 .constrained()
2004 .with_width(theme.workspace.titlebar.avatar_ribbon.width)
2005 .with_height(theme.workspace.titlebar.avatar_ribbon.height)
2006 .aligned()
2007 .bottom()
2008 .boxed(),
2009 )
2010 .constrained()
2011 .with_width(theme.workspace.titlebar.avatar_width)
2012 .contained()
2013 .with_margin_left(theme.workspace.titlebar.avatar_margin)
2014 .boxed();
2015
2016 if let Some((peer_id, peer_github_login)) = peer {
2017 MouseEventHandler::new::<ToggleFollow, _, _>(replica_id.into(), cx, move |_, _| content)
2018 .with_cursor_style(CursorStyle::PointingHand)
2019 .on_click(move |_, _, cx| cx.dispatch_action(ToggleFollow(peer_id)))
2020 .with_tooltip::<ToggleFollow, _>(
2021 peer_id.0 as usize,
2022 if is_followed {
2023 format!("Unfollow {}", peer_github_login)
2024 } else {
2025 format!("Follow {}", peer_github_login)
2026 },
2027 Some(Box::new(FollowNextCollaborator)),
2028 theme.tooltip.clone(),
2029 cx,
2030 )
2031 .boxed()
2032 } else {
2033 content
2034 }
2035 }
2036
2037 fn render_disconnected_overlay(&self, cx: &AppContext) -> Option<ElementBox> {
2038 if self.project.read(cx).is_read_only() {
2039 let theme = &cx.global::<Settings>().theme;
2040 Some(
2041 EventHandler::new(
2042 Label::new(
2043 "Your connection to the remote project has been lost.".to_string(),
2044 theme.workspace.disconnected_overlay.text.clone(),
2045 )
2046 .aligned()
2047 .contained()
2048 .with_style(theme.workspace.disconnected_overlay.container)
2049 .boxed(),
2050 )
2051 .capture_all::<Self>(0)
2052 .boxed(),
2053 )
2054 } else {
2055 None
2056 }
2057 }
2058
2059 fn render_notifications(&self, theme: &theme::Workspace) -> Option<ElementBox> {
2060 if self.notifications.is_empty() {
2061 None
2062 } else {
2063 Some(
2064 Flex::column()
2065 .with_children(self.notifications.iter().map(|(_, _, notification)| {
2066 ChildView::new(notification.as_ref())
2067 .contained()
2068 .with_style(theme.notification)
2069 .boxed()
2070 }))
2071 .constrained()
2072 .with_width(theme.notifications.width)
2073 .contained()
2074 .with_style(theme.notifications.container)
2075 .aligned()
2076 .bottom()
2077 .right()
2078 .boxed(),
2079 )
2080 }
2081 }
2082
2083 // RPC handlers
2084
2085 async fn handle_follow(
2086 this: ViewHandle<Self>,
2087 envelope: TypedEnvelope<proto::Follow>,
2088 _: Arc<Client>,
2089 mut cx: AsyncAppContext,
2090 ) -> Result<proto::FollowResponse> {
2091 this.update(&mut cx, |this, cx| {
2092 this.leader_state
2093 .followers
2094 .insert(envelope.original_sender_id()?);
2095
2096 let active_view_id = this
2097 .active_item(cx)
2098 .and_then(|i| i.to_followable_item_handle(cx))
2099 .map(|i| i.id() as u64);
2100 Ok(proto::FollowResponse {
2101 active_view_id,
2102 views: this
2103 .panes()
2104 .iter()
2105 .flat_map(|pane| {
2106 let leader_id = this.leader_for_pane(pane).map(|id| id.0);
2107 pane.read(cx).items().filter_map({
2108 let cx = &cx;
2109 move |item| {
2110 let id = item.id() as u64;
2111 let item = item.to_followable_item_handle(cx)?;
2112 let variant = item.to_state_proto(cx)?;
2113 Some(proto::View {
2114 id,
2115 leader_id,
2116 variant: Some(variant),
2117 })
2118 }
2119 })
2120 })
2121 .collect(),
2122 })
2123 })
2124 }
2125
2126 async fn handle_unfollow(
2127 this: ViewHandle<Self>,
2128 envelope: TypedEnvelope<proto::Unfollow>,
2129 _: Arc<Client>,
2130 mut cx: AsyncAppContext,
2131 ) -> Result<()> {
2132 this.update(&mut cx, |this, _| {
2133 this.leader_state
2134 .followers
2135 .remove(&envelope.original_sender_id()?);
2136 Ok(())
2137 })
2138 }
2139
2140 async fn handle_update_followers(
2141 this: ViewHandle<Self>,
2142 envelope: TypedEnvelope<proto::UpdateFollowers>,
2143 _: Arc<Client>,
2144 mut cx: AsyncAppContext,
2145 ) -> Result<()> {
2146 let leader_id = envelope.original_sender_id()?;
2147 match envelope
2148 .payload
2149 .variant
2150 .ok_or_else(|| anyhow!("invalid update"))?
2151 {
2152 proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
2153 this.update(&mut cx, |this, cx| {
2154 this.update_leader_state(leader_id, cx, |state, _| {
2155 state.active_view_id = update_active_view.id;
2156 });
2157 Ok::<_, anyhow::Error>(())
2158 })
2159 }
2160 proto::update_followers::Variant::UpdateView(update_view) => {
2161 this.update(&mut cx, |this, cx| {
2162 let variant = update_view
2163 .variant
2164 .ok_or_else(|| anyhow!("missing update view variant"))?;
2165 this.update_leader_state(leader_id, cx, |state, cx| {
2166 let variant = variant.clone();
2167 match state
2168 .items_by_leader_view_id
2169 .entry(update_view.id)
2170 .or_insert(FollowerItem::Loading(Vec::new()))
2171 {
2172 FollowerItem::Loaded(item) => {
2173 item.apply_update_proto(variant, cx).log_err();
2174 }
2175 FollowerItem::Loading(updates) => updates.push(variant),
2176 }
2177 });
2178 Ok(())
2179 })
2180 }
2181 proto::update_followers::Variant::CreateView(view) => {
2182 let panes = this.read_with(&cx, |this, _| {
2183 this.follower_states_by_leader
2184 .get(&leader_id)
2185 .into_iter()
2186 .flat_map(|states_by_pane| states_by_pane.keys())
2187 .cloned()
2188 .collect()
2189 });
2190 Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], &mut cx)
2191 .await?;
2192 Ok(())
2193 }
2194 }
2195 .log_err();
2196
2197 Ok(())
2198 }
2199
2200 async fn add_views_from_leader(
2201 this: ViewHandle<Self>,
2202 leader_id: PeerId,
2203 panes: Vec<ViewHandle<Pane>>,
2204 views: Vec<proto::View>,
2205 cx: &mut AsyncAppContext,
2206 ) -> Result<()> {
2207 let project = this.read_with(cx, |this, _| this.project.clone());
2208 let replica_id = project
2209 .read_with(cx, |project, _| {
2210 project
2211 .collaborators()
2212 .get(&leader_id)
2213 .map(|c| c.replica_id)
2214 })
2215 .ok_or_else(|| anyhow!("no such collaborator {}", leader_id))?;
2216
2217 let item_builders = cx.update(|cx| {
2218 cx.default_global::<FollowableItemBuilders>()
2219 .values()
2220 .map(|b| b.0)
2221 .collect::<Vec<_>>()
2222 .clone()
2223 });
2224
2225 let mut item_tasks_by_pane = HashMap::default();
2226 for pane in panes {
2227 let mut item_tasks = Vec::new();
2228 let mut leader_view_ids = Vec::new();
2229 for view in &views {
2230 let mut variant = view.variant.clone();
2231 if variant.is_none() {
2232 Err(anyhow!("missing variant"))?;
2233 }
2234 for build_item in &item_builders {
2235 let task =
2236 cx.update(|cx| build_item(pane.clone(), project.clone(), &mut variant, cx));
2237 if let Some(task) = task {
2238 item_tasks.push(task);
2239 leader_view_ids.push(view.id);
2240 break;
2241 } else {
2242 assert!(variant.is_some());
2243 }
2244 }
2245 }
2246
2247 item_tasks_by_pane.insert(pane, (item_tasks, leader_view_ids));
2248 }
2249
2250 for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane {
2251 let items = futures::future::try_join_all(item_tasks).await?;
2252 this.update(cx, |this, cx| {
2253 let state = this
2254 .follower_states_by_leader
2255 .get_mut(&leader_id)?
2256 .get_mut(&pane)?;
2257
2258 for (id, item) in leader_view_ids.into_iter().zip(items) {
2259 item.set_leader_replica_id(Some(replica_id), cx);
2260 match state.items_by_leader_view_id.entry(id) {
2261 hash_map::Entry::Occupied(e) => {
2262 let e = e.into_mut();
2263 if let FollowerItem::Loading(updates) = e {
2264 for update in updates.drain(..) {
2265 item.apply_update_proto(update, cx)
2266 .context("failed to apply view update")
2267 .log_err();
2268 }
2269 }
2270 *e = FollowerItem::Loaded(item);
2271 }
2272 hash_map::Entry::Vacant(e) => {
2273 e.insert(FollowerItem::Loaded(item));
2274 }
2275 }
2276 }
2277
2278 Some(())
2279 });
2280 }
2281 this.update(cx, |this, cx| this.leader_updated(leader_id, cx));
2282
2283 Ok(())
2284 }
2285
2286 fn update_followers(
2287 &self,
2288 update: proto::update_followers::Variant,
2289 cx: &AppContext,
2290 ) -> Option<()> {
2291 let project_id = self.project.read(cx).remote_id()?;
2292 if !self.leader_state.followers.is_empty() {
2293 self.client
2294 .send(proto::UpdateFollowers {
2295 project_id,
2296 follower_ids: self.leader_state.followers.iter().map(|f| f.0).collect(),
2297 variant: Some(update),
2298 })
2299 .log_err();
2300 }
2301 None
2302 }
2303
2304 pub fn leader_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<PeerId> {
2305 self.follower_states_by_leader
2306 .iter()
2307 .find_map(|(leader_id, state)| {
2308 if state.contains_key(pane) {
2309 Some(*leader_id)
2310 } else {
2311 None
2312 }
2313 })
2314 }
2315
2316 fn update_leader_state(
2317 &mut self,
2318 leader_id: PeerId,
2319 cx: &mut ViewContext<Self>,
2320 mut update_fn: impl FnMut(&mut FollowerState, &mut ViewContext<Self>),
2321 ) {
2322 for (_, state) in self
2323 .follower_states_by_leader
2324 .get_mut(&leader_id)
2325 .into_iter()
2326 .flatten()
2327 {
2328 update_fn(state, cx);
2329 }
2330 self.leader_updated(leader_id, cx);
2331 }
2332
2333 fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
2334 let mut items_to_add = Vec::new();
2335 for (pane, state) in self.follower_states_by_leader.get(&leader_id)? {
2336 if let Some(active_item) = state
2337 .active_view_id
2338 .and_then(|id| state.items_by_leader_view_id.get(&id))
2339 {
2340 if let FollowerItem::Loaded(item) = active_item {
2341 items_to_add.push((pane.clone(), item.boxed_clone()));
2342 }
2343 }
2344 }
2345
2346 for (pane, item) in items_to_add {
2347 Pane::add_item(self, pane.clone(), item.boxed_clone(), false, false, cx);
2348 if pane == self.active_pane {
2349 pane.update(cx, |pane, cx| pane.focus_active_item(cx));
2350 }
2351 cx.notify();
2352 }
2353 None
2354 }
2355
2356 fn on_window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
2357 if !active
2358 && matches!(
2359 cx.global::<Settings>().autosave,
2360 Autosave::OnWindowChange | Autosave::OnFocusChange
2361 )
2362 {
2363 for pane in &self.panes {
2364 pane.update(cx, |pane, cx| {
2365 for item in pane.items() {
2366 Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
2367 .detach_and_log_err(cx);
2368 }
2369 });
2370 }
2371 }
2372 }
2373}
2374
2375impl Entity for Workspace {
2376 type Event = Event;
2377}
2378
2379impl View for Workspace {
2380 fn ui_name() -> &'static str {
2381 "Workspace"
2382 }
2383
2384 fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
2385 let theme = cx.global::<Settings>().theme.clone();
2386 Stack::new()
2387 .with_child(
2388 Flex::column()
2389 .with_child(self.render_titlebar(&theme, cx))
2390 .with_child(
2391 Stack::new()
2392 .with_child({
2393 Flex::row()
2394 .with_children(
2395 if self.left_sidebar.read(cx).active_item().is_some() {
2396 Some(
2397 ChildView::new(&self.left_sidebar)
2398 .flex(0.8, false)
2399 .boxed(),
2400 )
2401 } else {
2402 None
2403 },
2404 )
2405 .with_child(
2406 FlexItem::new(self.center.render(
2407 &theme,
2408 &self.follower_states_by_leader,
2409 self.project.read(cx).collaborators(),
2410 ))
2411 .flex(1., true)
2412 .boxed(),
2413 )
2414 .with_children(
2415 if self.right_sidebar.read(cx).active_item().is_some() {
2416 Some(
2417 ChildView::new(&self.right_sidebar)
2418 .flex(0.8, false)
2419 .boxed(),
2420 )
2421 } else {
2422 None
2423 },
2424 )
2425 .boxed()
2426 })
2427 .with_children(self.modal.as_ref().map(|m| {
2428 ChildView::new(m)
2429 .contained()
2430 .with_style(theme.workspace.modal)
2431 .aligned()
2432 .top()
2433 .boxed()
2434 }))
2435 .with_children(self.render_notifications(&theme.workspace))
2436 .flex(1.0, true)
2437 .boxed(),
2438 )
2439 .with_child(ChildView::new(&self.status_bar).boxed())
2440 .contained()
2441 .with_background_color(theme.workspace.background)
2442 .boxed(),
2443 )
2444 .with_children(self.render_disconnected_overlay(cx))
2445 .named("workspace")
2446 }
2447
2448 fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
2449 cx.focus(&self.active_pane);
2450 }
2451}
2452
2453pub trait WorkspaceHandle {
2454 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
2455}
2456
2457impl WorkspaceHandle for ViewHandle<Workspace> {
2458 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
2459 self.read(cx)
2460 .worktrees(cx)
2461 .flat_map(|worktree| {
2462 let worktree_id = worktree.read(cx).id();
2463 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
2464 worktree_id,
2465 path: f.path.clone(),
2466 })
2467 })
2468 .collect::<Vec<_>>()
2469 }
2470}
2471
2472pub struct AvatarRibbon {
2473 color: Color,
2474}
2475
2476impl AvatarRibbon {
2477 pub fn new(color: Color) -> AvatarRibbon {
2478 AvatarRibbon { color }
2479 }
2480}
2481
2482impl Element for AvatarRibbon {
2483 type LayoutState = ();
2484
2485 type PaintState = ();
2486
2487 fn layout(
2488 &mut self,
2489 constraint: gpui::SizeConstraint,
2490 _: &mut gpui::LayoutContext,
2491 ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
2492 (constraint.max, ())
2493 }
2494
2495 fn paint(
2496 &mut self,
2497 bounds: gpui::geometry::rect::RectF,
2498 _: gpui::geometry::rect::RectF,
2499 _: &mut Self::LayoutState,
2500 cx: &mut gpui::PaintContext,
2501 ) -> Self::PaintState {
2502 let mut path = PathBuilder::new();
2503 path.reset(bounds.lower_left());
2504 path.curve_to(
2505 bounds.origin() + vec2f(bounds.height(), 0.),
2506 bounds.origin(),
2507 );
2508 path.line_to(bounds.upper_right() - vec2f(bounds.height(), 0.));
2509 path.curve_to(bounds.lower_right(), bounds.upper_right());
2510 path.line_to(bounds.lower_left());
2511 cx.scene.push_path(path.build(self.color, None));
2512 }
2513
2514 fn dispatch_event(
2515 &mut self,
2516 _: &gpui::Event,
2517 _: RectF,
2518 _: RectF,
2519 _: &mut Self::LayoutState,
2520 _: &mut Self::PaintState,
2521 _: &mut gpui::EventContext,
2522 ) -> bool {
2523 false
2524 }
2525
2526 fn debug(
2527 &self,
2528 bounds: gpui::geometry::rect::RectF,
2529 _: &Self::LayoutState,
2530 _: &Self::PaintState,
2531 _: &gpui::DebugContext,
2532 ) -> gpui::json::Value {
2533 json::json!({
2534 "type": "AvatarRibbon",
2535 "bounds": bounds.to_json(),
2536 "color": self.color.to_json(),
2537 })
2538 }
2539}
2540
2541impl std::fmt::Debug for OpenPaths {
2542 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2543 f.debug_struct("OpenPaths")
2544 .field("paths", &self.paths)
2545 .finish()
2546 }
2547}
2548
2549fn open(_: &Open, cx: &mut MutableAppContext) {
2550 let mut paths = cx.prompt_for_paths(PathPromptOptions {
2551 files: true,
2552 directories: true,
2553 multiple: true,
2554 });
2555 cx.spawn(|mut cx| async move {
2556 if let Some(paths) = paths.recv().await.flatten() {
2557 cx.update(|cx| cx.dispatch_global_action(OpenPaths { paths }));
2558 }
2559 })
2560 .detach();
2561}
2562
2563pub struct WorkspaceCreated(WeakViewHandle<Workspace>);
2564
2565pub fn activate_workspace_for_project(
2566 cx: &mut MutableAppContext,
2567 predicate: impl Fn(&mut Project, &mut ModelContext<Project>) -> bool,
2568) -> Option<ViewHandle<Workspace>> {
2569 for window_id in cx.window_ids().collect::<Vec<_>>() {
2570 if let Some(workspace_handle) = cx.root_view::<Workspace>(window_id) {
2571 let project = workspace_handle.read(cx).project.clone();
2572 if project.update(cx, &predicate) {
2573 cx.activate_window(window_id);
2574 return Some(workspace_handle);
2575 }
2576 }
2577 }
2578 None
2579}
2580
2581pub fn open_paths(
2582 abs_paths: &[PathBuf],
2583 app_state: &Arc<AppState>,
2584 cx: &mut MutableAppContext,
2585) -> Task<(
2586 ViewHandle<Workspace>,
2587 Vec<Option<Result<Box<dyn ItemHandle>, Arc<anyhow::Error>>>>,
2588)> {
2589 log::info!("open paths {:?}", abs_paths);
2590
2591 // Open paths in existing workspace if possible
2592 let existing =
2593 activate_workspace_for_project(cx, |project, cx| project.contains_paths(abs_paths, cx));
2594
2595 let app_state = app_state.clone();
2596 let abs_paths = abs_paths.to_vec();
2597 cx.spawn(|mut cx| async move {
2598 let mut new_project = None;
2599 let workspace = if let Some(existing) = existing {
2600 existing
2601 } else {
2602 let contains_directory =
2603 futures::future::join_all(abs_paths.iter().map(|path| app_state.fs.is_file(path)))
2604 .await
2605 .contains(&false);
2606
2607 cx.add_window((app_state.build_window_options)(), |cx| {
2608 let project = Project::local(
2609 false,
2610 app_state.client.clone(),
2611 app_state.user_store.clone(),
2612 app_state.project_store.clone(),
2613 app_state.languages.clone(),
2614 app_state.fs.clone(),
2615 cx,
2616 );
2617 new_project = Some(project.clone());
2618 let mut workspace = Workspace::new(project, cx);
2619 (app_state.initialize_workspace)(&mut workspace, &app_state, cx);
2620 if contains_directory {
2621 workspace.toggle_sidebar(Side::Left, cx);
2622 }
2623 workspace
2624 })
2625 .1
2626 };
2627
2628 let items = workspace
2629 .update(&mut cx, |workspace, cx| {
2630 workspace.open_paths(abs_paths, true, cx)
2631 })
2632 .await;
2633
2634 if let Some(project) = new_project {
2635 project
2636 .update(&mut cx, |project, cx| project.restore_state(cx))
2637 .await
2638 .log_err();
2639 }
2640
2641 (workspace, items)
2642 })
2643}
2644
2645pub fn join_project(
2646 contact: Arc<Contact>,
2647 project_index: usize,
2648 app_state: &Arc<AppState>,
2649 cx: &mut MutableAppContext,
2650) {
2651 let project_id = contact.projects[project_index].id;
2652
2653 for window_id in cx.window_ids().collect::<Vec<_>>() {
2654 if let Some(workspace) = cx.root_view::<Workspace>(window_id) {
2655 if workspace.read(cx).project().read(cx).remote_id() == Some(project_id) {
2656 cx.activate_window(window_id);
2657 return;
2658 }
2659 }
2660 }
2661
2662 cx.add_window((app_state.build_window_options)(), |cx| {
2663 WaitingRoom::new(contact, project_index, app_state.clone(), cx)
2664 });
2665}
2666
2667fn open_new(app_state: &Arc<AppState>, cx: &mut MutableAppContext) {
2668 let (window_id, workspace) = cx.add_window((app_state.build_window_options)(), |cx| {
2669 let mut workspace = Workspace::new(
2670 Project::local(
2671 false,
2672 app_state.client.clone(),
2673 app_state.user_store.clone(),
2674 app_state.project_store.clone(),
2675 app_state.languages.clone(),
2676 app_state.fs.clone(),
2677 cx,
2678 ),
2679 cx,
2680 );
2681 (app_state.initialize_workspace)(&mut workspace, app_state, cx);
2682 workspace
2683 });
2684 cx.dispatch_action(window_id, vec![workspace.id()], &NewFile);
2685}
2686
2687#[cfg(test)]
2688mod tests {
2689 use super::*;
2690 use gpui::{executor::Deterministic, ModelHandle, TestAppContext, ViewContext};
2691 use project::{FakeFs, Project, ProjectEntryId};
2692 use serde_json::json;
2693
2694 #[gpui::test]
2695 async fn test_tracking_active_path(cx: &mut TestAppContext) {
2696 cx.foreground().forbid_parking();
2697 Settings::test_async(cx);
2698 let fs = FakeFs::new(cx.background());
2699 fs.insert_tree(
2700 "/root1",
2701 json!({
2702 "one.txt": "",
2703 "two.txt": "",
2704 }),
2705 )
2706 .await;
2707 fs.insert_tree(
2708 "/root2",
2709 json!({
2710 "three.txt": "",
2711 }),
2712 )
2713 .await;
2714
2715 let project = Project::test(fs, ["root1".as_ref()], cx).await;
2716 let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
2717 let worktree_id = project.read_with(cx, |project, cx| {
2718 project.worktrees(cx).next().unwrap().read(cx).id()
2719 });
2720
2721 let item1 = cx.add_view(window_id, |_| {
2722 let mut item = TestItem::new();
2723 item.project_path = Some((worktree_id, "one.txt").into());
2724 item
2725 });
2726 let item2 = cx.add_view(window_id, |_| {
2727 let mut item = TestItem::new();
2728 item.project_path = Some((worktree_id, "two.txt").into());
2729 item
2730 });
2731
2732 // Add an item to an empty pane
2733 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx));
2734 project.read_with(cx, |project, cx| {
2735 assert_eq!(
2736 project.active_entry(),
2737 project.entry_for_path(&(worktree_id, "one.txt").into(), cx)
2738 );
2739 });
2740 assert_eq!(
2741 cx.current_window_title(window_id).as_deref(),
2742 Some("one.txt — root1")
2743 );
2744
2745 // Add a second item to a non-empty pane
2746 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
2747 assert_eq!(
2748 cx.current_window_title(window_id).as_deref(),
2749 Some("two.txt — root1")
2750 );
2751 project.read_with(cx, |project, cx| {
2752 assert_eq!(
2753 project.active_entry(),
2754 project.entry_for_path(&(worktree_id, "two.txt").into(), cx)
2755 );
2756 });
2757
2758 // Close the active item
2759 workspace
2760 .update(cx, |workspace, cx| {
2761 Pane::close_active_item(workspace, &Default::default(), cx).unwrap()
2762 })
2763 .await
2764 .unwrap();
2765 assert_eq!(
2766 cx.current_window_title(window_id).as_deref(),
2767 Some("one.txt — root1")
2768 );
2769 project.read_with(cx, |project, cx| {
2770 assert_eq!(
2771 project.active_entry(),
2772 project.entry_for_path(&(worktree_id, "one.txt").into(), cx)
2773 );
2774 });
2775
2776 // Add a project folder
2777 project
2778 .update(cx, |project, cx| {
2779 project.find_or_create_local_worktree("/root2", true, cx)
2780 })
2781 .await
2782 .unwrap();
2783 assert_eq!(
2784 cx.current_window_title(window_id).as_deref(),
2785 Some("one.txt — root1, root2")
2786 );
2787
2788 // Remove a project folder
2789 project.update(cx, |project, cx| {
2790 project.remove_worktree(worktree_id, cx);
2791 });
2792 assert_eq!(
2793 cx.current_window_title(window_id).as_deref(),
2794 Some("one.txt — root2")
2795 );
2796 }
2797
2798 #[gpui::test]
2799 async fn test_close_window(cx: &mut TestAppContext) {
2800 cx.foreground().forbid_parking();
2801 Settings::test_async(cx);
2802 let fs = FakeFs::new(cx.background());
2803 fs.insert_tree("/root", json!({ "one": "" })).await;
2804
2805 let project = Project::test(fs, ["root".as_ref()], cx).await;
2806 let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
2807
2808 // When there are no dirty items, there's nothing to do.
2809 let item1 = cx.add_view(window_id, |_| TestItem::new());
2810 workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
2811 let task = workspace.update(cx, |w, cx| w.prepare_to_close(cx));
2812 assert_eq!(task.await.unwrap(), true);
2813
2814 // When there are dirty untitled items, prompt to save each one. If the user
2815 // cancels any prompt, then abort.
2816 let item2 = cx.add_view(window_id, |_| {
2817 let mut item = TestItem::new();
2818 item.is_dirty = true;
2819 item
2820 });
2821 let item3 = cx.add_view(window_id, |_| {
2822 let mut item = TestItem::new();
2823 item.is_dirty = true;
2824 item.project_entry_ids = vec![ProjectEntryId::from_proto(1)];
2825 item
2826 });
2827 workspace.update(cx, |w, cx| {
2828 w.add_item(Box::new(item2.clone()), cx);
2829 w.add_item(Box::new(item3.clone()), cx);
2830 });
2831 let task = workspace.update(cx, |w, cx| w.prepare_to_close(cx));
2832 cx.foreground().run_until_parked();
2833 cx.simulate_prompt_answer(window_id, 2 /* cancel */);
2834 cx.foreground().run_until_parked();
2835 assert!(!cx.has_pending_prompt(window_id));
2836 assert_eq!(task.await.unwrap(), false);
2837 }
2838
2839 #[gpui::test]
2840 async fn test_close_pane_items(cx: &mut TestAppContext) {
2841 cx.foreground().forbid_parking();
2842 Settings::test_async(cx);
2843 let fs = FakeFs::new(cx.background());
2844
2845 let project = Project::test(fs, None, cx).await;
2846 let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
2847
2848 let item1 = cx.add_view(window_id, |_| {
2849 let mut item = TestItem::new();
2850 item.is_dirty = true;
2851 item.project_entry_ids = vec![ProjectEntryId::from_proto(1)];
2852 item
2853 });
2854 let item2 = cx.add_view(window_id, |_| {
2855 let mut item = TestItem::new();
2856 item.is_dirty = true;
2857 item.has_conflict = true;
2858 item.project_entry_ids = vec![ProjectEntryId::from_proto(2)];
2859 item
2860 });
2861 let item3 = cx.add_view(window_id, |_| {
2862 let mut item = TestItem::new();
2863 item.is_dirty = true;
2864 item.has_conflict = true;
2865 item.project_entry_ids = vec![ProjectEntryId::from_proto(3)];
2866 item
2867 });
2868 let item4 = cx.add_view(window_id, |_| {
2869 let mut item = TestItem::new();
2870 item.is_dirty = true;
2871 item
2872 });
2873 let pane = workspace.update(cx, |workspace, cx| {
2874 workspace.add_item(Box::new(item1.clone()), cx);
2875 workspace.add_item(Box::new(item2.clone()), cx);
2876 workspace.add_item(Box::new(item3.clone()), cx);
2877 workspace.add_item(Box::new(item4.clone()), cx);
2878 workspace.active_pane().clone()
2879 });
2880
2881 let close_items = workspace.update(cx, |workspace, cx| {
2882 pane.update(cx, |pane, cx| {
2883 pane.activate_item(1, true, true, cx);
2884 assert_eq!(pane.active_item().unwrap().id(), item2.id());
2885 });
2886
2887 let item1_id = item1.id();
2888 let item3_id = item3.id();
2889 let item4_id = item4.id();
2890 Pane::close_items(workspace, pane.clone(), cx, move |id| {
2891 [item1_id, item3_id, item4_id].contains(&id)
2892 })
2893 });
2894
2895 cx.foreground().run_until_parked();
2896 pane.read_with(cx, |pane, _| {
2897 assert_eq!(pane.items().count(), 4);
2898 assert_eq!(pane.active_item().unwrap().id(), item1.id());
2899 });
2900
2901 cx.simulate_prompt_answer(window_id, 0);
2902 cx.foreground().run_until_parked();
2903 pane.read_with(cx, |pane, cx| {
2904 assert_eq!(item1.read(cx).save_count, 1);
2905 assert_eq!(item1.read(cx).save_as_count, 0);
2906 assert_eq!(item1.read(cx).reload_count, 0);
2907 assert_eq!(pane.items().count(), 3);
2908 assert_eq!(pane.active_item().unwrap().id(), item3.id());
2909 });
2910
2911 cx.simulate_prompt_answer(window_id, 1);
2912 cx.foreground().run_until_parked();
2913 pane.read_with(cx, |pane, cx| {
2914 assert_eq!(item3.read(cx).save_count, 0);
2915 assert_eq!(item3.read(cx).save_as_count, 0);
2916 assert_eq!(item3.read(cx).reload_count, 1);
2917 assert_eq!(pane.items().count(), 2);
2918 assert_eq!(pane.active_item().unwrap().id(), item4.id());
2919 });
2920
2921 cx.simulate_prompt_answer(window_id, 0);
2922 cx.foreground().run_until_parked();
2923 cx.simulate_new_path_selection(|_| Some(Default::default()));
2924 close_items.await.unwrap();
2925 pane.read_with(cx, |pane, cx| {
2926 assert_eq!(item4.read(cx).save_count, 0);
2927 assert_eq!(item4.read(cx).save_as_count, 1);
2928 assert_eq!(item4.read(cx).reload_count, 0);
2929 assert_eq!(pane.items().count(), 1);
2930 assert_eq!(pane.active_item().unwrap().id(), item2.id());
2931 });
2932 }
2933
2934 #[gpui::test]
2935 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
2936 cx.foreground().forbid_parking();
2937 Settings::test_async(cx);
2938 let fs = FakeFs::new(cx.background());
2939
2940 let project = Project::test(fs, [], cx).await;
2941 let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
2942
2943 // Create several workspace items with single project entries, and two
2944 // workspace items with multiple project entries.
2945 let single_entry_items = (0..=4)
2946 .map(|project_entry_id| {
2947 let mut item = TestItem::new();
2948 item.is_dirty = true;
2949 item.project_entry_ids = vec![ProjectEntryId::from_proto(project_entry_id)];
2950 item.is_singleton = true;
2951 item
2952 })
2953 .collect::<Vec<_>>();
2954 let item_2_3 = {
2955 let mut item = TestItem::new();
2956 item.is_dirty = true;
2957 item.is_singleton = false;
2958 item.project_entry_ids =
2959 vec![ProjectEntryId::from_proto(2), ProjectEntryId::from_proto(3)];
2960 item
2961 };
2962 let item_3_4 = {
2963 let mut item = TestItem::new();
2964 item.is_dirty = true;
2965 item.is_singleton = false;
2966 item.project_entry_ids =
2967 vec![ProjectEntryId::from_proto(3), ProjectEntryId::from_proto(4)];
2968 item
2969 };
2970
2971 // Create two panes that contain the following project entries:
2972 // left pane:
2973 // multi-entry items: (2, 3)
2974 // single-entry items: 0, 1, 2, 3, 4
2975 // right pane:
2976 // single-entry items: 1
2977 // multi-entry items: (3, 4)
2978 let left_pane = workspace.update(cx, |workspace, cx| {
2979 let left_pane = workspace.active_pane().clone();
2980 let right_pane = workspace.split_pane(left_pane.clone(), SplitDirection::Right, cx);
2981
2982 workspace.activate_pane(left_pane.clone(), cx);
2983 workspace.add_item(Box::new(cx.add_view(|_| item_2_3.clone())), cx);
2984 for item in &single_entry_items {
2985 workspace.add_item(Box::new(cx.add_view(|_| item.clone())), cx);
2986 }
2987
2988 workspace.activate_pane(right_pane.clone(), cx);
2989 workspace.add_item(Box::new(cx.add_view(|_| single_entry_items[1].clone())), cx);
2990 workspace.add_item(Box::new(cx.add_view(|_| item_3_4.clone())), cx);
2991
2992 left_pane
2993 });
2994
2995 // When closing all of the items in the left pane, we should be prompted twice:
2996 // once for project entry 0, and once for project entry 2. After those two
2997 // prompts, the task should complete.
2998 let close = workspace.update(cx, |workspace, cx| {
2999 workspace.activate_pane(left_pane.clone(), cx);
3000 Pane::close_items(workspace, left_pane.clone(), cx, |_| true)
3001 });
3002
3003 cx.foreground().run_until_parked();
3004 left_pane.read_with(cx, |pane, cx| {
3005 assert_eq!(
3006 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
3007 &[ProjectEntryId::from_proto(0)]
3008 );
3009 });
3010 cx.simulate_prompt_answer(window_id, 0);
3011
3012 cx.foreground().run_until_parked();
3013 left_pane.read_with(cx, |pane, cx| {
3014 assert_eq!(
3015 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
3016 &[ProjectEntryId::from_proto(2)]
3017 );
3018 });
3019 cx.simulate_prompt_answer(window_id, 0);
3020
3021 cx.foreground().run_until_parked();
3022 close.await.unwrap();
3023 left_pane.read_with(cx, |pane, _| {
3024 assert_eq!(pane.items().count(), 0);
3025 });
3026 }
3027
3028 #[gpui::test]
3029 async fn test_autosave(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
3030 deterministic.forbid_parking();
3031
3032 Settings::test_async(cx);
3033 let fs = FakeFs::new(cx.background());
3034
3035 let project = Project::test(fs, [], cx).await;
3036 let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
3037
3038 let item = cx.add_view(window_id, |_| {
3039 let mut item = TestItem::new();
3040 item.project_entry_ids = vec![ProjectEntryId::from_proto(1)];
3041 item
3042 });
3043 let item_id = item.id();
3044 workspace.update(cx, |workspace, cx| {
3045 workspace.add_item(Box::new(item.clone()), cx);
3046 });
3047
3048 // Autosave on window change.
3049 item.update(cx, |item, cx| {
3050 cx.update_global(|settings: &mut Settings, _| {
3051 settings.autosave = Autosave::OnWindowChange;
3052 });
3053 item.is_dirty = true;
3054 });
3055
3056 // Deactivating the window saves the file.
3057 cx.simulate_window_activation(None);
3058 deterministic.run_until_parked();
3059 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
3060
3061 // Autosave on focus change.
3062 item.update(cx, |item, cx| {
3063 cx.focus_self();
3064 cx.update_global(|settings: &mut Settings, _| {
3065 settings.autosave = Autosave::OnFocusChange;
3066 });
3067 item.is_dirty = true;
3068 });
3069
3070 // Blurring the item saves the file.
3071 item.update(cx, |_, cx| cx.blur());
3072 deterministic.run_until_parked();
3073 item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
3074
3075 // Deactivating the window still saves the file.
3076 cx.simulate_window_activation(Some(window_id));
3077 item.update(cx, |item, cx| {
3078 cx.focus_self();
3079 item.is_dirty = true;
3080 });
3081 cx.simulate_window_activation(None);
3082
3083 deterministic.run_until_parked();
3084 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
3085
3086 // Autosave after delay.
3087 item.update(cx, |item, cx| {
3088 cx.update_global(|settings: &mut Settings, _| {
3089 settings.autosave = Autosave::AfterDelay { milliseconds: 500 };
3090 });
3091 item.is_dirty = true;
3092 cx.emit(TestItemEvent::Edit);
3093 });
3094
3095 // Delay hasn't fully expired, so the file is still dirty and unsaved.
3096 deterministic.advance_clock(Duration::from_millis(250));
3097 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
3098
3099 // After delay expires, the file is saved.
3100 deterministic.advance_clock(Duration::from_millis(250));
3101 item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
3102
3103 // Autosave on focus change, ensuring closing the tab counts as such.
3104 item.update(cx, |item, cx| {
3105 cx.update_global(|settings: &mut Settings, _| {
3106 settings.autosave = Autosave::OnFocusChange;
3107 });
3108 item.is_dirty = true;
3109 });
3110
3111 workspace
3112 .update(cx, |workspace, cx| {
3113 let pane = workspace.active_pane().clone();
3114 Pane::close_items(workspace, pane, cx, move |id| id == item_id)
3115 })
3116 .await
3117 .unwrap();
3118 assert!(!cx.has_pending_prompt(window_id));
3119 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
3120
3121 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
3122 workspace.update(cx, |workspace, cx| {
3123 workspace.add_item(Box::new(item.clone()), cx);
3124 });
3125 item.update(cx, |item, cx| {
3126 item.project_entry_ids = Default::default();
3127 item.is_dirty = true;
3128 cx.blur();
3129 });
3130 deterministic.run_until_parked();
3131 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
3132
3133 // Ensure autosave is prevented for deleted files also when closing the buffer.
3134 let _close_items = workspace.update(cx, |workspace, cx| {
3135 let pane = workspace.active_pane().clone();
3136 Pane::close_items(workspace, pane, cx, move |id| id == item_id)
3137 });
3138 deterministic.run_until_parked();
3139 assert!(cx.has_pending_prompt(window_id));
3140 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
3141 }
3142
3143 #[gpui::test]
3144 async fn test_pane_navigation(
3145 deterministic: Arc<Deterministic>,
3146 cx: &mut gpui::TestAppContext,
3147 ) {
3148 deterministic.forbid_parking();
3149 Settings::test_async(cx);
3150 let fs = FakeFs::new(cx.background());
3151
3152 let project = Project::test(fs, [], cx).await;
3153 let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
3154
3155 let item = cx.add_view(window_id, |_| {
3156 let mut item = TestItem::new();
3157 item.project_entry_ids = vec![ProjectEntryId::from_proto(1)];
3158 item
3159 });
3160 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3161 let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone());
3162 let toolbar_notify_count = Rc::new(RefCell::new(0));
3163
3164 workspace.update(cx, |workspace, cx| {
3165 workspace.add_item(Box::new(item.clone()), cx);
3166 let toolbar_notification_count = toolbar_notify_count.clone();
3167 cx.observe(&toolbar, move |_, _, _| {
3168 *toolbar_notification_count.borrow_mut() += 1
3169 })
3170 .detach();
3171 });
3172
3173 pane.read_with(cx, |pane, _| {
3174 assert!(!pane.can_navigate_backward());
3175 assert!(!pane.can_navigate_forward());
3176 });
3177
3178 item.update(cx, |item, cx| {
3179 item.set_state("one".to_string(), cx);
3180 });
3181
3182 // Toolbar must be notified to re-render the navigation buttons
3183 assert_eq!(*toolbar_notify_count.borrow(), 1);
3184
3185 pane.read_with(cx, |pane, _| {
3186 assert!(pane.can_navigate_backward());
3187 assert!(!pane.can_navigate_forward());
3188 });
3189
3190 workspace
3191 .update(cx, |workspace, cx| {
3192 Pane::go_back(workspace, Some(pane.clone()), cx)
3193 })
3194 .await;
3195
3196 assert_eq!(*toolbar_notify_count.borrow(), 3);
3197 pane.read_with(cx, |pane, _| {
3198 assert!(!pane.can_navigate_backward());
3199 assert!(pane.can_navigate_forward());
3200 });
3201 }
3202
3203 struct TestItem {
3204 state: String,
3205 save_count: usize,
3206 save_as_count: usize,
3207 reload_count: usize,
3208 is_dirty: bool,
3209 is_singleton: bool,
3210 has_conflict: bool,
3211 project_entry_ids: Vec<ProjectEntryId>,
3212 project_path: Option<ProjectPath>,
3213 nav_history: Option<ItemNavHistory>,
3214 }
3215
3216 enum TestItemEvent {
3217 Edit,
3218 }
3219
3220 impl Clone for TestItem {
3221 fn clone(&self) -> Self {
3222 Self {
3223 state: self.state.clone(),
3224 save_count: self.save_count,
3225 save_as_count: self.save_as_count,
3226 reload_count: self.reload_count,
3227 is_dirty: self.is_dirty,
3228 is_singleton: self.is_singleton,
3229 has_conflict: self.has_conflict,
3230 project_entry_ids: self.project_entry_ids.clone(),
3231 project_path: self.project_path.clone(),
3232 nav_history: None,
3233 }
3234 }
3235 }
3236
3237 impl TestItem {
3238 fn new() -> Self {
3239 Self {
3240 state: String::new(),
3241 save_count: 0,
3242 save_as_count: 0,
3243 reload_count: 0,
3244 is_dirty: false,
3245 has_conflict: false,
3246 project_entry_ids: Vec::new(),
3247 project_path: None,
3248 is_singleton: true,
3249 nav_history: None,
3250 }
3251 }
3252
3253 fn set_state(&mut self, state: String, cx: &mut ViewContext<Self>) {
3254 self.push_to_nav_history(cx);
3255 self.state = state;
3256 }
3257
3258 fn push_to_nav_history(&mut self, cx: &mut ViewContext<Self>) {
3259 if let Some(history) = &mut self.nav_history {
3260 history.push(Some(Box::new(self.state.clone())), cx);
3261 }
3262 }
3263 }
3264
3265 impl Entity for TestItem {
3266 type Event = TestItemEvent;
3267 }
3268
3269 impl View for TestItem {
3270 fn ui_name() -> &'static str {
3271 "TestItem"
3272 }
3273
3274 fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
3275 Empty::new().boxed()
3276 }
3277 }
3278
3279 impl Item for TestItem {
3280 fn tab_content(&self, _: &theme::Tab, _: &AppContext) -> ElementBox {
3281 Empty::new().boxed()
3282 }
3283
3284 fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
3285 self.project_path.clone()
3286 }
3287
3288 fn project_entry_ids(&self, _: &AppContext) -> SmallVec<[ProjectEntryId; 3]> {
3289 self.project_entry_ids.iter().copied().collect()
3290 }
3291
3292 fn is_singleton(&self, _: &AppContext) -> bool {
3293 self.is_singleton
3294 }
3295
3296 fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext<Self>) {
3297 self.nav_history = Some(history);
3298 }
3299
3300 fn navigate(&mut self, state: Box<dyn Any>, _: &mut ViewContext<Self>) -> bool {
3301 let state = *state.downcast::<String>().unwrap_or_default();
3302 if state != self.state {
3303 self.state = state;
3304 true
3305 } else {
3306 false
3307 }
3308 }
3309
3310 fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
3311 self.push_to_nav_history(cx);
3312 }
3313
3314 fn clone_on_split(&self, _: &mut ViewContext<Self>) -> Option<Self>
3315 where
3316 Self: Sized,
3317 {
3318 Some(self.clone())
3319 }
3320
3321 fn is_dirty(&self, _: &AppContext) -> bool {
3322 self.is_dirty
3323 }
3324
3325 fn has_conflict(&self, _: &AppContext) -> bool {
3326 self.has_conflict
3327 }
3328
3329 fn can_save(&self, _: &AppContext) -> bool {
3330 self.project_entry_ids.len() > 0
3331 }
3332
3333 fn save(
3334 &mut self,
3335 _: ModelHandle<Project>,
3336 _: &mut ViewContext<Self>,
3337 ) -> Task<anyhow::Result<()>> {
3338 self.save_count += 1;
3339 self.is_dirty = false;
3340 Task::ready(Ok(()))
3341 }
3342
3343 fn save_as(
3344 &mut self,
3345 _: ModelHandle<Project>,
3346 _: std::path::PathBuf,
3347 _: &mut ViewContext<Self>,
3348 ) -> Task<anyhow::Result<()>> {
3349 self.save_as_count += 1;
3350 self.is_dirty = false;
3351 Task::ready(Ok(()))
3352 }
3353
3354 fn reload(
3355 &mut self,
3356 _: ModelHandle<Project>,
3357 _: &mut ViewContext<Self>,
3358 ) -> Task<anyhow::Result<()>> {
3359 self.reload_count += 1;
3360 self.is_dirty = false;
3361 Task::ready(Ok(()))
3362 }
3363
3364 fn should_update_tab_on_event(_: &Self::Event) -> bool {
3365 true
3366 }
3367
3368 fn is_edit_event(event: &Self::Event) -> bool {
3369 matches!(event, TestItemEvent::Edit)
3370 }
3371 }
3372}