item.rs

   1use crate::{
   2    pane, persistence::model::ItemId, searchable::SearchableItemHandle, DelayedDebouncedEditAction,
   3    FollowableItemBuilders, ItemNavHistory, Pane, ToolbarItemLocation, ViewId, Workspace,
   4    WorkspaceId,
   5};
   6use anyhow::{anyhow, Result};
   7use client::{proto, Client};
   8use gpui::{
   9    fonts::HighlightStyle, AnyViewHandle, AppContext, Element, ModelHandle, Task, View,
  10    ViewContext, ViewHandle, WeakViewHandle, WindowContext,
  11};
  12use project::{Project, ProjectEntryId, ProjectPath};
  13use settings::{Autosave, Settings};
  14use smallvec::SmallVec;
  15use std::{
  16    any::{Any, TypeId},
  17    borrow::Cow,
  18    cell::RefCell,
  19    fmt,
  20    ops::Range,
  21    path::PathBuf,
  22    rc::Rc,
  23    sync::{
  24        atomic::{AtomicBool, Ordering},
  25        Arc,
  26    },
  27    time::Duration,
  28};
  29use theme::Theme;
  30
  31#[derive(Eq, PartialEq, Hash)]
  32pub enum ItemEvent {
  33    CloseItem,
  34    UpdateTab,
  35    UpdateBreadcrumbs,
  36    Edit,
  37}
  38
  39// TODO: Combine this with existing HighlightedText struct?
  40pub struct BreadcrumbText {
  41    pub text: String,
  42    pub highlights: Option<Vec<(Range<usize>, HighlightStyle)>>,
  43}
  44
  45pub trait Item: View {
  46    fn deactivated(&mut self, _: &mut ViewContext<Self>) {}
  47    fn workspace_deactivated(&mut self, _: &mut ViewContext<Self>) {}
  48    fn navigate(&mut self, _: Box<dyn Any>, _: &mut ViewContext<Self>) -> bool {
  49        false
  50    }
  51    fn tab_tooltip_text(&self, _: &AppContext) -> Option<Cow<str>> {
  52        None
  53    }
  54    fn tab_description<'a>(&'a self, _: usize, _: &'a AppContext) -> Option<Cow<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 WindowContext,
 174        handler: Box<dyn Fn(ItemEvent, &mut WindowContext)>,
 175    ) -> gpui::Subscription;
 176    fn tab_tooltip_text<'a>(&self, cx: &'a AppContext) -> Option<Cow<'a, str>>;
 177    fn tab_description<'a>(&'a self, detail: usize, cx: &'a AppContext) -> Option<Cow<'a, str>>;
 178    fn tab_content(
 179        &self,
 180        detail: Option<usize>,
 181        style: &theme::Tab,
 182        cx: &AppContext,
 183    ) -> Element<Pane>;
 184    fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
 185    fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]>;
 186    fn project_item_model_ids(&self, cx: &AppContext) -> SmallVec<[usize; 3]>;
 187    fn for_each_project_item(&self, _: &AppContext, _: &mut dyn FnMut(usize, &dyn project::Item));
 188    fn is_singleton(&self, cx: &AppContext) -> bool;
 189    fn boxed_clone(&self) -> Box<dyn ItemHandle>;
 190    fn clone_on_split(
 191        &self,
 192        workspace_id: WorkspaceId,
 193        cx: &mut WindowContext,
 194    ) -> Option<Box<dyn ItemHandle>>;
 195    fn added_to_pane(
 196        &self,
 197        workspace: &mut Workspace,
 198        pane: ViewHandle<Pane>,
 199        cx: &mut ViewContext<Workspace>,
 200    );
 201    fn deactivated(&self, cx: &mut WindowContext);
 202    fn workspace_deactivated(&self, cx: &mut WindowContext);
 203    fn navigate(&self, data: Box<dyn Any>, cx: &mut WindowContext) -> bool;
 204    fn id(&self) -> usize;
 205    fn window_id(&self) -> usize;
 206    fn as_any(&self) -> &AnyViewHandle;
 207    fn is_dirty(&self, cx: &AppContext) -> bool;
 208    fn has_conflict(&self, cx: &AppContext) -> bool;
 209    fn can_save(&self, cx: &AppContext) -> bool;
 210    fn save(&self, project: ModelHandle<Project>, cx: &mut WindowContext) -> Task<Result<()>>;
 211    fn save_as(
 212        &self,
 213        project: ModelHandle<Project>,
 214        abs_path: PathBuf,
 215        cx: &mut WindowContext,
 216    ) -> Task<Result<()>>;
 217    fn reload(&self, project: ModelHandle<Project>, cx: &mut WindowContext) -> Task<Result<()>>;
 218    fn git_diff_recalc(
 219        &self,
 220        project: ModelHandle<Project>,
 221        cx: &mut WindowContext,
 222    ) -> Task<Result<()>>;
 223    fn act_as_type<'a>(&'a self, type_id: TypeId, cx: &'a AppContext) -> Option<&'a AnyViewHandle>;
 224    fn to_followable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn FollowableItemHandle>>;
 225    fn on_release(
 226        &self,
 227        cx: &mut AppContext,
 228        callback: Box<dyn FnOnce(&mut AppContext)>,
 229    ) -> gpui::Subscription;
 230    fn to_searchable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn SearchableItemHandle>>;
 231    fn breadcrumb_location(&self, cx: &AppContext) -> ToolbarItemLocation;
 232    fn breadcrumbs(&self, theme: &Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>>;
 233    fn serialized_item_kind(&self) -> Option<&'static str>;
 234    fn show_toolbar(&self, cx: &AppContext) -> bool;
 235}
 236
 237pub trait WeakItemHandle {
 238    fn id(&self) -> usize;
 239    fn window_id(&self) -> usize;
 240    fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>>;
 241}
 242
 243impl dyn ItemHandle {
 244    pub fn downcast<T: View>(&self) -> Option<ViewHandle<T>> {
 245        self.as_any().clone().downcast()
 246    }
 247
 248    pub fn act_as<T: View>(&self, cx: &AppContext) -> Option<ViewHandle<T>> {
 249        self.act_as_type(TypeId::of::<T>(), cx)
 250            .and_then(|t| t.clone().downcast())
 251    }
 252}
 253
 254impl<T: Item> ItemHandle for ViewHandle<T> {
 255    fn subscribe_to_item_events(
 256        &self,
 257        cx: &mut WindowContext,
 258        handler: Box<dyn Fn(ItemEvent, &mut WindowContext)>,
 259    ) -> gpui::Subscription {
 260        cx.subscribe(self, move |_, event, cx| {
 261            for item_event in T::to_item_events(event) {
 262                handler(item_event, cx)
 263            }
 264        })
 265    }
 266
 267    fn tab_tooltip_text<'a>(&self, cx: &'a AppContext) -> Option<Cow<'a, str>> {
 268        self.read(cx).tab_tooltip_text(cx)
 269    }
 270
 271    fn tab_description<'a>(&'a self, detail: usize, cx: &'a AppContext) -> Option<Cow<'a, str>> {
 272        self.read(cx).tab_description(detail, cx)
 273    }
 274
 275    fn tab_content(
 276        &self,
 277        detail: Option<usize>,
 278        style: &theme::Tab,
 279        cx: &AppContext,
 280    ) -> Element<Pane> {
 281        self.read(cx).tab_content(detail, style, cx)
 282    }
 283
 284    fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
 285        let this = self.read(cx);
 286        let mut result = None;
 287        if this.is_singleton(cx) {
 288            this.for_each_project_item(cx, &mut |_, item| {
 289                result = item.project_path(cx);
 290            });
 291        }
 292        result
 293    }
 294
 295    fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]> {
 296        let mut result = SmallVec::new();
 297        self.read(cx).for_each_project_item(cx, &mut |_, item| {
 298            if let Some(id) = item.entry_id(cx) {
 299                result.push(id);
 300            }
 301        });
 302        result
 303    }
 304
 305    fn project_item_model_ids(&self, cx: &AppContext) -> SmallVec<[usize; 3]> {
 306        let mut result = SmallVec::new();
 307        self.read(cx).for_each_project_item(cx, &mut |id, _| {
 308            result.push(id);
 309        });
 310        result
 311    }
 312
 313    fn for_each_project_item(&self, cx: &AppContext, f: &mut dyn FnMut(usize, &dyn project::Item)) {
 314        self.read(cx).for_each_project_item(cx, f)
 315    }
 316
 317    fn is_singleton(&self, cx: &AppContext) -> bool {
 318        self.read(cx).is_singleton(cx)
 319    }
 320
 321    fn boxed_clone(&self) -> Box<dyn ItemHandle> {
 322        Box::new(self.clone())
 323    }
 324
 325    fn clone_on_split(
 326        &self,
 327        workspace_id: WorkspaceId,
 328        cx: &mut WindowContext,
 329    ) -> Option<Box<dyn ItemHandle>> {
 330        self.update(cx, |item, cx| {
 331            cx.add_option_view(|cx| item.clone_on_split(workspace_id, cx))
 332        })
 333        .map(|handle| Box::new(handle) as Box<dyn ItemHandle>)
 334    }
 335
 336    fn added_to_pane(
 337        &self,
 338        workspace: &mut Workspace,
 339        pane: ViewHandle<Pane>,
 340        cx: &mut ViewContext<Workspace>,
 341    ) {
 342        let history = pane.read(cx).nav_history_for_item(self);
 343        self.update(cx, |this, cx| {
 344            this.set_nav_history(history, cx);
 345            this.added_to_workspace(workspace, cx);
 346        });
 347
 348        if let Some(followed_item) = self.to_followable_item_handle(cx) {
 349            if let Some(message) = followed_item.to_state_proto(cx) {
 350                workspace.update_followers(
 351                    proto::update_followers::Variant::CreateView(proto::View {
 352                        id: followed_item
 353                            .remote_id(&workspace.client, cx)
 354                            .map(|id| id.to_proto()),
 355                        variant: Some(message),
 356                        leader_id: workspace.leader_for_pane(&pane),
 357                    }),
 358                    cx,
 359                );
 360            }
 361        }
 362
 363        if workspace
 364            .panes_by_item
 365            .insert(self.id(), pane.downgrade())
 366            .is_none()
 367        {
 368            let mut pending_autosave = DelayedDebouncedEditAction::new();
 369            let mut pending_git_update = DelayedDebouncedEditAction::new();
 370            let pending_update = Rc::new(RefCell::new(None));
 371            let pending_update_scheduled = Rc::new(AtomicBool::new(false));
 372
 373            let mut event_subscription =
 374                Some(cx.subscribe(self, move |workspace, item, event, cx| {
 375                    let pane = if let Some(pane) = workspace
 376                        .panes_by_item
 377                        .get(&item.id())
 378                        .and_then(|pane| pane.upgrade(cx))
 379                    {
 380                        pane
 381                    } else {
 382                        log::error!("unexpected item event after pane was dropped");
 383                        return;
 384                    };
 385
 386                    if let Some(item) = item.to_followable_item_handle(cx) {
 387                        let leader_id = workspace.leader_for_pane(&pane);
 388
 389                        if leader_id.is_some() && item.should_unfollow_on_event(event, cx) {
 390                            workspace.unfollow(&pane, cx);
 391                        }
 392
 393                        if item.add_event_to_update_proto(
 394                            event,
 395                            &mut *pending_update.borrow_mut(),
 396                            cx,
 397                        ) && !pending_update_scheduled.load(Ordering::SeqCst)
 398                        {
 399                            pending_update_scheduled.store(true, Ordering::SeqCst);
 400                            cx.after_window_update({
 401                                let pending_update = pending_update.clone();
 402                                let pending_update_scheduled = pending_update_scheduled.clone();
 403                                move |this, cx| {
 404                                    pending_update_scheduled.store(false, Ordering::SeqCst);
 405                                    this.update_followers(
 406                                        proto::update_followers::Variant::UpdateView(
 407                                            proto::UpdateView {
 408                                                id: item
 409                                                    .remote_id(&this.client, cx)
 410                                                    .map(|id| id.to_proto()),
 411                                                variant: pending_update.borrow_mut().take(),
 412                                                leader_id,
 413                                            },
 414                                        ),
 415                                        cx,
 416                                    );
 417                                }
 418                            });
 419                        }
 420                    }
 421
 422                    for item_event in T::to_item_events(event).into_iter() {
 423                        match item_event {
 424                            ItemEvent::CloseItem => {
 425                                Pane::close_item_by_id(workspace, pane, item.id(), cx)
 426                                    .detach_and_log_err(cx);
 427                                return;
 428                            }
 429
 430                            ItemEvent::UpdateTab => {
 431                                pane.update(cx, |_, cx| {
 432                                    cx.emit(pane::Event::ChangeItemTitle);
 433                                    cx.notify();
 434                                });
 435                            }
 436
 437                            ItemEvent::Edit => {
 438                                if let Autosave::AfterDelay { milliseconds } =
 439                                    cx.global::<Settings>().autosave
 440                                {
 441                                    let delay = Duration::from_millis(milliseconds);
 442                                    let item = item.clone();
 443                                    pending_autosave.fire_new(delay, cx, move |workspace, cx| {
 444                                        Pane::autosave_item(&item, workspace.project().clone(), cx)
 445                                    });
 446                                }
 447
 448                                let settings = cx.global::<Settings>();
 449                                let debounce_delay = settings.git_overrides.gutter_debounce;
 450
 451                                let item = item.clone();
 452
 453                                if let Some(delay) = debounce_delay {
 454                                    const MIN_GIT_DELAY: u64 = 50;
 455
 456                                    let delay = delay.max(MIN_GIT_DELAY);
 457                                    let duration = Duration::from_millis(delay);
 458
 459                                    pending_git_update.fire_new(
 460                                        duration,
 461                                        cx,
 462                                        move |workspace, cx| {
 463                                            item.git_diff_recalc(workspace.project().clone(), cx)
 464                                        },
 465                                    );
 466                                } else {
 467                                    cx.spawn_weak(|workspace, mut cx| async move {
 468                                        workspace
 469                                            .upgrade(&cx)
 470                                            .ok_or_else(|| anyhow!("workspace was dropped"))?
 471                                            .update(&mut cx, |workspace, cx| {
 472                                                item.git_diff_recalc(
 473                                                    workspace.project().clone(),
 474                                                    cx,
 475                                                )
 476                                            })?
 477                                            .await?;
 478                                        anyhow::Ok(())
 479                                    })
 480                                    .detach_and_log_err(cx);
 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 WindowContext) {
 511        self.update(cx, |this, cx| this.deactivated(cx));
 512    }
 513
 514    fn workspace_deactivated(&self, cx: &mut WindowContext) {
 515        self.update(cx, |this, cx| this.workspace_deactivated(cx));
 516    }
 517
 518    fn navigate(&self, data: Box<dyn Any>, cx: &mut WindowContext) -> 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 WindowContext) -> 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 WindowContext,
 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 WindowContext) -> 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 WindowContext,
 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 WindowContext);
 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 WindowContext,
 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 WindowContext) {
 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 WindowContext,
 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(&self, detail: usize, _: &AppContext) -> Option<Cow<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}