item.rs

   1use std::{
   2    any::{Any, TypeId},
   3    borrow::Cow,
   4    cell::RefCell,
   5    fmt,
   6    ops::Range,
   7    path::PathBuf,
   8    rc::Rc,
   9    sync::{
  10        atomic::{AtomicBool, Ordering},
  11        Arc,
  12    },
  13    time::Duration,
  14};
  15
  16use anyhow::Result;
  17use client::{proto, Client};
  18use gpui::{
  19    fonts::HighlightStyle, AnyViewHandle, AppContext, Element, ModelHandle, Task, View,
  20    ViewContext, ViewHandle, WeakViewHandle,
  21};
  22use project::{Project, ProjectEntryId, ProjectPath};
  23use settings::{Autosave, Settings};
  24use smallvec::SmallVec;
  25use theme::Theme;
  26use util::ResultExt;
  27
  28use crate::{
  29    pane, persistence::model::ItemId, searchable::SearchableItemHandle, DelayedDebouncedEditAction,
  30    FollowableItemBuilders, ItemNavHistory, Pane, ToolbarItemLocation, ViewId, Workspace,
  31    WorkspaceId,
  32};
  33
  34#[derive(Eq, PartialEq, Hash)]
  35pub enum ItemEvent {
  36    CloseItem,
  37    UpdateTab,
  38    UpdateBreadcrumbs,
  39    Edit,
  40}
  41
  42// TODO: Combine this with existing HighlightedText struct?
  43pub struct BreadcrumbText {
  44    pub text: String,
  45    pub highlights: Option<Vec<(Range<usize>, HighlightStyle)>>,
  46}
  47
  48pub trait Item: View {
  49    fn deactivated(&mut self, _: &mut ViewContext<Self>) {}
  50    fn workspace_deactivated(&mut self, _: &mut ViewContext<Self>) {}
  51    fn navigate(&mut self, _: Box<dyn Any>, _: &mut ViewContext<Self>) -> bool {
  52        false
  53    }
  54    fn tab_description<'a>(&'a self, _: usize, _: &'a AppContext) -> Option<Cow<'a, str>> {
  55        None
  56    }
  57    fn tab_content(
  58        &self,
  59        detail: Option<usize>,
  60        style: &theme::Tab,
  61        cx: &AppContext,
  62    ) -> Element<Pane>;
  63    fn for_each_project_item(&self, _: &AppContext, _: &mut dyn FnMut(usize, &dyn project::Item)) {}
  64    fn is_singleton(&self, _cx: &AppContext) -> bool {
  65        false
  66    }
  67    fn set_nav_history(&mut self, _: ItemNavHistory, _: &mut ViewContext<Self>) {}
  68    fn clone_on_split(&self, _workspace_id: WorkspaceId, _: &mut ViewContext<Self>) -> Option<Self>
  69    where
  70        Self: Sized,
  71    {
  72        None
  73    }
  74    fn is_dirty(&self, _: &AppContext) -> bool {
  75        false
  76    }
  77    fn has_conflict(&self, _: &AppContext) -> bool {
  78        false
  79    }
  80    fn can_save(&self, _cx: &AppContext) -> bool {
  81        false
  82    }
  83    fn save(
  84        &mut self,
  85        _project: ModelHandle<Project>,
  86        _cx: &mut ViewContext<Self>,
  87    ) -> Task<Result<()>> {
  88        unimplemented!("save() must be implemented if can_save() returns true")
  89    }
  90    fn save_as(
  91        &mut self,
  92        _project: ModelHandle<Project>,
  93        _abs_path: PathBuf,
  94        _cx: &mut ViewContext<Self>,
  95    ) -> Task<Result<()>> {
  96        unimplemented!("save_as() must be implemented if can_save() returns true")
  97    }
  98    fn reload(
  99        &mut self,
 100        _project: ModelHandle<Project>,
 101        _cx: &mut ViewContext<Self>,
 102    ) -> Task<Result<()>> {
 103        unimplemented!("reload() must be implemented if can_save() returns true")
 104    }
 105    fn git_diff_recalc(
 106        &mut self,
 107        _project: ModelHandle<Project>,
 108        _cx: &mut ViewContext<Self>,
 109    ) -> Task<Result<()>> {
 110        Task::ready(Ok(()))
 111    }
 112    fn to_item_events(_event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
 113        SmallVec::new()
 114    }
 115    fn should_close_item_on_event(_: &Self::Event) -> bool {
 116        false
 117    }
 118    fn should_update_tab_on_event(_: &Self::Event) -> bool {
 119        false
 120    }
 121    fn is_edit_event(_: &Self::Event) -> bool {
 122        false
 123    }
 124    fn act_as_type<'a>(
 125        &'a self,
 126        type_id: TypeId,
 127        self_handle: &'a ViewHandle<Self>,
 128        _: &'a AppContext,
 129    ) -> Option<&AnyViewHandle> {
 130        if TypeId::of::<Self>() == type_id {
 131            Some(self_handle)
 132        } else {
 133            None
 134        }
 135    }
 136    fn as_searchable(&self, _: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
 137        None
 138    }
 139
 140    fn breadcrumb_location(&self) -> ToolbarItemLocation {
 141        ToolbarItemLocation::Hidden
 142    }
 143
 144    fn breadcrumbs(&self, _theme: &Theme, _cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
 145        None
 146    }
 147
 148    fn added_to_workspace(&mut self, _workspace: &mut Workspace, _cx: &mut ViewContext<Self>) {}
 149
 150    fn serialized_item_kind() -> Option<&'static str> {
 151        None
 152    }
 153
 154    fn deserialize(
 155        _project: ModelHandle<Project>,
 156        _workspace: WeakViewHandle<Workspace>,
 157        _workspace_id: WorkspaceId,
 158        _item_id: ItemId,
 159        _cx: &mut ViewContext<Pane>,
 160    ) -> Task<Result<ViewHandle<Self>>> {
 161        unimplemented!(
 162            "deserialize() must be implemented if serialized_item_kind() returns Some(_)"
 163        )
 164    }
 165    fn show_toolbar(&self) -> bool {
 166        true
 167    }
 168}
 169
 170pub trait ItemHandle: 'static + fmt::Debug {
 171    fn subscribe_to_item_events(
 172        &self,
 173        cx: &mut AppContext,
 174        handler: Box<dyn Fn(ItemEvent, &mut AppContext)>,
 175    ) -> gpui::Subscription;
 176    fn tab_description<'a>(&self, detail: usize, cx: &'a AppContext) -> Option<Cow<'a, str>>;
 177    fn tab_content(
 178        &self,
 179        detail: Option<usize>,
 180        style: &theme::Tab,
 181        cx: &AppContext,
 182    ) -> Element<Pane>;
 183    fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
 184    fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]>;
 185    fn project_item_model_ids(&self, cx: &AppContext) -> SmallVec<[usize; 3]>;
 186    fn for_each_project_item(&self, _: &AppContext, _: &mut dyn FnMut(usize, &dyn project::Item));
 187    fn is_singleton(&self, cx: &AppContext) -> bool;
 188    fn boxed_clone(&self) -> Box<dyn ItemHandle>;
 189    fn clone_on_split(
 190        &self,
 191        workspace_id: WorkspaceId,
 192        cx: &mut AppContext,
 193    ) -> Option<Box<dyn ItemHandle>>;
 194    fn added_to_pane(
 195        &self,
 196        workspace: &mut Workspace,
 197        pane: ViewHandle<Pane>,
 198        cx: &mut ViewContext<Workspace>,
 199    );
 200    fn deactivated(&self, cx: &mut AppContext);
 201    fn workspace_deactivated(&self, cx: &mut AppContext);
 202    fn navigate(&self, data: Box<dyn Any>, cx: &mut AppContext) -> bool;
 203    fn id(&self) -> usize;
 204    fn window_id(&self) -> usize;
 205    fn as_any(&self) -> &AnyViewHandle;
 206    fn is_dirty(&self, cx: &AppContext) -> bool;
 207    fn has_conflict(&self, cx: &AppContext) -> bool;
 208    fn can_save(&self, cx: &AppContext) -> bool;
 209    fn save(&self, project: ModelHandle<Project>, cx: &mut AppContext) -> Task<Result<()>>;
 210    fn save_as(
 211        &self,
 212        project: ModelHandle<Project>,
 213        abs_path: PathBuf,
 214        cx: &mut AppContext,
 215    ) -> Task<Result<()>>;
 216    fn reload(&self, project: ModelHandle<Project>, cx: &mut AppContext) -> Task<Result<()>>;
 217    fn git_diff_recalc(
 218        &self,
 219        project: ModelHandle<Project>,
 220        cx: &mut AppContext,
 221    ) -> Task<Result<()>>;
 222    fn act_as_type<'a>(&'a self, type_id: TypeId, cx: &'a AppContext) -> Option<&'a AnyViewHandle>;
 223    fn to_followable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn FollowableItemHandle>>;
 224    fn on_release(
 225        &self,
 226        cx: &mut AppContext,
 227        callback: Box<dyn FnOnce(&mut AppContext)>,
 228    ) -> gpui::Subscription;
 229    fn to_searchable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn SearchableItemHandle>>;
 230    fn breadcrumb_location(&self, cx: &AppContext) -> ToolbarItemLocation;
 231    fn breadcrumbs(&self, theme: &Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>>;
 232    fn serialized_item_kind(&self) -> Option<&'static str>;
 233    fn show_toolbar(&self, cx: &AppContext) -> bool;
 234}
 235
 236pub trait WeakItemHandle {
 237    fn id(&self) -> usize;
 238    fn window_id(&self) -> usize;
 239    fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>>;
 240}
 241
 242impl dyn ItemHandle {
 243    pub fn downcast<T: View>(&self) -> Option<ViewHandle<T>> {
 244        self.as_any().clone().downcast()
 245    }
 246
 247    pub fn act_as<T: View>(&self, cx: &AppContext) -> Option<ViewHandle<T>> {
 248        self.act_as_type(TypeId::of::<T>(), cx)
 249            .and_then(|t| t.clone().downcast())
 250    }
 251}
 252
 253impl<T: Item> ItemHandle for ViewHandle<T> {
 254    fn subscribe_to_item_events(
 255        &self,
 256        cx: &mut AppContext,
 257        handler: Box<dyn Fn(ItemEvent, &mut AppContext)>,
 258    ) -> gpui::Subscription {
 259        cx.subscribe(self, move |_, event, cx| {
 260            for item_event in T::to_item_events(event) {
 261                handler(item_event, cx)
 262            }
 263        })
 264    }
 265
 266    fn tab_description<'a>(&self, detail: usize, cx: &'a AppContext) -> Option<Cow<'a, str>> {
 267        self.read(cx).tab_description(detail, cx)
 268    }
 269
 270    fn tab_content(
 271        &self,
 272        detail: Option<usize>,
 273        style: &theme::Tab,
 274        cx: &AppContext,
 275    ) -> Element<Pane> {
 276        self.read(cx).tab_content(detail, style, cx)
 277    }
 278
 279    fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
 280        let this = self.read(cx);
 281        let mut result = None;
 282        if this.is_singleton(cx) {
 283            this.for_each_project_item(cx, &mut |_, item| {
 284                result = item.project_path(cx);
 285            });
 286        }
 287        result
 288    }
 289
 290    fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]> {
 291        let mut result = SmallVec::new();
 292        self.read(cx).for_each_project_item(cx, &mut |_, item| {
 293            if let Some(id) = item.entry_id(cx) {
 294                result.push(id);
 295            }
 296        });
 297        result
 298    }
 299
 300    fn project_item_model_ids(&self, cx: &AppContext) -> SmallVec<[usize; 3]> {
 301        let mut result = SmallVec::new();
 302        self.read(cx).for_each_project_item(cx, &mut |id, _| {
 303            result.push(id);
 304        });
 305        result
 306    }
 307
 308    fn for_each_project_item(&self, cx: &AppContext, f: &mut dyn FnMut(usize, &dyn project::Item)) {
 309        self.read(cx).for_each_project_item(cx, f)
 310    }
 311
 312    fn is_singleton(&self, cx: &AppContext) -> bool {
 313        self.read(cx).is_singleton(cx)
 314    }
 315
 316    fn boxed_clone(&self) -> Box<dyn ItemHandle> {
 317        Box::new(self.clone())
 318    }
 319
 320    fn clone_on_split(
 321        &self,
 322        workspace_id: WorkspaceId,
 323        cx: &mut AppContext,
 324    ) -> Option<Box<dyn ItemHandle>> {
 325        self.update(cx, |item, cx| {
 326            cx.add_option_view(|cx| item.clone_on_split(workspace_id, cx))
 327        })
 328        .map(|handle| Box::new(handle) as Box<dyn ItemHandle>)
 329    }
 330
 331    fn added_to_pane(
 332        &self,
 333        workspace: &mut Workspace,
 334        pane: ViewHandle<Pane>,
 335        cx: &mut ViewContext<Workspace>,
 336    ) {
 337        let history = pane.read(cx).nav_history_for_item(self);
 338        self.update(cx, |this, cx| {
 339            this.set_nav_history(history, cx);
 340            this.added_to_workspace(workspace, cx);
 341        });
 342
 343        if let Some(followed_item) = self.to_followable_item_handle(cx) {
 344            if let Some(message) = followed_item.to_state_proto(cx) {
 345                workspace.update_followers(
 346                    proto::update_followers::Variant::CreateView(proto::View {
 347                        id: followed_item
 348                            .remote_id(&workspace.client, cx)
 349                            .map(|id| id.to_proto()),
 350                        variant: Some(message),
 351                        leader_id: workspace.leader_for_pane(&pane),
 352                    }),
 353                    cx,
 354                );
 355            }
 356        }
 357
 358        if workspace
 359            .panes_by_item
 360            .insert(self.id(), pane.downgrade())
 361            .is_none()
 362        {
 363            let mut pending_autosave = DelayedDebouncedEditAction::new();
 364            let mut pending_git_update = DelayedDebouncedEditAction::new();
 365            let pending_update = Rc::new(RefCell::new(None));
 366            let pending_update_scheduled = Rc::new(AtomicBool::new(false));
 367
 368            let mut event_subscription =
 369                Some(cx.subscribe(self, move |workspace, item, event, cx| {
 370                    let pane = if let Some(pane) = workspace
 371                        .panes_by_item
 372                        .get(&item.id())
 373                        .and_then(|pane| pane.upgrade(cx))
 374                    {
 375                        pane
 376                    } else {
 377                        log::error!("unexpected item event after pane was dropped");
 378                        return;
 379                    };
 380
 381                    if let Some(item) = item.to_followable_item_handle(cx) {
 382                        let leader_id = workspace.leader_for_pane(&pane);
 383
 384                        if leader_id.is_some() && item.should_unfollow_on_event(event, cx) {
 385                            workspace.unfollow(&pane, cx);
 386                        }
 387
 388                        if item.add_event_to_update_proto(
 389                            event,
 390                            &mut *pending_update.borrow_mut(),
 391                            cx,
 392                        ) && !pending_update_scheduled.load(Ordering::SeqCst)
 393                        {
 394                            pending_update_scheduled.store(true, Ordering::SeqCst);
 395                            cx.after_window_update({
 396                                let pending_update = pending_update.clone();
 397                                let pending_update_scheduled = pending_update_scheduled.clone();
 398                                move |this, cx| {
 399                                    pending_update_scheduled.store(false, Ordering::SeqCst);
 400                                    this.update_followers(
 401                                        proto::update_followers::Variant::UpdateView(
 402                                            proto::UpdateView {
 403                                                id: item
 404                                                    .remote_id(&this.client, cx)
 405                                                    .map(|id| id.to_proto()),
 406                                                variant: pending_update.borrow_mut().take(),
 407                                                leader_id,
 408                                            },
 409                                        ),
 410                                        cx,
 411                                    );
 412                                }
 413                            });
 414                        }
 415                    }
 416
 417                    for item_event in T::to_item_events(event).into_iter() {
 418                        match item_event {
 419                            ItemEvent::CloseItem => {
 420                                Pane::close_item(workspace, pane, item.id(), cx)
 421                                    .detach_and_log_err(cx);
 422                                return;
 423                            }
 424
 425                            ItemEvent::UpdateTab => {
 426                                pane.update(cx, |_, cx| {
 427                                    cx.emit(pane::Event::ChangeItemTitle);
 428                                    cx.notify();
 429                                });
 430                            }
 431
 432                            ItemEvent::Edit => {
 433                                if let Autosave::AfterDelay { milliseconds } =
 434                                    cx.global::<Settings>().autosave
 435                                {
 436                                    let delay = Duration::from_millis(milliseconds);
 437                                    let item = item.clone();
 438                                    pending_autosave.fire_new(
 439                                        delay,
 440                                        workspace,
 441                                        cx,
 442                                        |project, mut cx| async move {
 443                                            cx.update(|cx| Pane::autosave_item(&item, project, cx))
 444                                                .await
 445                                                .log_err();
 446                                        },
 447                                    );
 448                                }
 449
 450                                let settings = cx.global::<Settings>();
 451                                let debounce_delay = settings.git_overrides.gutter_debounce;
 452
 453                                let item = item.clone();
 454
 455                                if let Some(delay) = debounce_delay {
 456                                    const MIN_GIT_DELAY: u64 = 50;
 457
 458                                    let delay = delay.max(MIN_GIT_DELAY);
 459                                    let duration = Duration::from_millis(delay);
 460
 461                                    pending_git_update.fire_new(
 462                                        duration,
 463                                        workspace,
 464                                        cx,
 465                                        |project, mut cx| async move {
 466                                            cx.update(|cx| item.git_diff_recalc(project, cx))
 467                                                .await
 468                                                .log_err();
 469                                        },
 470                                    );
 471                                } else {
 472                                    let project = workspace.project().downgrade();
 473                                    cx.spawn_weak(|_, mut cx| async move {
 474                                        if let Some(project) = project.upgrade(&cx) {
 475                                            cx.update(|cx| item.git_diff_recalc(project, cx))
 476                                                .await
 477                                                .log_err();
 478                                        }
 479                                    })
 480                                    .detach();
 481                                }
 482                            }
 483
 484                            _ => {}
 485                        }
 486                    }
 487                }));
 488
 489            cx.observe_focus(self, move |workspace, item, focused, cx| {
 490                if !focused && cx.global::<Settings>().autosave == Autosave::OnFocusChange {
 491                    Pane::autosave_item(&item, workspace.project.clone(), cx)
 492                        .detach_and_log_err(cx);
 493                }
 494            })
 495            .detach();
 496
 497            let item_id = self.id();
 498            cx.observe_release(self, move |workspace, _, _| {
 499                workspace.panes_by_item.remove(&item_id);
 500                event_subscription.take();
 501            })
 502            .detach();
 503        }
 504
 505        cx.defer(|workspace, cx| {
 506            workspace.serialize_workspace(cx);
 507        });
 508    }
 509
 510    fn deactivated(&self, cx: &mut AppContext) {
 511        self.update(cx, |this, cx| this.deactivated(cx));
 512    }
 513
 514    fn workspace_deactivated(&self, cx: &mut AppContext) {
 515        self.update(cx, |this, cx| this.workspace_deactivated(cx));
 516    }
 517
 518    fn navigate(&self, data: Box<dyn Any>, cx: &mut AppContext) -> bool {
 519        self.update(cx, |this, cx| this.navigate(data, cx))
 520    }
 521
 522    fn id(&self) -> usize {
 523        self.id()
 524    }
 525
 526    fn window_id(&self) -> usize {
 527        self.window_id()
 528    }
 529
 530    fn as_any(&self) -> &AnyViewHandle {
 531        self
 532    }
 533
 534    fn is_dirty(&self, cx: &AppContext) -> bool {
 535        self.read(cx).is_dirty(cx)
 536    }
 537
 538    fn has_conflict(&self, cx: &AppContext) -> bool {
 539        self.read(cx).has_conflict(cx)
 540    }
 541
 542    fn can_save(&self, cx: &AppContext) -> bool {
 543        self.read(cx).can_save(cx)
 544    }
 545
 546    fn save(&self, project: ModelHandle<Project>, cx: &mut AppContext) -> Task<Result<()>> {
 547        self.update(cx, |item, cx| item.save(project, cx))
 548    }
 549
 550    fn save_as(
 551        &self,
 552        project: ModelHandle<Project>,
 553        abs_path: PathBuf,
 554        cx: &mut AppContext,
 555    ) -> Task<anyhow::Result<()>> {
 556        self.update(cx, |item, cx| item.save_as(project, abs_path, cx))
 557    }
 558
 559    fn reload(&self, project: ModelHandle<Project>, cx: &mut AppContext) -> Task<Result<()>> {
 560        self.update(cx, |item, cx| item.reload(project, cx))
 561    }
 562
 563    fn git_diff_recalc(
 564        &self,
 565        project: ModelHandle<Project>,
 566        cx: &mut AppContext,
 567    ) -> Task<Result<()>> {
 568        self.update(cx, |item, cx| item.git_diff_recalc(project, cx))
 569    }
 570
 571    fn act_as_type<'a>(&'a self, type_id: TypeId, cx: &'a AppContext) -> Option<&'a AnyViewHandle> {
 572        self.read(cx).act_as_type(type_id, self, cx)
 573    }
 574
 575    fn to_followable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn FollowableItemHandle>> {
 576        if cx.has_global::<FollowableItemBuilders>() {
 577            let builders = cx.global::<FollowableItemBuilders>();
 578            let item = self.as_any();
 579            Some(builders.get(&item.view_type())?.1(item))
 580        } else {
 581            None
 582        }
 583    }
 584
 585    fn on_release(
 586        &self,
 587        cx: &mut AppContext,
 588        callback: Box<dyn FnOnce(&mut AppContext)>,
 589    ) -> gpui::Subscription {
 590        cx.observe_release(self, move |_, cx| callback(cx))
 591    }
 592
 593    fn to_searchable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn SearchableItemHandle>> {
 594        self.read(cx).as_searchable(self)
 595    }
 596
 597    fn breadcrumb_location(&self, cx: &AppContext) -> ToolbarItemLocation {
 598        self.read(cx).breadcrumb_location()
 599    }
 600
 601    fn breadcrumbs(&self, theme: &Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
 602        self.read(cx).breadcrumbs(theme, cx)
 603    }
 604
 605    fn serialized_item_kind(&self) -> Option<&'static str> {
 606        T::serialized_item_kind()
 607    }
 608
 609    fn show_toolbar(&self, cx: &AppContext) -> bool {
 610        self.read(cx).show_toolbar()
 611    }
 612}
 613
 614impl From<Box<dyn ItemHandle>> for AnyViewHandle {
 615    fn from(val: Box<dyn ItemHandle>) -> Self {
 616        val.as_any().clone()
 617    }
 618}
 619
 620impl From<&Box<dyn ItemHandle>> for AnyViewHandle {
 621    fn from(val: &Box<dyn ItemHandle>) -> Self {
 622        val.as_any().clone()
 623    }
 624}
 625
 626impl Clone for Box<dyn ItemHandle> {
 627    fn clone(&self) -> Box<dyn ItemHandle> {
 628        self.boxed_clone()
 629    }
 630}
 631
 632impl<T: Item> WeakItemHandle for WeakViewHandle<T> {
 633    fn id(&self) -> usize {
 634        self.id()
 635    }
 636
 637    fn window_id(&self) -> usize {
 638        self.window_id()
 639    }
 640
 641    fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
 642        self.upgrade(cx).map(|v| Box::new(v) as Box<dyn ItemHandle>)
 643    }
 644}
 645
 646pub trait ProjectItem: Item {
 647    type Item: project::Item + gpui::Entity;
 648
 649    fn for_project_item(
 650        project: ModelHandle<Project>,
 651        item: ModelHandle<Self::Item>,
 652        cx: &mut ViewContext<Self>,
 653    ) -> Self;
 654}
 655
 656pub trait FollowableItem: Item {
 657    fn remote_id(&self) -> Option<ViewId>;
 658    fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant>;
 659    fn from_state_proto(
 660        pane: ViewHandle<Pane>,
 661        project: ModelHandle<Project>,
 662        id: ViewId,
 663        state: &mut Option<proto::view::Variant>,
 664        cx: &mut AppContext,
 665    ) -> Option<Task<Result<ViewHandle<Self>>>>;
 666    fn add_event_to_update_proto(
 667        &self,
 668        event: &Self::Event,
 669        update: &mut Option<proto::update_view::Variant>,
 670        cx: &AppContext,
 671    ) -> bool;
 672    fn apply_update_proto(
 673        &mut self,
 674        project: &ModelHandle<Project>,
 675        message: proto::update_view::Variant,
 676        cx: &mut ViewContext<Self>,
 677    ) -> Task<Result<()>>;
 678
 679    fn set_leader_replica_id(&mut self, leader_replica_id: Option<u16>, cx: &mut ViewContext<Self>);
 680    fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool;
 681}
 682
 683pub trait FollowableItemHandle: ItemHandle {
 684    fn remote_id(&self, client: &Arc<Client>, cx: &AppContext) -> Option<ViewId>;
 685    fn set_leader_replica_id(&self, leader_replica_id: Option<u16>, cx: &mut AppContext);
 686    fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant>;
 687    fn add_event_to_update_proto(
 688        &self,
 689        event: &dyn Any,
 690        update: &mut Option<proto::update_view::Variant>,
 691        cx: &AppContext,
 692    ) -> bool;
 693    fn apply_update_proto(
 694        &self,
 695        project: &ModelHandle<Project>,
 696        message: proto::update_view::Variant,
 697        cx: &mut AppContext,
 698    ) -> Task<Result<()>>;
 699    fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool;
 700}
 701
 702impl<T: FollowableItem> FollowableItemHandle for ViewHandle<T> {
 703    fn remote_id(&self, client: &Arc<Client>, cx: &AppContext) -> Option<ViewId> {
 704        self.read(cx).remote_id().or_else(|| {
 705            client.peer_id().map(|creator| ViewId {
 706                creator,
 707                id: self.id() as u64,
 708            })
 709        })
 710    }
 711
 712    fn set_leader_replica_id(&self, leader_replica_id: Option<u16>, cx: &mut AppContext) {
 713        self.update(cx, |this, cx| {
 714            this.set_leader_replica_id(leader_replica_id, cx)
 715        })
 716    }
 717
 718    fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
 719        self.read(cx).to_state_proto(cx)
 720    }
 721
 722    fn add_event_to_update_proto(
 723        &self,
 724        event: &dyn Any,
 725        update: &mut Option<proto::update_view::Variant>,
 726        cx: &AppContext,
 727    ) -> bool {
 728        if let Some(event) = event.downcast_ref() {
 729            self.read(cx).add_event_to_update_proto(event, update, cx)
 730        } else {
 731            false
 732        }
 733    }
 734
 735    fn apply_update_proto(
 736        &self,
 737        project: &ModelHandle<Project>,
 738        message: proto::update_view::Variant,
 739        cx: &mut AppContext,
 740    ) -> Task<Result<()>> {
 741        self.update(cx, |this, cx| this.apply_update_proto(project, message, cx))
 742    }
 743
 744    fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool {
 745        if let Some(event) = event.downcast_ref() {
 746            T::should_unfollow_on_event(event, cx)
 747        } else {
 748            false
 749        }
 750    }
 751}
 752
 753#[cfg(test)]
 754pub(crate) mod test {
 755    use super::{Item, ItemEvent};
 756    use crate::{sidebar::SidebarItem, ItemId, ItemNavHistory, Pane, Workspace, WorkspaceId};
 757    use gpui::{
 758        elements::Empty, AppContext, Drawable, Element, Entity, ModelHandle, Task, View,
 759        ViewContext, ViewHandle, WeakViewHandle,
 760    };
 761    use project::{Project, ProjectEntryId, ProjectPath, WorktreeId};
 762    use smallvec::SmallVec;
 763    use std::{any::Any, borrow::Cow, cell::Cell, path::Path};
 764
 765    pub struct TestProjectItem {
 766        pub entry_id: Option<ProjectEntryId>,
 767        pub project_path: Option<ProjectPath>,
 768    }
 769
 770    pub struct TestItem {
 771        pub workspace_id: WorkspaceId,
 772        pub state: String,
 773        pub label: String,
 774        pub save_count: usize,
 775        pub save_as_count: usize,
 776        pub reload_count: usize,
 777        pub is_dirty: bool,
 778        pub is_singleton: bool,
 779        pub has_conflict: bool,
 780        pub project_items: Vec<ModelHandle<TestProjectItem>>,
 781        pub nav_history: Option<ItemNavHistory>,
 782        pub tab_descriptions: Option<Vec<&'static str>>,
 783        pub tab_detail: Cell<Option<usize>>,
 784    }
 785
 786    impl Entity for TestProjectItem {
 787        type Event = ();
 788    }
 789
 790    impl project::Item for TestProjectItem {
 791        fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
 792            self.entry_id
 793        }
 794
 795        fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
 796            self.project_path.clone()
 797        }
 798    }
 799
 800    pub enum TestItemEvent {
 801        Edit,
 802    }
 803
 804    impl Clone for TestItem {
 805        fn clone(&self) -> Self {
 806            Self {
 807                state: self.state.clone(),
 808                label: self.label.clone(),
 809                save_count: self.save_count,
 810                save_as_count: self.save_as_count,
 811                reload_count: self.reload_count,
 812                is_dirty: self.is_dirty,
 813                is_singleton: self.is_singleton,
 814                has_conflict: self.has_conflict,
 815                project_items: self.project_items.clone(),
 816                nav_history: None,
 817                tab_descriptions: None,
 818                tab_detail: Default::default(),
 819                workspace_id: self.workspace_id,
 820            }
 821        }
 822    }
 823
 824    impl TestProjectItem {
 825        pub fn new(id: u64, path: &str, cx: &mut AppContext) -> ModelHandle<Self> {
 826            let entry_id = Some(ProjectEntryId::from_proto(id));
 827            let project_path = Some(ProjectPath {
 828                worktree_id: WorktreeId::from_usize(0),
 829                path: Path::new(path).into(),
 830            });
 831            cx.add_model(|_| Self {
 832                entry_id,
 833                project_path,
 834            })
 835        }
 836
 837        pub fn new_untitled(cx: &mut AppContext) -> ModelHandle<Self> {
 838            cx.add_model(|_| Self {
 839                project_path: None,
 840                entry_id: None,
 841            })
 842        }
 843    }
 844
 845    impl TestItem {
 846        pub fn new() -> Self {
 847            Self {
 848                state: String::new(),
 849                label: String::new(),
 850                save_count: 0,
 851                save_as_count: 0,
 852                reload_count: 0,
 853                is_dirty: false,
 854                has_conflict: false,
 855                project_items: Vec::new(),
 856                is_singleton: true,
 857                nav_history: None,
 858                tab_descriptions: None,
 859                tab_detail: Default::default(),
 860                workspace_id: 0,
 861            }
 862        }
 863
 864        pub fn new_deserialized(id: WorkspaceId) -> Self {
 865            let mut this = Self::new();
 866            this.workspace_id = id;
 867            this
 868        }
 869
 870        pub fn with_label(mut self, state: &str) -> Self {
 871            self.label = state.to_string();
 872            self
 873        }
 874
 875        pub fn with_singleton(mut self, singleton: bool) -> Self {
 876            self.is_singleton = singleton;
 877            self
 878        }
 879
 880        pub fn with_dirty(mut self, dirty: bool) -> Self {
 881            self.is_dirty = dirty;
 882            self
 883        }
 884
 885        pub fn with_conflict(mut self, has_conflict: bool) -> Self {
 886            self.has_conflict = has_conflict;
 887            self
 888        }
 889
 890        pub fn with_project_items(mut self, items: &[ModelHandle<TestProjectItem>]) -> Self {
 891            self.project_items.clear();
 892            self.project_items.extend(items.iter().cloned());
 893            self
 894        }
 895
 896        pub fn set_state(&mut self, state: String, cx: &mut ViewContext<Self>) {
 897            self.push_to_nav_history(cx);
 898            self.state = state;
 899        }
 900
 901        fn push_to_nav_history(&mut self, cx: &mut ViewContext<Self>) {
 902            if let Some(history) = &mut self.nav_history {
 903                history.push(Some(Box::new(self.state.clone())), cx);
 904            }
 905        }
 906    }
 907
 908    impl Entity for TestItem {
 909        type Event = TestItemEvent;
 910    }
 911
 912    impl View for TestItem {
 913        fn ui_name() -> &'static str {
 914            "TestItem"
 915        }
 916
 917        fn render(&mut self, _: &mut ViewContext<Self>) -> Element<Self> {
 918            Empty::new().boxed()
 919        }
 920    }
 921
 922    impl Item for TestItem {
 923        fn tab_description<'a>(&'a self, detail: usize, _: &'a AppContext) -> Option<Cow<'a, str>> {
 924            self.tab_descriptions.as_ref().and_then(|descriptions| {
 925                let description = *descriptions.get(detail).or_else(|| descriptions.last())?;
 926                Some(description.into())
 927            })
 928        }
 929
 930        fn tab_content(
 931            &self,
 932            detail: Option<usize>,
 933            _: &theme::Tab,
 934            _: &AppContext,
 935        ) -> Element<Pane> {
 936            self.tab_detail.set(detail);
 937            Empty::new().boxed()
 938        }
 939
 940        fn for_each_project_item(
 941            &self,
 942            cx: &AppContext,
 943            f: &mut dyn FnMut(usize, &dyn project::Item),
 944        ) {
 945            self.project_items
 946                .iter()
 947                .for_each(|item| f(item.id(), item.read(cx)))
 948        }
 949
 950        fn is_singleton(&self, _: &AppContext) -> bool {
 951            self.is_singleton
 952        }
 953
 954        fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext<Self>) {
 955            self.nav_history = Some(history);
 956        }
 957
 958        fn navigate(&mut self, state: Box<dyn Any>, _: &mut ViewContext<Self>) -> bool {
 959            let state = *state.downcast::<String>().unwrap_or_default();
 960            if state != self.state {
 961                self.state = state;
 962                true
 963            } else {
 964                false
 965            }
 966        }
 967
 968        fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
 969            self.push_to_nav_history(cx);
 970        }
 971
 972        fn clone_on_split(
 973            &self,
 974            _workspace_id: WorkspaceId,
 975            _: &mut ViewContext<Self>,
 976        ) -> Option<Self>
 977        where
 978            Self: Sized,
 979        {
 980            Some(self.clone())
 981        }
 982
 983        fn is_dirty(&self, _: &AppContext) -> bool {
 984            self.is_dirty
 985        }
 986
 987        fn has_conflict(&self, _: &AppContext) -> bool {
 988            self.has_conflict
 989        }
 990
 991        fn can_save(&self, cx: &AppContext) -> bool {
 992            !self.project_items.is_empty()
 993                && self
 994                    .project_items
 995                    .iter()
 996                    .all(|item| item.read(cx).entry_id.is_some())
 997        }
 998
 999        fn save(
1000            &mut self,
1001            _: ModelHandle<Project>,
1002            _: &mut ViewContext<Self>,
1003        ) -> Task<anyhow::Result<()>> {
1004            self.save_count += 1;
1005            self.is_dirty = false;
1006            Task::ready(Ok(()))
1007        }
1008
1009        fn save_as(
1010            &mut self,
1011            _: ModelHandle<Project>,
1012            _: std::path::PathBuf,
1013            _: &mut ViewContext<Self>,
1014        ) -> Task<anyhow::Result<()>> {
1015            self.save_as_count += 1;
1016            self.is_dirty = false;
1017            Task::ready(Ok(()))
1018        }
1019
1020        fn reload(
1021            &mut self,
1022            _: ModelHandle<Project>,
1023            _: &mut ViewContext<Self>,
1024        ) -> Task<anyhow::Result<()>> {
1025            self.reload_count += 1;
1026            self.is_dirty = false;
1027            Task::ready(Ok(()))
1028        }
1029
1030        fn to_item_events(_: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
1031            [ItemEvent::UpdateTab, ItemEvent::Edit].into()
1032        }
1033
1034        fn serialized_item_kind() -> Option<&'static str> {
1035            Some("TestItem")
1036        }
1037
1038        fn deserialize(
1039            _project: ModelHandle<Project>,
1040            _workspace: WeakViewHandle<Workspace>,
1041            workspace_id: WorkspaceId,
1042            _item_id: ItemId,
1043            cx: &mut ViewContext<Pane>,
1044        ) -> Task<anyhow::Result<ViewHandle<Self>>> {
1045            let view = cx.add_view(|_cx| Self::new_deserialized(workspace_id));
1046            Task::Ready(Some(anyhow::Ok(view)))
1047        }
1048    }
1049
1050    impl SidebarItem for TestItem {}
1051}