item.rs

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