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