pane.rs

   1use crate::{
   2    item::{
   3        ClosePosition, Item, ItemHandle, ItemSettings, PreviewTabsSettings, TabContentParams,
   4        WeakItemHandle,
   5    },
   6    notifications::NotifyResultExt,
   7    toolbar::Toolbar,
   8    workspace_settings::{AutosaveSetting, TabBarSettings, WorkspaceSettings},
   9    CloseWindow, CopyPath, CopyRelativePath, NewFile, NewTerminal, OpenInTerminal, OpenTerminal,
  10    OpenVisible, SplitDirection, ToggleFileFinder, ToggleProjectSymbols, ToggleZoom, Workspace,
  11};
  12use anyhow::Result;
  13use collections::{BTreeSet, HashMap, HashSet, VecDeque};
  14use futures::{stream::FuturesUnordered, StreamExt};
  15use git::repository::GitFileStatus;
  16use gpui::{
  17    actions, anchored, deferred, impl_actions, prelude::*, Action, AnchorCorner, AnyElement,
  18    AppContext, AsyncWindowContext, ClickEvent, ClipboardItem, Div, DragMoveEvent, EntityId,
  19    EventEmitter, ExternalPaths, FocusHandle, FocusOutEvent, FocusableView, KeyContext, Model,
  20    MouseButton, MouseDownEvent, NavigationDirection, Pixels, Point, PromptLevel, Render,
  21    ScrollHandle, Subscription, Task, View, ViewContext, VisualContext, WeakFocusHandle, WeakView,
  22    WindowContext,
  23};
  24use itertools::Itertools;
  25use parking_lot::Mutex;
  26use project::{Project, ProjectEntryId, ProjectPath, WorktreeId};
  27use serde::Deserialize;
  28use settings::{Settings, SettingsStore};
  29use std::{
  30    any::Any,
  31    cmp, fmt, mem,
  32    ops::ControlFlow,
  33    path::PathBuf,
  34    rc::Rc,
  35    sync::{
  36        atomic::{AtomicUsize, Ordering},
  37        Arc,
  38    },
  39};
  40use theme::ThemeSettings;
  41
  42use ui::{
  43    prelude::*, right_click_menu, ButtonSize, Color, IconButton, IconButtonShape, IconName,
  44    IconSize, Indicator, Label, PopoverMenu, PopoverMenuHandle, Tab, TabBar, TabPosition, Tooltip,
  45};
  46use ui::{v_flex, ContextMenu};
  47use util::{debug_panic, maybe, truncate_and_remove_front, ResultExt};
  48
  49/// A selected entry in e.g. project panel.
  50#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
  51pub struct SelectedEntry {
  52    pub worktree_id: WorktreeId,
  53    pub entry_id: ProjectEntryId,
  54}
  55
  56/// A group of selected entries from project panel.
  57#[derive(Debug)]
  58pub struct DraggedSelection {
  59    pub active_selection: SelectedEntry,
  60    pub marked_selections: Arc<BTreeSet<SelectedEntry>>,
  61}
  62
  63impl DraggedSelection {
  64    pub fn items<'a>(&'a self) -> Box<dyn Iterator<Item = &'a SelectedEntry> + 'a> {
  65        if self.marked_selections.contains(&self.active_selection) {
  66            Box::new(self.marked_selections.iter())
  67        } else {
  68            Box::new(std::iter::once(&self.active_selection))
  69        }
  70    }
  71}
  72
  73#[derive(PartialEq, Clone, Copy, Deserialize, Debug)]
  74#[serde(rename_all = "camelCase")]
  75pub enum SaveIntent {
  76    /// write all files (even if unchanged)
  77    /// prompt before overwriting on-disk changes
  78    Save,
  79    /// same as Save, but without auto formatting
  80    SaveWithoutFormat,
  81    /// write any files that have local changes
  82    /// prompt before overwriting on-disk changes
  83    SaveAll,
  84    /// always prompt for a new path
  85    SaveAs,
  86    /// prompt "you have unsaved changes" before writing
  87    Close,
  88    /// write all dirty files, don't prompt on conflict
  89    Overwrite,
  90    /// skip all save-related behavior
  91    Skip,
  92}
  93
  94#[derive(Clone, Deserialize, PartialEq, Debug)]
  95pub struct ActivateItem(pub usize);
  96
  97#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
  98#[serde(rename_all = "camelCase")]
  99pub struct CloseActiveItem {
 100    pub save_intent: Option<SaveIntent>,
 101}
 102
 103#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
 104#[serde(rename_all = "camelCase")]
 105pub struct CloseInactiveItems {
 106    pub save_intent: Option<SaveIntent>,
 107}
 108
 109#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
 110#[serde(rename_all = "camelCase")]
 111pub struct CloseAllItems {
 112    pub save_intent: Option<SaveIntent>,
 113}
 114
 115#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
 116#[serde(rename_all = "camelCase")]
 117pub struct RevealInProjectPanel {
 118    pub entry_id: Option<u64>,
 119}
 120
 121#[derive(Default, PartialEq, Clone, Deserialize)]
 122pub struct DeploySearch {
 123    #[serde(default)]
 124    pub replace_enabled: bool,
 125}
 126
 127impl_actions!(
 128    pane,
 129    [
 130        CloseAllItems,
 131        CloseActiveItem,
 132        CloseInactiveItems,
 133        ActivateItem,
 134        RevealInProjectPanel,
 135        DeploySearch,
 136    ]
 137);
 138
 139actions!(
 140    pane,
 141    [
 142        ActivatePrevItem,
 143        ActivateNextItem,
 144        ActivateLastItem,
 145        AlternateFile,
 146        CloseCleanItems,
 147        CloseItemsToTheLeft,
 148        CloseItemsToTheRight,
 149        GoBack,
 150        GoForward,
 151        JoinIntoNext,
 152        ReopenClosedItem,
 153        SplitLeft,
 154        SplitUp,
 155        SplitRight,
 156        SplitDown,
 157        SplitHorizontal,
 158        SplitVertical,
 159        TogglePreviewTab,
 160        TogglePinTab,
 161    ]
 162);
 163
 164impl DeploySearch {
 165    pub fn find() -> Self {
 166        Self {
 167            replace_enabled: false,
 168        }
 169    }
 170}
 171
 172const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
 173
 174pub enum Event {
 175    AddItem {
 176        item: Box<dyn ItemHandle>,
 177    },
 178    ActivateItem {
 179        local: bool,
 180    },
 181    Remove {
 182        focus_on_pane: Option<View<Pane>>,
 183    },
 184    RemoveItem {
 185        idx: usize,
 186    },
 187    RemovedItem {
 188        item_id: EntityId,
 189    },
 190    Split(SplitDirection),
 191    JoinIntoNext,
 192    ChangeItemTitle,
 193    Focus,
 194    ZoomIn,
 195    ZoomOut,
 196    UserSavedItem {
 197        item: Box<dyn WeakItemHandle>,
 198        save_intent: SaveIntent,
 199    },
 200}
 201
 202impl fmt::Debug for Event {
 203    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
 204        match self {
 205            Event::AddItem { item } => f
 206                .debug_struct("AddItem")
 207                .field("item", &item.item_id())
 208                .finish(),
 209            Event::ActivateItem { local } => f
 210                .debug_struct("ActivateItem")
 211                .field("local", local)
 212                .finish(),
 213            Event::Remove { .. } => f.write_str("Remove"),
 214            Event::RemoveItem { idx } => f.debug_struct("RemoveItem").field("idx", idx).finish(),
 215            Event::RemovedItem { item_id } => f
 216                .debug_struct("RemovedItem")
 217                .field("item_id", item_id)
 218                .finish(),
 219            Event::Split(direction) => f
 220                .debug_struct("Split")
 221                .field("direction", direction)
 222                .finish(),
 223            Event::JoinIntoNext => f.write_str("JoinIntoNext"),
 224            Event::ChangeItemTitle => f.write_str("ChangeItemTitle"),
 225            Event::Focus => f.write_str("Focus"),
 226            Event::ZoomIn => f.write_str("ZoomIn"),
 227            Event::ZoomOut => f.write_str("ZoomOut"),
 228            Event::UserSavedItem { item, save_intent } => f
 229                .debug_struct("UserSavedItem")
 230                .field("item", &item.id())
 231                .field("save_intent", save_intent)
 232                .finish(),
 233        }
 234    }
 235}
 236
 237/// A container for 0 to many items that are open in the workspace.
 238/// Treats all items uniformly via the [`ItemHandle`] trait, whether it's an editor, search results multibuffer, terminal or something else,
 239/// responsible for managing item tabs, focus and zoom states and drag and drop features.
 240/// Can be split, see `PaneGroup` for more details.
 241pub struct Pane {
 242    alternate_file_items: (
 243        Option<Box<dyn WeakItemHandle>>,
 244        Option<Box<dyn WeakItemHandle>>,
 245    ),
 246    focus_handle: FocusHandle,
 247    items: Vec<Box<dyn ItemHandle>>,
 248    activation_history: Vec<ActivationHistoryEntry>,
 249    next_activation_timestamp: Arc<AtomicUsize>,
 250    zoomed: bool,
 251    was_focused: bool,
 252    active_item_index: usize,
 253    preview_item_id: Option<EntityId>,
 254    last_focus_handle_by_item: HashMap<EntityId, WeakFocusHandle>,
 255    nav_history: NavHistory,
 256    toolbar: View<Toolbar>,
 257    pub(crate) workspace: WeakView<Workspace>,
 258    project: Model<Project>,
 259    drag_split_direction: Option<SplitDirection>,
 260    can_drop_predicate: Option<Arc<dyn Fn(&dyn Any, &mut WindowContext) -> bool>>,
 261    custom_drop_handle:
 262        Option<Arc<dyn Fn(&mut Pane, &dyn Any, &mut ViewContext<Pane>) -> ControlFlow<(), ()>>>,
 263    can_split: bool,
 264    should_display_tab_bar: Rc<dyn Fn(&ViewContext<Pane>) -> bool>,
 265    render_tab_bar_buttons:
 266        Rc<dyn Fn(&mut Pane, &mut ViewContext<Pane>) -> (Option<AnyElement>, Option<AnyElement>)>,
 267    _subscriptions: Vec<Subscription>,
 268    tab_bar_scroll_handle: ScrollHandle,
 269    /// Is None if navigation buttons are permanently turned off (and should not react to setting changes).
 270    /// Otherwise, when `display_nav_history_buttons` is Some, it determines whether nav buttons should be displayed.
 271    display_nav_history_buttons: Option<bool>,
 272    double_click_dispatch_action: Box<dyn Action>,
 273    save_modals_spawned: HashSet<EntityId>,
 274    pub new_item_context_menu_handle: PopoverMenuHandle<ContextMenu>,
 275    split_item_context_menu_handle: PopoverMenuHandle<ContextMenu>,
 276    pinned_tab_count: usize,
 277}
 278
 279pub struct ActivationHistoryEntry {
 280    pub entity_id: EntityId,
 281    pub timestamp: usize,
 282}
 283
 284pub struct ItemNavHistory {
 285    history: NavHistory,
 286    item: Arc<dyn WeakItemHandle>,
 287    is_preview: bool,
 288}
 289
 290#[derive(Clone)]
 291pub struct NavHistory(Arc<Mutex<NavHistoryState>>);
 292
 293struct NavHistoryState {
 294    mode: NavigationMode,
 295    backward_stack: VecDeque<NavigationEntry>,
 296    forward_stack: VecDeque<NavigationEntry>,
 297    closed_stack: VecDeque<NavigationEntry>,
 298    paths_by_item: HashMap<EntityId, (ProjectPath, Option<PathBuf>)>,
 299    pane: WeakView<Pane>,
 300    next_timestamp: Arc<AtomicUsize>,
 301}
 302
 303#[derive(Debug, Copy, Clone)]
 304pub enum NavigationMode {
 305    Normal,
 306    GoingBack,
 307    GoingForward,
 308    ClosingItem,
 309    ReopeningClosedItem,
 310    Disabled,
 311}
 312
 313impl Default for NavigationMode {
 314    fn default() -> Self {
 315        Self::Normal
 316    }
 317}
 318
 319pub struct NavigationEntry {
 320    pub item: Arc<dyn WeakItemHandle>,
 321    pub data: Option<Box<dyn Any + Send>>,
 322    pub timestamp: usize,
 323    pub is_preview: bool,
 324}
 325
 326#[derive(Clone)]
 327pub struct DraggedTab {
 328    pub pane: View<Pane>,
 329    pub item: Box<dyn ItemHandle>,
 330    pub ix: usize,
 331    pub detail: usize,
 332    pub is_active: bool,
 333}
 334
 335impl EventEmitter<Event> for Pane {}
 336
 337impl Pane {
 338    pub fn new(
 339        workspace: WeakView<Workspace>,
 340        project: Model<Project>,
 341        next_timestamp: Arc<AtomicUsize>,
 342        can_drop_predicate: Option<Arc<dyn Fn(&dyn Any, &mut WindowContext) -> bool + 'static>>,
 343        double_click_dispatch_action: Box<dyn Action>,
 344        cx: &mut ViewContext<Self>,
 345    ) -> Self {
 346        let focus_handle = cx.focus_handle();
 347
 348        let subscriptions = vec![
 349            cx.on_focus(&focus_handle, Pane::focus_in),
 350            cx.on_focus_in(&focus_handle, Pane::focus_in),
 351            cx.on_focus_out(&focus_handle, Pane::focus_out),
 352            cx.observe_global::<SettingsStore>(Self::settings_changed),
 353        ];
 354
 355        let handle = cx.view().downgrade();
 356        Self {
 357            alternate_file_items: (None, None),
 358            focus_handle,
 359            items: Vec::new(),
 360            activation_history: Vec::new(),
 361            next_activation_timestamp: next_timestamp.clone(),
 362            was_focused: false,
 363            zoomed: false,
 364            active_item_index: 0,
 365            preview_item_id: None,
 366            last_focus_handle_by_item: Default::default(),
 367            nav_history: NavHistory(Arc::new(Mutex::new(NavHistoryState {
 368                mode: NavigationMode::Normal,
 369                backward_stack: Default::default(),
 370                forward_stack: Default::default(),
 371                closed_stack: Default::default(),
 372                paths_by_item: Default::default(),
 373                pane: handle.clone(),
 374                next_timestamp,
 375            }))),
 376            toolbar: cx.new_view(|_| Toolbar::new()),
 377            tab_bar_scroll_handle: ScrollHandle::new(),
 378            drag_split_direction: None,
 379            workspace,
 380            project,
 381            can_drop_predicate,
 382            custom_drop_handle: None,
 383            can_split: true,
 384            should_display_tab_bar: Rc::new(|cx| TabBarSettings::get_global(cx).show),
 385            render_tab_bar_buttons: Rc::new(move |pane, cx| {
 386                if !pane.has_focus(cx) && !pane.context_menu_focused(cx) {
 387                    return (None, None);
 388                }
 389                // Ideally we would return a vec of elements here to pass directly to the [TabBar]'s
 390                // `end_slot`, but due to needing a view here that isn't possible.
 391                let right_children = h_flex()
 392                    // Instead we need to replicate the spacing from the [TabBar]'s `end_slot` here.
 393                    .gap(Spacing::Small.rems(cx))
 394                    .child(
 395                        PopoverMenu::new("pane-tab-bar-popover-menu")
 396                            .trigger(
 397                                IconButton::new("plus", IconName::Plus)
 398                                    .icon_size(IconSize::Small)
 399                                    .tooltip(|cx| Tooltip::text("New...", cx)),
 400                            )
 401                            .anchor(AnchorCorner::TopRight)
 402                            .with_handle(pane.new_item_context_menu_handle.clone())
 403                            .menu(move |cx| {
 404                                Some(ContextMenu::build(cx, |menu, _| {
 405                                    menu.action("New File", NewFile.boxed_clone())
 406                                        .action(
 407                                            "Open File",
 408                                            ToggleFileFinder::default().boxed_clone(),
 409                                        )
 410                                        .separator()
 411                                        .action(
 412                                            "Search Project",
 413                                            DeploySearch {
 414                                                replace_enabled: false,
 415                                            }
 416                                            .boxed_clone(),
 417                                        )
 418                                        .action(
 419                                            "Search Symbols",
 420                                            ToggleProjectSymbols.boxed_clone(),
 421                                        )
 422                                        .separator()
 423                                        .action("New Terminal", NewTerminal.boxed_clone())
 424                                }))
 425                            }),
 426                    )
 427                    .child(
 428                        PopoverMenu::new("pane-tab-bar-split")
 429                            .trigger(
 430                                IconButton::new("split", IconName::Split)
 431                                    .icon_size(IconSize::Small)
 432                                    .tooltip(|cx| Tooltip::text("Split Pane", cx)),
 433                            )
 434                            .anchor(AnchorCorner::TopRight)
 435                            .with_handle(pane.split_item_context_menu_handle.clone())
 436                            .menu(move |cx| {
 437                                ContextMenu::build(cx, |menu, _| {
 438                                    menu.action("Split Right", SplitRight.boxed_clone())
 439                                        .action("Split Left", SplitLeft.boxed_clone())
 440                                        .action("Split Up", SplitUp.boxed_clone())
 441                                        .action("Split Down", SplitDown.boxed_clone())
 442                                })
 443                                .into()
 444                            }),
 445                    )
 446                    .child({
 447                        let zoomed = pane.is_zoomed();
 448                        IconButton::new("toggle_zoom", IconName::Maximize)
 449                            .icon_size(IconSize::Small)
 450                            .selected(zoomed)
 451                            .selected_icon(IconName::Minimize)
 452                            .on_click(cx.listener(|pane, _, cx| {
 453                                pane.toggle_zoom(&crate::ToggleZoom, cx);
 454                            }))
 455                            .tooltip(move |cx| {
 456                                Tooltip::for_action(
 457                                    if zoomed { "Zoom Out" } else { "Zoom In" },
 458                                    &ToggleZoom,
 459                                    cx,
 460                                )
 461                            })
 462                    })
 463                    .into_any_element()
 464                    .into();
 465                (None, right_children)
 466            }),
 467            display_nav_history_buttons: Some(
 468                TabBarSettings::get_global(cx).show_nav_history_buttons,
 469            ),
 470            _subscriptions: subscriptions,
 471            double_click_dispatch_action,
 472            save_modals_spawned: HashSet::default(),
 473            split_item_context_menu_handle: Default::default(),
 474            new_item_context_menu_handle: Default::default(),
 475            pinned_tab_count: 0,
 476        }
 477    }
 478
 479    fn alternate_file(&mut self, cx: &mut ViewContext<Pane>) {
 480        let (_, alternative) = &self.alternate_file_items;
 481        if let Some(alternative) = alternative {
 482            let existing = self
 483                .items()
 484                .find_position(|item| item.item_id() == alternative.id());
 485            if let Some((ix, _)) = existing {
 486                self.activate_item(ix, true, true, cx);
 487            } else {
 488                if let Some(upgraded) = alternative.upgrade() {
 489                    self.add_item(upgraded, true, true, None, cx);
 490                }
 491            }
 492        }
 493    }
 494
 495    pub fn track_alternate_file_items(&mut self) {
 496        if let Some(item) = self.active_item().map(|item| item.downgrade_item()) {
 497            let (current, _) = &self.alternate_file_items;
 498            match current {
 499                Some(current) => {
 500                    if current.id() != item.id() {
 501                        self.alternate_file_items =
 502                            (Some(item), self.alternate_file_items.0.take());
 503                    }
 504                }
 505                None => {
 506                    self.alternate_file_items = (Some(item), None);
 507                }
 508            }
 509        }
 510    }
 511
 512    pub fn has_focus(&self, cx: &WindowContext) -> bool {
 513        // We not only check whether our focus handle contains focus, but also
 514        // whether the active item might have focus, because we might have just activated an item
 515        // that hasn't rendered yet.
 516        // Before the next render, we might transfer focus
 517        // to the item, and `focus_handle.contains_focus` returns false because the `active_item`
 518        // is not hooked up to us in the dispatch tree.
 519        self.focus_handle.contains_focused(cx)
 520            || self
 521                .active_item()
 522                .map_or(false, |item| item.focus_handle(cx).contains_focused(cx))
 523    }
 524
 525    fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
 526        if !self.was_focused {
 527            self.was_focused = true;
 528            cx.emit(Event::Focus);
 529            cx.notify();
 530        }
 531
 532        self.toolbar.update(cx, |toolbar, cx| {
 533            toolbar.focus_changed(true, cx);
 534        });
 535
 536        if let Some(active_item) = self.active_item() {
 537            if self.focus_handle.is_focused(cx) {
 538                // Pane was focused directly. We need to either focus a view inside the active item,
 539                // or focus the active item itself
 540                if let Some(weak_last_focus_handle) =
 541                    self.last_focus_handle_by_item.get(&active_item.item_id())
 542                {
 543                    if let Some(focus_handle) = weak_last_focus_handle.upgrade() {
 544                        focus_handle.focus(cx);
 545                        return;
 546                    }
 547                }
 548
 549                active_item.focus_handle(cx).focus(cx);
 550            } else if let Some(focused) = cx.focused() {
 551                if !self.context_menu_focused(cx) {
 552                    self.last_focus_handle_by_item
 553                        .insert(active_item.item_id(), focused.downgrade());
 554                }
 555            }
 556        }
 557    }
 558
 559    pub fn context_menu_focused(&self, cx: &mut ViewContext<Self>) -> bool {
 560        self.new_item_context_menu_handle.is_focused(cx)
 561            || self.split_item_context_menu_handle.is_focused(cx)
 562    }
 563
 564    fn focus_out(&mut self, _event: FocusOutEvent, cx: &mut ViewContext<Self>) {
 565        self.was_focused = false;
 566        self.toolbar.update(cx, |toolbar, cx| {
 567            toolbar.focus_changed(false, cx);
 568        });
 569        cx.notify();
 570    }
 571
 572    fn settings_changed(&mut self, cx: &mut ViewContext<Self>) {
 573        if let Some(display_nav_history_buttons) = self.display_nav_history_buttons.as_mut() {
 574            *display_nav_history_buttons = TabBarSettings::get_global(cx).show_nav_history_buttons;
 575        }
 576        if !PreviewTabsSettings::get_global(cx).enabled {
 577            self.preview_item_id = None;
 578        }
 579        cx.notify();
 580    }
 581
 582    pub fn active_item_index(&self) -> usize {
 583        self.active_item_index
 584    }
 585
 586    pub fn activation_history(&self) -> &[ActivationHistoryEntry] {
 587        &self.activation_history
 588    }
 589
 590    pub fn set_should_display_tab_bar<F>(&mut self, should_display_tab_bar: F)
 591    where
 592        F: 'static + Fn(&ViewContext<Pane>) -> bool,
 593    {
 594        self.should_display_tab_bar = Rc::new(should_display_tab_bar);
 595    }
 596
 597    pub fn set_can_split(&mut self, can_split: bool, cx: &mut ViewContext<Self>) {
 598        self.can_split = can_split;
 599        cx.notify();
 600    }
 601
 602    pub fn set_can_navigate(&mut self, can_navigate: bool, cx: &mut ViewContext<Self>) {
 603        self.toolbar.update(cx, |toolbar, cx| {
 604            toolbar.set_can_navigate(can_navigate, cx);
 605        });
 606        cx.notify();
 607    }
 608
 609    pub fn set_render_tab_bar_buttons<F>(&mut self, cx: &mut ViewContext<Self>, render: F)
 610    where
 611        F: 'static
 612            + Fn(&mut Pane, &mut ViewContext<Pane>) -> (Option<AnyElement>, Option<AnyElement>),
 613    {
 614        self.render_tab_bar_buttons = Rc::new(render);
 615        cx.notify();
 616    }
 617
 618    pub fn set_custom_drop_handle<F>(&mut self, cx: &mut ViewContext<Self>, handle: F)
 619    where
 620        F: 'static + Fn(&mut Pane, &dyn Any, &mut ViewContext<Pane>) -> ControlFlow<(), ()>,
 621    {
 622        self.custom_drop_handle = Some(Arc::new(handle));
 623        cx.notify();
 624    }
 625
 626    pub fn nav_history_for_item<T: Item>(&self, item: &View<T>) -> ItemNavHistory {
 627        ItemNavHistory {
 628            history: self.nav_history.clone(),
 629            item: Arc::new(item.downgrade()),
 630            is_preview: self.preview_item_id == Some(item.item_id()),
 631        }
 632    }
 633
 634    pub fn nav_history(&self) -> &NavHistory {
 635        &self.nav_history
 636    }
 637
 638    pub fn nav_history_mut(&mut self) -> &mut NavHistory {
 639        &mut self.nav_history
 640    }
 641
 642    pub fn disable_history(&mut self) {
 643        self.nav_history.disable();
 644    }
 645
 646    pub fn enable_history(&mut self) {
 647        self.nav_history.enable();
 648    }
 649
 650    pub fn can_navigate_backward(&self) -> bool {
 651        !self.nav_history.0.lock().backward_stack.is_empty()
 652    }
 653
 654    pub fn can_navigate_forward(&self) -> bool {
 655        !self.nav_history.0.lock().forward_stack.is_empty()
 656    }
 657
 658    fn navigate_backward(&mut self, cx: &mut ViewContext<Self>) {
 659        if let Some(workspace) = self.workspace.upgrade() {
 660            let pane = cx.view().downgrade();
 661            cx.window_context().defer(move |cx| {
 662                workspace.update(cx, |workspace, cx| {
 663                    workspace.go_back(pane, cx).detach_and_log_err(cx)
 664                })
 665            })
 666        }
 667    }
 668
 669    fn navigate_forward(&mut self, cx: &mut ViewContext<Self>) {
 670        if let Some(workspace) = self.workspace.upgrade() {
 671            let pane = cx.view().downgrade();
 672            cx.window_context().defer(move |cx| {
 673                workspace.update(cx, |workspace, cx| {
 674                    workspace.go_forward(pane, cx).detach_and_log_err(cx)
 675                })
 676            })
 677        }
 678    }
 679
 680    fn join_into_next(&mut self, cx: &mut ViewContext<Self>) {
 681        cx.emit(Event::JoinIntoNext);
 682    }
 683
 684    fn history_updated(&mut self, cx: &mut ViewContext<Self>) {
 685        self.toolbar.update(cx, |_, cx| cx.notify());
 686    }
 687
 688    pub fn preview_item_id(&self) -> Option<EntityId> {
 689        self.preview_item_id
 690    }
 691
 692    pub fn preview_item(&self) -> Option<Box<dyn ItemHandle>> {
 693        self.preview_item_id
 694            .and_then(|id| self.items.iter().find(|item| item.item_id() == id))
 695            .cloned()
 696    }
 697
 698    fn preview_item_idx(&self) -> Option<usize> {
 699        if let Some(preview_item_id) = self.preview_item_id {
 700            self.items
 701                .iter()
 702                .position(|item| item.item_id() == preview_item_id)
 703        } else {
 704            None
 705        }
 706    }
 707
 708    pub fn is_active_preview_item(&self, item_id: EntityId) -> bool {
 709        self.preview_item_id == Some(item_id)
 710    }
 711
 712    /// Marks the item with the given ID as the preview item.
 713    /// This will be ignored if the global setting `preview_tabs` is disabled.
 714    pub fn set_preview_item_id(&mut self, item_id: Option<EntityId>, cx: &AppContext) {
 715        if PreviewTabsSettings::get_global(cx).enabled {
 716            self.preview_item_id = item_id;
 717        }
 718    }
 719
 720    pub fn handle_item_edit(&mut self, item_id: EntityId, cx: &AppContext) {
 721        if let Some(preview_item) = self.preview_item() {
 722            if preview_item.item_id() == item_id && !preview_item.preserve_preview(cx) {
 723                self.set_preview_item_id(None, cx);
 724            }
 725        }
 726    }
 727
 728    pub(crate) fn open_item(
 729        &mut self,
 730        project_entry_id: Option<ProjectEntryId>,
 731        focus_item: bool,
 732        allow_preview: bool,
 733        cx: &mut ViewContext<Self>,
 734        build_item: impl FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
 735    ) -> Box<dyn ItemHandle> {
 736        let mut existing_item = None;
 737        if let Some(project_entry_id) = project_entry_id {
 738            for (index, item) in self.items.iter().enumerate() {
 739                if item.is_singleton(cx)
 740                    && item.project_entry_ids(cx).as_slice() == [project_entry_id]
 741                {
 742                    let item = item.boxed_clone();
 743                    existing_item = Some((index, item));
 744                    break;
 745                }
 746            }
 747        }
 748
 749        if let Some((index, existing_item)) = existing_item {
 750            // If the item is already open, and the item is a preview item
 751            // and we are not allowing items to open as preview, mark the item as persistent.
 752            if let Some(preview_item_id) = self.preview_item_id {
 753                if let Some(tab) = self.items.get(index) {
 754                    if tab.item_id() == preview_item_id && !allow_preview {
 755                        self.set_preview_item_id(None, cx);
 756                    }
 757                }
 758            }
 759
 760            self.activate_item(index, focus_item, focus_item, cx);
 761            existing_item
 762        } else {
 763            // If the item is being opened as preview and we have an existing preview tab,
 764            // open the new item in the position of the existing preview tab.
 765            let destination_index = if allow_preview {
 766                self.close_current_preview_item(cx)
 767            } else {
 768                None
 769            };
 770
 771            let new_item = build_item(cx);
 772
 773            if allow_preview {
 774                self.set_preview_item_id(Some(new_item.item_id()), cx);
 775            }
 776
 777            self.add_item(new_item.clone(), true, focus_item, destination_index, cx);
 778
 779            new_item
 780        }
 781    }
 782
 783    pub fn close_current_preview_item(&mut self, cx: &mut ViewContext<Self>) -> Option<usize> {
 784        let Some(item_idx) = self.preview_item_idx() else {
 785            return None;
 786        };
 787
 788        let prev_active_item_index = self.active_item_index;
 789        self.remove_item(item_idx, false, false, cx);
 790        self.active_item_index = prev_active_item_index;
 791
 792        if item_idx < self.items.len() {
 793            Some(item_idx)
 794        } else {
 795            None
 796        }
 797    }
 798
 799    pub fn add_item(
 800        &mut self,
 801        item: Box<dyn ItemHandle>,
 802        activate_pane: bool,
 803        focus_item: bool,
 804        destination_index: Option<usize>,
 805        cx: &mut ViewContext<Self>,
 806    ) {
 807        if item.is_singleton(cx) {
 808            if let Some(&entry_id) = item.project_entry_ids(cx).get(0) {
 809                let project = self.project.read(cx);
 810                if let Some(project_path) = project.path_for_entry(entry_id, cx) {
 811                    let abs_path = project.absolute_path(&project_path, cx);
 812                    self.nav_history
 813                        .0
 814                        .lock()
 815                        .paths_by_item
 816                        .insert(item.item_id(), (project_path, abs_path));
 817                }
 818            }
 819        }
 820        // If no destination index is specified, add or move the item after the active item.
 821        let mut insertion_index = {
 822            cmp::min(
 823                if let Some(destination_index) = destination_index {
 824                    destination_index
 825                } else {
 826                    self.active_item_index + 1
 827                },
 828                self.items.len(),
 829            )
 830        };
 831
 832        // Does the item already exist?
 833        let project_entry_id = if item.is_singleton(cx) {
 834            item.project_entry_ids(cx).get(0).copied()
 835        } else {
 836            None
 837        };
 838
 839        let existing_item_index = self.items.iter().position(|existing_item| {
 840            if existing_item.item_id() == item.item_id() {
 841                true
 842            } else if existing_item.is_singleton(cx) {
 843                existing_item
 844                    .project_entry_ids(cx)
 845                    .get(0)
 846                    .map_or(false, |existing_entry_id| {
 847                        Some(existing_entry_id) == project_entry_id.as_ref()
 848                    })
 849            } else {
 850                false
 851            }
 852        });
 853
 854        if let Some(existing_item_index) = existing_item_index {
 855            // If the item already exists, move it to the desired destination and activate it
 856
 857            if existing_item_index != insertion_index {
 858                let existing_item_is_active = existing_item_index == self.active_item_index;
 859
 860                // If the caller didn't specify a destination and the added item is already
 861                // the active one, don't move it
 862                if existing_item_is_active && destination_index.is_none() {
 863                    insertion_index = existing_item_index;
 864                } else {
 865                    self.items.remove(existing_item_index);
 866                    if existing_item_index < self.active_item_index {
 867                        self.active_item_index -= 1;
 868                    }
 869                    insertion_index = insertion_index.min(self.items.len());
 870
 871                    self.items.insert(insertion_index, item.clone());
 872
 873                    if existing_item_is_active {
 874                        self.active_item_index = insertion_index;
 875                    } else if insertion_index <= self.active_item_index {
 876                        self.active_item_index += 1;
 877                    }
 878                }
 879
 880                cx.notify();
 881            }
 882
 883            self.activate_item(insertion_index, activate_pane, focus_item, cx);
 884        } else {
 885            self.items.insert(insertion_index, item.clone());
 886
 887            if insertion_index <= self.active_item_index
 888                && self.preview_item_idx() != Some(self.active_item_index)
 889            {
 890                self.active_item_index += 1;
 891            }
 892
 893            self.activate_item(insertion_index, activate_pane, focus_item, cx);
 894            cx.notify();
 895        }
 896
 897        cx.emit(Event::AddItem { item });
 898    }
 899
 900    pub fn items_len(&self) -> usize {
 901        self.items.len()
 902    }
 903
 904    pub fn items(&self) -> impl DoubleEndedIterator<Item = &Box<dyn ItemHandle>> {
 905        self.items.iter()
 906    }
 907
 908    pub fn items_of_type<T: Render>(&self) -> impl '_ + Iterator<Item = View<T>> {
 909        self.items
 910            .iter()
 911            .filter_map(|item| item.to_any().downcast().ok())
 912    }
 913
 914    pub fn active_item(&self) -> Option<Box<dyn ItemHandle>> {
 915        self.items.get(self.active_item_index).cloned()
 916    }
 917
 918    pub fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Point<Pixels>> {
 919        self.items
 920            .get(self.active_item_index)?
 921            .pixel_position_of_cursor(cx)
 922    }
 923
 924    pub fn item_for_entry(
 925        &self,
 926        entry_id: ProjectEntryId,
 927        cx: &AppContext,
 928    ) -> Option<Box<dyn ItemHandle>> {
 929        self.items.iter().find_map(|item| {
 930            if item.is_singleton(cx) && (item.project_entry_ids(cx).as_slice() == [entry_id]) {
 931                Some(item.boxed_clone())
 932            } else {
 933                None
 934            }
 935        })
 936    }
 937
 938    pub fn item_for_path(
 939        &self,
 940        project_path: ProjectPath,
 941        cx: &AppContext,
 942    ) -> Option<Box<dyn ItemHandle>> {
 943        self.items.iter().find_map(move |item| {
 944            if item.is_singleton(cx) && (item.project_path(cx).as_slice() == [project_path.clone()])
 945            {
 946                Some(item.boxed_clone())
 947            } else {
 948                None
 949            }
 950        })
 951    }
 952
 953    pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option<usize> {
 954        self.index_for_item_id(item.item_id())
 955    }
 956
 957    fn index_for_item_id(&self, item_id: EntityId) -> Option<usize> {
 958        self.items.iter().position(|i| i.item_id() == item_id)
 959    }
 960
 961    pub fn item_for_index(&self, ix: usize) -> Option<&dyn ItemHandle> {
 962        self.items.get(ix).map(|i| i.as_ref())
 963    }
 964
 965    pub fn toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext<Self>) {
 966        if self.zoomed {
 967            cx.emit(Event::ZoomOut);
 968        } else if !self.items.is_empty() {
 969            if !self.focus_handle.contains_focused(cx) {
 970                cx.focus_self();
 971            }
 972            cx.emit(Event::ZoomIn);
 973        }
 974    }
 975
 976    pub fn activate_item(
 977        &mut self,
 978        index: usize,
 979        activate_pane: bool,
 980        focus_item: bool,
 981        cx: &mut ViewContext<Self>,
 982    ) {
 983        use NavigationMode::{GoingBack, GoingForward};
 984
 985        if index < self.items.len() {
 986            let prev_active_item_ix = mem::replace(&mut self.active_item_index, index);
 987            if prev_active_item_ix != self.active_item_index
 988                || matches!(self.nav_history.mode(), GoingBack | GoingForward)
 989            {
 990                if let Some(prev_item) = self.items.get(prev_active_item_ix) {
 991                    prev_item.deactivated(cx);
 992                }
 993            }
 994            cx.emit(Event::ActivateItem {
 995                local: activate_pane,
 996            });
 997
 998            if let Some(newly_active_item) = self.items.get(index) {
 999                self.activation_history
1000                    .retain(|entry| entry.entity_id != newly_active_item.item_id());
1001                self.activation_history.push(ActivationHistoryEntry {
1002                    entity_id: newly_active_item.item_id(),
1003                    timestamp: self
1004                        .next_activation_timestamp
1005                        .fetch_add(1, Ordering::SeqCst),
1006                });
1007            }
1008
1009            self.update_toolbar(cx);
1010            self.update_status_bar(cx);
1011
1012            if focus_item {
1013                self.focus_active_item(cx);
1014            }
1015
1016            self.tab_bar_scroll_handle.scroll_to_item(index);
1017            cx.notify();
1018        }
1019    }
1020
1021    pub fn activate_prev_item(&mut self, activate_pane: bool, cx: &mut ViewContext<Self>) {
1022        let mut index = self.active_item_index;
1023        if index > 0 {
1024            index -= 1;
1025        } else if !self.items.is_empty() {
1026            index = self.items.len() - 1;
1027        }
1028        self.activate_item(index, activate_pane, activate_pane, cx);
1029    }
1030
1031    pub fn activate_next_item(&mut self, activate_pane: bool, cx: &mut ViewContext<Self>) {
1032        let mut index = self.active_item_index;
1033        if index + 1 < self.items.len() {
1034            index += 1;
1035        } else {
1036            index = 0;
1037        }
1038        self.activate_item(index, activate_pane, activate_pane, cx);
1039    }
1040
1041    pub fn close_active_item(
1042        &mut self,
1043        action: &CloseActiveItem,
1044        cx: &mut ViewContext<Self>,
1045    ) -> Option<Task<Result<()>>> {
1046        if self.items.is_empty() {
1047            // Close the window when there's no active items to close, if configured
1048            if WorkspaceSettings::get_global(cx)
1049                .when_closing_with_no_tabs
1050                .should_close()
1051            {
1052                cx.dispatch_action(Box::new(CloseWindow));
1053            }
1054
1055            return None;
1056        }
1057        let active_item_id = self.items[self.active_item_index].item_id();
1058        Some(self.close_item_by_id(
1059            active_item_id,
1060            action.save_intent.unwrap_or(SaveIntent::Close),
1061            cx,
1062        ))
1063    }
1064
1065    pub fn close_item_by_id(
1066        &mut self,
1067        item_id_to_close: EntityId,
1068        save_intent: SaveIntent,
1069        cx: &mut ViewContext<Self>,
1070    ) -> Task<Result<()>> {
1071        self.close_items(cx, save_intent, move |view_id| view_id == item_id_to_close)
1072    }
1073
1074    pub fn close_inactive_items(
1075        &mut self,
1076        action: &CloseInactiveItems,
1077        cx: &mut ViewContext<Self>,
1078    ) -> Option<Task<Result<()>>> {
1079        if self.items.is_empty() {
1080            return None;
1081        }
1082
1083        let active_item_id = self.items[self.active_item_index].item_id();
1084        Some(self.close_items(
1085            cx,
1086            action.save_intent.unwrap_or(SaveIntent::Close),
1087            move |item_id| item_id != active_item_id,
1088        ))
1089    }
1090
1091    pub fn close_clean_items(
1092        &mut self,
1093        _: &CloseCleanItems,
1094        cx: &mut ViewContext<Self>,
1095    ) -> Option<Task<Result<()>>> {
1096        let item_ids: Vec<_> = self
1097            .items()
1098            .filter(|item| !item.is_dirty(cx))
1099            .map(|item| item.item_id())
1100            .collect();
1101        Some(self.close_items(cx, SaveIntent::Close, move |item_id| {
1102            item_ids.contains(&item_id)
1103        }))
1104    }
1105
1106    pub fn close_items_to_the_left(
1107        &mut self,
1108        _: &CloseItemsToTheLeft,
1109        cx: &mut ViewContext<Self>,
1110    ) -> Option<Task<Result<()>>> {
1111        if self.items.is_empty() {
1112            return None;
1113        }
1114        let active_item_id = self.items[self.active_item_index].item_id();
1115        Some(self.close_items_to_the_left_by_id(active_item_id, cx))
1116    }
1117
1118    pub fn close_items_to_the_left_by_id(
1119        &mut self,
1120        item_id: EntityId,
1121        cx: &mut ViewContext<Self>,
1122    ) -> Task<Result<()>> {
1123        let item_ids: Vec<_> = self
1124            .items()
1125            .take_while(|item| item.item_id() != item_id)
1126            .map(|item| item.item_id())
1127            .collect();
1128        self.close_items(cx, SaveIntent::Close, move |item_id| {
1129            item_ids.contains(&item_id)
1130        })
1131    }
1132
1133    pub fn close_items_to_the_right(
1134        &mut self,
1135        _: &CloseItemsToTheRight,
1136        cx: &mut ViewContext<Self>,
1137    ) -> Option<Task<Result<()>>> {
1138        if self.items.is_empty() {
1139            return None;
1140        }
1141        let active_item_id = self.items[self.active_item_index].item_id();
1142        Some(self.close_items_to_the_right_by_id(active_item_id, cx))
1143    }
1144
1145    pub fn close_items_to_the_right_by_id(
1146        &mut self,
1147        item_id: EntityId,
1148        cx: &mut ViewContext<Self>,
1149    ) -> Task<Result<()>> {
1150        let item_ids: Vec<_> = self
1151            .items()
1152            .rev()
1153            .take_while(|item| item.item_id() != item_id)
1154            .map(|item| item.item_id())
1155            .collect();
1156        self.close_items(cx, SaveIntent::Close, move |item_id| {
1157            item_ids.contains(&item_id)
1158        })
1159    }
1160
1161    pub fn close_all_items(
1162        &mut self,
1163        action: &CloseAllItems,
1164        cx: &mut ViewContext<Self>,
1165    ) -> Option<Task<Result<()>>> {
1166        if self.items.is_empty() {
1167            return None;
1168        }
1169
1170        Some(
1171            self.close_items(cx, action.save_intent.unwrap_or(SaveIntent::Close), |_| {
1172                true
1173            }),
1174        )
1175    }
1176
1177    pub(super) fn file_names_for_prompt(
1178        items: &mut dyn Iterator<Item = &Box<dyn ItemHandle>>,
1179        all_dirty_items: usize,
1180        cx: &AppContext,
1181    ) -> (String, String) {
1182        /// Quantity of item paths displayed in prompt prior to cutoff..
1183        const FILE_NAMES_CUTOFF_POINT: usize = 10;
1184        let mut file_names: Vec<_> = items
1185            .filter_map(|item| {
1186                item.project_path(cx).and_then(|project_path| {
1187                    project_path
1188                        .path
1189                        .file_name()
1190                        .and_then(|name| name.to_str().map(ToOwned::to_owned))
1191                })
1192            })
1193            .take(FILE_NAMES_CUTOFF_POINT)
1194            .collect();
1195        let should_display_followup_text =
1196            all_dirty_items > FILE_NAMES_CUTOFF_POINT || file_names.len() != all_dirty_items;
1197        if should_display_followup_text {
1198            let not_shown_files = all_dirty_items - file_names.len();
1199            if not_shown_files == 1 {
1200                file_names.push(".. 1 file not shown".into());
1201            } else {
1202                file_names.push(format!(".. {} files not shown", not_shown_files));
1203            }
1204        }
1205        (
1206            format!(
1207                "Do you want to save changes to the following {} files?",
1208                all_dirty_items
1209            ),
1210            file_names.join("\n"),
1211        )
1212    }
1213
1214    pub fn close_items(
1215        &mut self,
1216        cx: &mut ViewContext<Pane>,
1217        mut save_intent: SaveIntent,
1218        should_close: impl Fn(EntityId) -> bool,
1219    ) -> Task<Result<()>> {
1220        // Find the items to close.
1221        let mut items_to_close = Vec::new();
1222        let mut dirty_items = Vec::new();
1223        for item in &self.items {
1224            if should_close(item.item_id()) {
1225                items_to_close.push(item.boxed_clone());
1226                if item.is_dirty(cx) {
1227                    dirty_items.push(item.boxed_clone());
1228                }
1229            }
1230        }
1231
1232        let active_item_id = self.active_item().map(|item| item.item_id());
1233
1234        items_to_close.sort_by_key(|item| {
1235            // Put the currently active item at the end, because if the currently active item is not closed last
1236            // closing the currently active item will cause the focus to switch to another item
1237            // This will cause Zed to expand the content of the currently active item
1238            active_item_id.filter(|&id| id == item.item_id()).is_some()
1239              // If a buffer is open both in a singleton editor and in a multibuffer, make sure
1240              // to focus the singleton buffer when prompting to save that buffer, as opposed
1241              // to focusing the multibuffer, because this gives the user a more clear idea
1242              // of what content they would be saving.
1243              || !item.is_singleton(cx)
1244        });
1245
1246        let workspace = self.workspace.clone();
1247        cx.spawn(|pane, mut cx| async move {
1248            if save_intent == SaveIntent::Close && dirty_items.len() > 1 {
1249                let answer = pane.update(&mut cx, |_, cx| {
1250                    let (prompt, detail) =
1251                        Self::file_names_for_prompt(&mut dirty_items.iter(), dirty_items.len(), cx);
1252                    cx.prompt(
1253                        PromptLevel::Warning,
1254                        &prompt,
1255                        Some(&detail),
1256                        &["Save all", "Discard all", "Cancel"],
1257                    )
1258                })?;
1259                match answer.await {
1260                    Ok(0) => save_intent = SaveIntent::SaveAll,
1261                    Ok(1) => save_intent = SaveIntent::Skip,
1262                    _ => {}
1263                }
1264            }
1265            let mut saved_project_items_ids = HashSet::default();
1266            for item in items_to_close.clone() {
1267                // Find the item's current index and its set of project item models. Avoid
1268                // storing these in advance, in case they have changed since this task
1269                // was started.
1270                let (item_ix, mut project_item_ids) = pane.update(&mut cx, |pane, cx| {
1271                    (pane.index_for_item(&*item), item.project_item_model_ids(cx))
1272                })?;
1273                let item_ix = if let Some(ix) = item_ix {
1274                    ix
1275                } else {
1276                    continue;
1277                };
1278
1279                // Check if this view has any project items that are not open anywhere else
1280                // in the workspace, AND that the user has not already been prompted to save.
1281                // If there are any such project entries, prompt the user to save this item.
1282                let project = workspace.update(&mut cx, |workspace, cx| {
1283                    for item in workspace.items(cx) {
1284                        if !items_to_close
1285                            .iter()
1286                            .any(|item_to_close| item_to_close.item_id() == item.item_id())
1287                        {
1288                            let other_project_item_ids = item.project_item_model_ids(cx);
1289                            project_item_ids.retain(|id| !other_project_item_ids.contains(id));
1290                        }
1291                    }
1292                    workspace.project().clone()
1293                })?;
1294                let should_save = project_item_ids
1295                    .iter()
1296                    .any(|id| saved_project_items_ids.insert(*id));
1297
1298                if should_save
1299                    && !Self::save_item(
1300                        project.clone(),
1301                        &pane,
1302                        item_ix,
1303                        &*item,
1304                        save_intent,
1305                        &mut cx,
1306                    )
1307                    .await?
1308                {
1309                    break;
1310                }
1311
1312                // Remove the item from the pane.
1313                pane.update(&mut cx, |pane, cx| {
1314                    if let Some(item_ix) = pane
1315                        .items
1316                        .iter()
1317                        .position(|i| i.item_id() == item.item_id())
1318                    {
1319                        pane.remove_item(item_ix, false, true, cx);
1320                    }
1321                })
1322                .ok();
1323            }
1324
1325            pane.update(&mut cx, |_, cx| cx.notify()).ok();
1326            Ok(())
1327        })
1328    }
1329
1330    pub fn remove_item(
1331        &mut self,
1332        item_index: usize,
1333        activate_pane: bool,
1334        close_pane_if_empty: bool,
1335        cx: &mut ViewContext<Self>,
1336    ) {
1337        self._remove_item(item_index, activate_pane, close_pane_if_empty, None, cx)
1338    }
1339
1340    pub fn remove_item_and_focus_on_pane(
1341        &mut self,
1342        item_index: usize,
1343        activate_pane: bool,
1344        focus_on_pane_if_closed: View<Pane>,
1345        cx: &mut ViewContext<Self>,
1346    ) {
1347        self._remove_item(
1348            item_index,
1349            activate_pane,
1350            true,
1351            Some(focus_on_pane_if_closed),
1352            cx,
1353        )
1354    }
1355
1356    fn _remove_item(
1357        &mut self,
1358        item_index: usize,
1359        activate_pane: bool,
1360        close_pane_if_empty: bool,
1361        focus_on_pane_if_closed: Option<View<Pane>>,
1362        cx: &mut ViewContext<Self>,
1363    ) {
1364        self.activation_history
1365            .retain(|entry| entry.entity_id != self.items[item_index].item_id());
1366
1367        if item_index == self.active_item_index {
1368            let index_to_activate = self
1369                .activation_history
1370                .pop()
1371                .and_then(|last_activated_item| {
1372                    self.items.iter().enumerate().find_map(|(index, item)| {
1373                        (item.item_id() == last_activated_item.entity_id).then_some(index)
1374                    })
1375                })
1376                // We didn't have a valid activation history entry, so fallback
1377                // to activating the item to the left
1378                .unwrap_or_else(|| item_index.min(self.items.len()).saturating_sub(1));
1379
1380            let should_activate = activate_pane || self.has_focus(cx);
1381            if self.items.len() == 1 && should_activate {
1382                self.focus_handle.focus(cx);
1383            } else {
1384                self.activate_item(index_to_activate, should_activate, should_activate, cx);
1385            }
1386        }
1387
1388        cx.emit(Event::RemoveItem { idx: item_index });
1389
1390        let item = self.items.remove(item_index);
1391
1392        cx.emit(Event::RemovedItem {
1393            item_id: item.item_id(),
1394        });
1395        if self.items.is_empty() {
1396            item.deactivated(cx);
1397            if close_pane_if_empty {
1398                self.update_toolbar(cx);
1399                cx.emit(Event::Remove {
1400                    focus_on_pane: focus_on_pane_if_closed,
1401                });
1402            }
1403        }
1404
1405        if item_index < self.active_item_index {
1406            self.active_item_index -= 1;
1407        }
1408
1409        let mode = self.nav_history.mode();
1410        self.nav_history.set_mode(NavigationMode::ClosingItem);
1411        item.deactivated(cx);
1412        self.nav_history.set_mode(mode);
1413
1414        if self.is_active_preview_item(item.item_id()) {
1415            self.set_preview_item_id(None, cx);
1416        }
1417
1418        if let Some(path) = item.project_path(cx) {
1419            let abs_path = self
1420                .nav_history
1421                .0
1422                .lock()
1423                .paths_by_item
1424                .get(&item.item_id())
1425                .and_then(|(_, abs_path)| abs_path.clone());
1426
1427            self.nav_history
1428                .0
1429                .lock()
1430                .paths_by_item
1431                .insert(item.item_id(), (path, abs_path));
1432        } else {
1433            self.nav_history
1434                .0
1435                .lock()
1436                .paths_by_item
1437                .remove(&item.item_id());
1438        }
1439
1440        if self.items.is_empty() && close_pane_if_empty && self.zoomed {
1441            cx.emit(Event::ZoomOut);
1442        }
1443
1444        cx.notify();
1445    }
1446
1447    pub async fn save_item(
1448        project: Model<Project>,
1449        pane: &WeakView<Pane>,
1450        item_ix: usize,
1451        item: &dyn ItemHandle,
1452        save_intent: SaveIntent,
1453        cx: &mut AsyncWindowContext,
1454    ) -> Result<bool> {
1455        const CONFLICT_MESSAGE: &str =
1456                "This file has changed on disk since you started editing it. Do you want to overwrite it?";
1457
1458        if save_intent == SaveIntent::Skip {
1459            return Ok(true);
1460        }
1461
1462        let (mut has_conflict, mut is_dirty, mut can_save, can_save_as) = cx.update(|cx| {
1463            (
1464                item.has_conflict(cx),
1465                item.is_dirty(cx),
1466                item.can_save(cx),
1467                item.is_singleton(cx),
1468            )
1469        })?;
1470
1471        // when saving a single buffer, we ignore whether or not it's dirty.
1472        if save_intent == SaveIntent::Save || save_intent == SaveIntent::SaveWithoutFormat {
1473            is_dirty = true;
1474        }
1475
1476        if save_intent == SaveIntent::SaveAs {
1477            is_dirty = true;
1478            has_conflict = false;
1479            can_save = false;
1480        }
1481
1482        if save_intent == SaveIntent::Overwrite {
1483            has_conflict = false;
1484        }
1485
1486        let should_format = save_intent != SaveIntent::SaveWithoutFormat;
1487
1488        if has_conflict && can_save {
1489            let answer = pane.update(cx, |pane, cx| {
1490                pane.activate_item(item_ix, true, true, cx);
1491                cx.prompt(
1492                    PromptLevel::Warning,
1493                    CONFLICT_MESSAGE,
1494                    None,
1495                    &["Overwrite", "Discard", "Cancel"],
1496                )
1497            })?;
1498            match answer.await {
1499                Ok(0) => {
1500                    pane.update(cx, |_, cx| item.save(should_format, project, cx))?
1501                        .await?
1502                }
1503                Ok(1) => pane.update(cx, |_, cx| item.reload(project, cx))?.await?,
1504                _ => return Ok(false),
1505            }
1506        } else if is_dirty && (can_save || can_save_as) {
1507            if save_intent == SaveIntent::Close {
1508                let will_autosave = cx.update(|cx| {
1509                    matches!(
1510                        item.workspace_settings(cx).autosave,
1511                        AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange
1512                    ) && Self::can_autosave_item(item, cx)
1513                })?;
1514                if !will_autosave {
1515                    let item_id = item.item_id();
1516                    let answer_task = pane.update(cx, |pane, cx| {
1517                        if pane.save_modals_spawned.insert(item_id) {
1518                            pane.activate_item(item_ix, true, true, cx);
1519                            let prompt = dirty_message_for(item.project_path(cx));
1520                            Some(cx.prompt(
1521                                PromptLevel::Warning,
1522                                &prompt,
1523                                None,
1524                                &["Save", "Don't Save", "Cancel"],
1525                            ))
1526                        } else {
1527                            None
1528                        }
1529                    })?;
1530                    if let Some(answer_task) = answer_task {
1531                        let answer = answer_task.await;
1532                        pane.update(cx, |pane, _| {
1533                            if !pane.save_modals_spawned.remove(&item_id) {
1534                                debug_panic!(
1535                                    "save modal was not present in spawned modals after awaiting for its answer"
1536                                )
1537                            }
1538                        })?;
1539                        match answer {
1540                            Ok(0) => {}
1541                            Ok(1) => {
1542                                // Don't save this file
1543                                pane.update(cx, |_, cx| item.discarded(project, cx))
1544                                    .log_err();
1545                                return Ok(true);
1546                            }
1547                            _ => return Ok(false), // Cancel
1548                        }
1549                    } else {
1550                        return Ok(false);
1551                    }
1552                }
1553            }
1554
1555            if can_save {
1556                pane.update(cx, |_, cx| item.save(should_format, project, cx))?
1557                    .await?;
1558            } else if can_save_as {
1559                let abs_path = pane.update(cx, |pane, cx| {
1560                    pane.workspace
1561                        .update(cx, |workspace, cx| workspace.prompt_for_new_path(cx))
1562                })??;
1563                if let Some(abs_path) = abs_path.await.ok().flatten() {
1564                    pane.update(cx, |pane, cx| {
1565                        if let Some(item) = pane.item_for_path(abs_path.clone(), cx) {
1566                            if let Some(idx) = pane.index_for_item(&*item) {
1567                                pane.remove_item(idx, false, false, cx);
1568                            }
1569                        }
1570
1571                        item.save_as(project, abs_path, cx)
1572                    })?
1573                    .await?;
1574                } else {
1575                    return Ok(false);
1576                }
1577            }
1578        }
1579
1580        pane.update(cx, |_, cx| {
1581            cx.emit(Event::UserSavedItem {
1582                item: item.downgrade_item(),
1583                save_intent,
1584            });
1585            true
1586        })
1587    }
1588
1589    fn can_autosave_item(item: &dyn ItemHandle, cx: &AppContext) -> bool {
1590        let is_deleted = item.project_entry_ids(cx).is_empty();
1591        item.is_dirty(cx) && !item.has_conflict(cx) && item.can_save(cx) && !is_deleted
1592    }
1593
1594    pub fn autosave_item(
1595        item: &dyn ItemHandle,
1596        project: Model<Project>,
1597        cx: &mut WindowContext,
1598    ) -> Task<Result<()>> {
1599        let format =
1600            if let AutosaveSetting::AfterDelay { .. } = item.workspace_settings(cx).autosave {
1601                false
1602            } else {
1603                true
1604            };
1605        if Self::can_autosave_item(item, cx) {
1606            item.save(format, project, cx)
1607        } else {
1608            Task::ready(Ok(()))
1609        }
1610    }
1611
1612    pub fn focus(&mut self, cx: &mut ViewContext<Pane>) {
1613        cx.focus(&self.focus_handle);
1614    }
1615
1616    pub fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
1617        if let Some(active_item) = self.active_item() {
1618            let focus_handle = active_item.focus_handle(cx);
1619            cx.focus(&focus_handle);
1620        }
1621    }
1622
1623    pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext<Self>) {
1624        cx.emit(Event::Split(direction));
1625    }
1626
1627    pub fn toolbar(&self) -> &View<Toolbar> {
1628        &self.toolbar
1629    }
1630
1631    pub fn handle_deleted_project_item(
1632        &mut self,
1633        entry_id: ProjectEntryId,
1634        cx: &mut ViewContext<Pane>,
1635    ) -> Option<()> {
1636        let (item_index_to_delete, item_id) = self.items().enumerate().find_map(|(i, item)| {
1637            if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
1638                Some((i, item.item_id()))
1639            } else {
1640                None
1641            }
1642        })?;
1643
1644        self.remove_item(item_index_to_delete, false, true, cx);
1645        self.nav_history.remove_item(item_id);
1646
1647        Some(())
1648    }
1649
1650    fn update_toolbar(&mut self, cx: &mut ViewContext<Self>) {
1651        let active_item = self
1652            .items
1653            .get(self.active_item_index)
1654            .map(|item| item.as_ref());
1655        self.toolbar.update(cx, |toolbar, cx| {
1656            toolbar.set_active_item(active_item, cx);
1657        });
1658    }
1659
1660    fn update_status_bar(&mut self, cx: &mut ViewContext<Self>) {
1661        let workspace = self.workspace.clone();
1662        let pane = cx.view().clone();
1663
1664        cx.window_context().defer(move |cx| {
1665            let Ok(status_bar) = workspace.update(cx, |workspace, _| workspace.status_bar.clone())
1666            else {
1667                return;
1668            };
1669
1670            status_bar.update(cx, move |status_bar, cx| {
1671                status_bar.set_active_pane(&pane, cx);
1672            });
1673        });
1674    }
1675
1676    fn entry_abs_path(&self, entry: ProjectEntryId, cx: &WindowContext) -> Option<PathBuf> {
1677        let worktree = self
1678            .workspace
1679            .upgrade()?
1680            .read(cx)
1681            .project()
1682            .read(cx)
1683            .worktree_for_entry(entry, cx)?
1684            .read(cx);
1685        let entry = worktree.entry_for_id(entry)?;
1686        let abs_path = worktree.absolutize(&entry.path).ok()?;
1687        if entry.is_symlink {
1688            abs_path.canonicalize().ok()
1689        } else {
1690            Some(abs_path)
1691        }
1692    }
1693
1694    fn copy_relative_path(&mut self, _: &CopyRelativePath, cx: &mut ViewContext<Self>) {
1695        if let Some(clipboard_text) = self
1696            .active_item()
1697            .as_ref()
1698            .and_then(|entry| entry.project_path(cx))
1699            .map(|p| p.path.to_string_lossy().to_string())
1700        {
1701            cx.write_to_clipboard(ClipboardItem::new_string(clipboard_text));
1702        }
1703    }
1704
1705    pub fn icon_color(selected: bool) -> Color {
1706        if selected {
1707            Color::Default
1708        } else {
1709            Color::Muted
1710        }
1711    }
1712
1713    pub fn git_aware_icon_color(
1714        git_status: Option<GitFileStatus>,
1715        ignored: bool,
1716        selected: bool,
1717    ) -> Color {
1718        if ignored {
1719            Color::Ignored
1720        } else {
1721            match git_status {
1722                Some(GitFileStatus::Added) => Color::Created,
1723                Some(GitFileStatus::Modified) => Color::Modified,
1724                Some(GitFileStatus::Conflict) => Color::Conflict,
1725                None => Self::icon_color(selected),
1726            }
1727        }
1728    }
1729
1730    fn toggle_pin_tab(&mut self, _: &TogglePinTab, cx: &mut ViewContext<'_, Self>) {
1731        if self.items.is_empty() {
1732            return;
1733        }
1734        let active_tab_ix = self.active_item_index();
1735        if self.is_tab_pinned(active_tab_ix) {
1736            self.unpin_tab_at(active_tab_ix, cx);
1737        } else {
1738            self.pin_tab_at(active_tab_ix, cx);
1739        }
1740    }
1741
1742    fn pin_tab_at(&mut self, ix: usize, cx: &mut ViewContext<'_, Self>) {
1743        maybe!({
1744            let pane = cx.view().clone();
1745            let destination_index = self.pinned_tab_count;
1746            self.pinned_tab_count += 1;
1747            let id = self.item_for_index(ix)?.item_id();
1748
1749            self.workspace
1750                .update(cx, |_, cx| {
1751                    cx.defer(move |this, cx| {
1752                        this.move_item(pane.clone(), pane, id, destination_index, cx)
1753                    });
1754                })
1755                .ok()?;
1756
1757            Some(())
1758        });
1759    }
1760
1761    fn unpin_tab_at(&mut self, ix: usize, cx: &mut ViewContext<'_, Self>) {
1762        maybe!({
1763            let pane = cx.view().clone();
1764            self.pinned_tab_count = self.pinned_tab_count.checked_sub(1).unwrap();
1765            let destination_index = self.pinned_tab_count;
1766
1767            let id = self.item_for_index(ix)?.item_id();
1768
1769            self.workspace
1770                .update(cx, |_, cx| {
1771                    cx.defer(move |this, cx| {
1772                        this.move_item(pane.clone(), pane, id, destination_index, cx)
1773                    });
1774                })
1775                .ok()?;
1776
1777            Some(())
1778        });
1779    }
1780
1781    fn is_tab_pinned(&self, ix: usize) -> bool {
1782        self.pinned_tab_count > ix
1783    }
1784
1785    fn has_pinned_tabs(&self) -> bool {
1786        self.pinned_tab_count != 0
1787    }
1788
1789    fn render_tab(
1790        &self,
1791        ix: usize,
1792        item: &dyn ItemHandle,
1793        detail: usize,
1794        cx: &mut ViewContext<'_, Pane>,
1795    ) -> impl IntoElement {
1796        let project_path = item.project_path(cx);
1797
1798        let is_active = ix == self.active_item_index;
1799        let is_preview = self
1800            .preview_item_id
1801            .map(|id| id == item.item_id())
1802            .unwrap_or(false);
1803
1804        let label = item.tab_content(
1805            TabContentParams {
1806                detail: Some(detail),
1807                selected: is_active,
1808                preview: is_preview,
1809            },
1810            cx,
1811        );
1812
1813        let icon_color = if ItemSettings::get_global(cx).git_status {
1814            project_path
1815                .as_ref()
1816                .and_then(|path| self.project.read(cx).entry_for_path(&path, cx))
1817                .map(|entry| {
1818                    Self::git_aware_icon_color(entry.git_status, entry.is_ignored, is_active)
1819                })
1820                .unwrap_or_else(|| Self::icon_color(is_active))
1821        } else {
1822            Self::icon_color(is_active)
1823        };
1824
1825        let icon = item.tab_icon(cx);
1826        let close_side = &ItemSettings::get_global(cx).close_position;
1827        let indicator = render_item_indicator(item.boxed_clone(), cx);
1828        let item_id = item.item_id();
1829        let is_first_item = ix == 0;
1830        let is_last_item = ix == self.items.len() - 1;
1831        let is_pinned = self.is_tab_pinned(ix);
1832        let position_relative_to_active_item = ix.cmp(&self.active_item_index);
1833
1834        let tab = Tab::new(ix)
1835            .position(if is_first_item {
1836                TabPosition::First
1837            } else if is_last_item {
1838                TabPosition::Last
1839            } else {
1840                TabPosition::Middle(position_relative_to_active_item)
1841            })
1842            .close_side(match close_side {
1843                ClosePosition::Left => ui::TabCloseSide::Start,
1844                ClosePosition::Right => ui::TabCloseSide::End,
1845            })
1846            .selected(is_active)
1847            .on_click(
1848                cx.listener(move |pane: &mut Self, _, cx| pane.activate_item(ix, true, true, cx)),
1849            )
1850            // TODO: This should be a click listener with the middle mouse button instead of a mouse down listener.
1851            .on_mouse_down(
1852                MouseButton::Middle,
1853                cx.listener(move |pane, _event, cx| {
1854                    pane.close_item_by_id(item_id, SaveIntent::Close, cx)
1855                        .detach_and_log_err(cx);
1856                }),
1857            )
1858            .on_mouse_down(
1859                MouseButton::Left,
1860                cx.listener(move |pane, event: &MouseDownEvent, cx| {
1861                    if let Some(id) = pane.preview_item_id {
1862                        if id == item_id && event.click_count > 1 {
1863                            pane.set_preview_item_id(None, cx);
1864                        }
1865                    }
1866                }),
1867            )
1868            .on_drag(
1869                DraggedTab {
1870                    item: item.boxed_clone(),
1871                    pane: cx.view().clone(),
1872                    detail,
1873                    is_active,
1874                    ix,
1875                },
1876                |tab, cx| cx.new_view(|_| tab.clone()),
1877            )
1878            .drag_over::<DraggedTab>(|tab, _, cx| {
1879                tab.bg(cx.theme().colors().drop_target_background)
1880            })
1881            .drag_over::<DraggedSelection>(|tab, _, cx| {
1882                tab.bg(cx.theme().colors().drop_target_background)
1883            })
1884            .when_some(self.can_drop_predicate.clone(), |this, p| {
1885                this.can_drop(move |a, cx| p(a, cx))
1886            })
1887            .on_drop(cx.listener(move |this, dragged_tab: &DraggedTab, cx| {
1888                this.drag_split_direction = None;
1889                this.handle_tab_drop(dragged_tab, ix, cx)
1890            }))
1891            .on_drop(cx.listener(move |this, selection: &DraggedSelection, cx| {
1892                this.drag_split_direction = None;
1893                this.handle_dragged_selection_drop(selection, cx)
1894            }))
1895            .on_drop(cx.listener(move |this, paths, cx| {
1896                this.drag_split_direction = None;
1897                this.handle_external_paths_drop(paths, cx)
1898            }))
1899            .when_some(item.tab_tooltip_text(cx), |tab, text| {
1900                tab.tooltip(move |cx| Tooltip::text(text.clone(), cx))
1901            })
1902            .start_slot::<Indicator>(indicator)
1903            .map(|this| {
1904                let end_slot = if is_pinned {
1905                    IconButton::new("unpin tab", IconName::PinAlt)
1906                        .shape(IconButtonShape::Square)
1907                        .icon_color(Color::Muted)
1908                        .size(ButtonSize::None)
1909                        .icon_size(IconSize::XSmall)
1910                        .on_click(cx.listener(move |pane, _, cx| {
1911                            pane.unpin_tab_at(ix, cx);
1912                        }))
1913                        .tooltip(|cx| Tooltip::text("Unpin Tab", cx))
1914                } else {
1915                    IconButton::new("close tab", IconName::Close)
1916                        .visible_on_hover("")
1917                        .shape(IconButtonShape::Square)
1918                        .icon_color(Color::Muted)
1919                        .size(ButtonSize::None)
1920                        .icon_size(IconSize::XSmall)
1921                        .on_click(cx.listener(move |pane, _, cx| {
1922                            pane.close_item_by_id(item_id, SaveIntent::Close, cx)
1923                                .detach_and_log_err(cx);
1924                        }))
1925                };
1926                this.end_slot(end_slot)
1927            })
1928            .child(
1929                h_flex()
1930                    .gap_1()
1931                    .children(icon.map(|icon| icon.size(IconSize::Small).color(icon_color)))
1932                    .child(label),
1933            );
1934
1935        let single_entry_to_resolve = {
1936            let item_entries = self.items[ix].project_entry_ids(cx);
1937            if item_entries.len() == 1 {
1938                Some(item_entries[0])
1939            } else {
1940                None
1941            }
1942        };
1943
1944        let is_pinned = self.is_tab_pinned(ix);
1945        let pane = cx.view().downgrade();
1946        right_click_menu(ix).trigger(tab).menu(move |cx| {
1947            let pane = pane.clone();
1948            ContextMenu::build(cx, move |mut menu, cx| {
1949                if let Some(pane) = pane.upgrade() {
1950                    menu = menu
1951                        .entry(
1952                            "Close",
1953                            Some(Box::new(CloseActiveItem { save_intent: None })),
1954                            cx.handler_for(&pane, move |pane, cx| {
1955                                pane.close_item_by_id(item_id, SaveIntent::Close, cx)
1956                                    .detach_and_log_err(cx);
1957                            }),
1958                        )
1959                        .entry(
1960                            "Close Others",
1961                            Some(Box::new(CloseInactiveItems { save_intent: None })),
1962                            cx.handler_for(&pane, move |pane, cx| {
1963                                pane.close_items(cx, SaveIntent::Close, |id| id != item_id)
1964                                    .detach_and_log_err(cx);
1965                            }),
1966                        )
1967                        .separator()
1968                        .entry(
1969                            "Close Left",
1970                            Some(Box::new(CloseItemsToTheLeft)),
1971                            cx.handler_for(&pane, move |pane, cx| {
1972                                pane.close_items_to_the_left_by_id(item_id, cx)
1973                                    .detach_and_log_err(cx);
1974                            }),
1975                        )
1976                        .entry(
1977                            "Close Right",
1978                            Some(Box::new(CloseItemsToTheRight)),
1979                            cx.handler_for(&pane, move |pane, cx| {
1980                                pane.close_items_to_the_right_by_id(item_id, cx)
1981                                    .detach_and_log_err(cx);
1982                            }),
1983                        )
1984                        .separator()
1985                        .entry(
1986                            "Close Clean",
1987                            Some(Box::new(CloseCleanItems)),
1988                            cx.handler_for(&pane, move |pane, cx| {
1989                                if let Some(task) = pane.close_clean_items(&CloseCleanItems, cx) {
1990                                    task.detach_and_log_err(cx)
1991                                }
1992                            }),
1993                        )
1994                        .entry(
1995                            "Close All",
1996                            Some(Box::new(CloseAllItems { save_intent: None })),
1997                            cx.handler_for(&pane, |pane, cx| {
1998                                if let Some(task) =
1999                                    pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
2000                                {
2001                                    task.detach_and_log_err(cx)
2002                                }
2003                            }),
2004                        );
2005
2006                    let pin_tab_entries = |menu: ContextMenu| {
2007                        menu.separator().map(|this| {
2008                            if is_pinned {
2009                                this.entry(
2010                                    "Unpin Tab",
2011                                    Some(TogglePinTab.boxed_clone()),
2012                                    cx.handler_for(&pane, move |pane, cx| {
2013                                        pane.unpin_tab_at(ix, cx);
2014                                    }),
2015                                )
2016                            } else {
2017                                this.entry(
2018                                    "Pin Tab",
2019                                    Some(TogglePinTab.boxed_clone()),
2020                                    cx.handler_for(&pane, move |pane, cx| {
2021                                        pane.pin_tab_at(ix, cx);
2022                                    }),
2023                                )
2024                            }
2025                        })
2026                    };
2027                    if let Some(entry) = single_entry_to_resolve {
2028                        let entry_abs_path = pane.read(cx).entry_abs_path(entry, cx);
2029                        let parent_abs_path = entry_abs_path
2030                            .as_deref()
2031                            .and_then(|abs_path| Some(abs_path.parent()?.to_path_buf()));
2032
2033                        let entry_id = entry.to_proto();
2034                        menu = menu
2035                            .separator()
2036                            .when_some(entry_abs_path, |menu, abs_path| {
2037                                menu.entry(
2038                                    "Copy Path",
2039                                    Some(Box::new(CopyPath)),
2040                                    cx.handler_for(&pane, move |_, cx| {
2041                                        cx.write_to_clipboard(ClipboardItem::new_string(
2042                                            abs_path.to_string_lossy().to_string(),
2043                                        ));
2044                                    }),
2045                                )
2046                            })
2047                            .entry(
2048                                "Copy Relative Path",
2049                                Some(Box::new(CopyRelativePath)),
2050                                cx.handler_for(&pane, move |pane, cx| {
2051                                    pane.copy_relative_path(&CopyRelativePath, cx);
2052                                }),
2053                            )
2054                            .map(pin_tab_entries)
2055                            .separator()
2056                            .entry(
2057                                "Reveal In Project Panel",
2058                                Some(Box::new(RevealInProjectPanel {
2059                                    entry_id: Some(entry_id),
2060                                })),
2061                                cx.handler_for(&pane, move |pane, cx| {
2062                                    pane.project.update(cx, |_, cx| {
2063                                        cx.emit(project::Event::RevealInProjectPanel(
2064                                            ProjectEntryId::from_proto(entry_id),
2065                                        ))
2066                                    });
2067                                }),
2068                            )
2069                            .when_some(parent_abs_path, |menu, parent_abs_path| {
2070                                menu.entry(
2071                                    "Open in Terminal",
2072                                    Some(Box::new(OpenInTerminal)),
2073                                    cx.handler_for(&pane, move |_, cx| {
2074                                        cx.dispatch_action(
2075                                            OpenTerminal {
2076                                                working_directory: parent_abs_path.clone(),
2077                                            }
2078                                            .boxed_clone(),
2079                                        );
2080                                    }),
2081                                )
2082                            });
2083                    } else {
2084                        menu = menu.map(pin_tab_entries);
2085                    }
2086                }
2087
2088                menu
2089            })
2090        })
2091    }
2092
2093    fn render_tab_bar(&mut self, cx: &mut ViewContext<'_, Pane>) -> impl IntoElement {
2094        let focus_handle = self.focus_handle.clone();
2095        let navigate_backward = IconButton::new("navigate_backward", IconName::ArrowLeft)
2096            .shape(IconButtonShape::Square)
2097            .icon_size(IconSize::Small)
2098            .on_click({
2099                let view = cx.view().clone();
2100                move |_, cx| view.update(cx, Self::navigate_backward)
2101            })
2102            .disabled(!self.can_navigate_backward())
2103            .tooltip({
2104                let focus_handle = focus_handle.clone();
2105                move |cx| Tooltip::for_action_in("Go Back", &GoBack, &focus_handle, cx)
2106            });
2107
2108        let navigate_forward = IconButton::new("navigate_forward", IconName::ArrowRight)
2109            .shape(IconButtonShape::Square)
2110            .icon_size(IconSize::Small)
2111            .on_click({
2112                let view = cx.view().clone();
2113                move |_, cx| view.update(cx, Self::navigate_forward)
2114            })
2115            .disabled(!self.can_navigate_forward())
2116            .tooltip({
2117                let focus_handle = focus_handle.clone();
2118                move |cx| Tooltip::for_action_in("Go Forward", &GoForward, &focus_handle, cx)
2119            });
2120
2121        let mut tab_items = self
2122            .items
2123            .iter()
2124            .enumerate()
2125            .zip(tab_details(&self.items, cx))
2126            .map(|((ix, item), detail)| self.render_tab(ix, &**item, detail, cx))
2127            .collect::<Vec<_>>();
2128
2129        let unpinned_tabs = tab_items.split_off(self.pinned_tab_count);
2130        let pinned_tabs = tab_items;
2131        TabBar::new("tab_bar")
2132            .when(
2133                self.display_nav_history_buttons.unwrap_or_default(),
2134                |tab_bar| {
2135                    tab_bar
2136                        .start_child(navigate_backward)
2137                        .start_child(navigate_forward)
2138                },
2139            )
2140            .map(|tab_bar| {
2141                let render_tab_buttons = self.render_tab_bar_buttons.clone();
2142                let (left_children, right_children) = render_tab_buttons(self, cx);
2143
2144                tab_bar
2145                    .start_children(left_children)
2146                    .end_children(right_children)
2147            })
2148            .children(pinned_tabs.len().ne(&0).then(|| {
2149                h_flex()
2150                    .children(pinned_tabs)
2151                    .border_r_2()
2152                    .border_color(cx.theme().colors().border)
2153            }))
2154            .child(
2155                h_flex()
2156                    .id("unpinned tabs")
2157                    .overflow_x_scroll()
2158                    .w_full()
2159                    .track_scroll(&self.tab_bar_scroll_handle)
2160                    .children(unpinned_tabs)
2161                    .child(
2162                        div()
2163                            .id("tab_bar_drop_target")
2164                            .min_w_6()
2165                            // HACK: This empty child is currently necessary to force the drop target to appear
2166                            // despite us setting a min width above.
2167                            .child("")
2168                            .h_full()
2169                            .flex_grow()
2170                            .drag_over::<DraggedTab>(|bar, _, cx| {
2171                                bar.bg(cx.theme().colors().drop_target_background)
2172                            })
2173                            .drag_over::<DraggedSelection>(|bar, _, cx| {
2174                                bar.bg(cx.theme().colors().drop_target_background)
2175                            })
2176                            .on_drop(cx.listener(move |this, dragged_tab: &DraggedTab, cx| {
2177                                this.drag_split_direction = None;
2178                                this.handle_tab_drop(dragged_tab, this.items.len(), cx)
2179                            }))
2180                            .on_drop(cx.listener(move |this, selection: &DraggedSelection, cx| {
2181                                this.drag_split_direction = None;
2182                                this.handle_project_entry_drop(
2183                                    &selection.active_selection.entry_id,
2184                                    cx,
2185                                )
2186                            }))
2187                            .on_drop(cx.listener(move |this, paths, cx| {
2188                                this.drag_split_direction = None;
2189                                this.handle_external_paths_drop(paths, cx)
2190                            }))
2191                            .on_click(cx.listener(move |this, event: &ClickEvent, cx| {
2192                                if event.up.click_count == 2 {
2193                                    cx.dispatch_action(
2194                                        this.double_click_dispatch_action.boxed_clone(),
2195                                    )
2196                                }
2197                            })),
2198                    ),
2199            )
2200    }
2201
2202    pub fn render_menu_overlay(menu: &View<ContextMenu>) -> Div {
2203        div().absolute().bottom_0().right_0().size_0().child(
2204            deferred(
2205                anchored()
2206                    .anchor(AnchorCorner::TopRight)
2207                    .child(menu.clone()),
2208            )
2209            .with_priority(1),
2210        )
2211    }
2212
2213    pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
2214        self.zoomed = zoomed;
2215        cx.notify();
2216    }
2217
2218    pub fn is_zoomed(&self) -> bool {
2219        self.zoomed
2220    }
2221
2222    fn handle_drag_move<T>(&mut self, event: &DragMoveEvent<T>, cx: &mut ViewContext<Self>) {
2223        if !self.can_split {
2224            return;
2225        }
2226
2227        let rect = event.bounds.size;
2228
2229        let size = event.bounds.size.width.min(event.bounds.size.height)
2230            * WorkspaceSettings::get_global(cx).drop_target_size;
2231
2232        let relative_cursor = Point::new(
2233            event.event.position.x - event.bounds.left(),
2234            event.event.position.y - event.bounds.top(),
2235        );
2236
2237        let direction = if relative_cursor.x < size
2238            || relative_cursor.x > rect.width - size
2239            || relative_cursor.y < size
2240            || relative_cursor.y > rect.height - size
2241        {
2242            [
2243                SplitDirection::Up,
2244                SplitDirection::Right,
2245                SplitDirection::Down,
2246                SplitDirection::Left,
2247            ]
2248            .iter()
2249            .min_by_key(|side| match side {
2250                SplitDirection::Up => relative_cursor.y,
2251                SplitDirection::Right => rect.width - relative_cursor.x,
2252                SplitDirection::Down => rect.height - relative_cursor.y,
2253                SplitDirection::Left => relative_cursor.x,
2254            })
2255            .cloned()
2256        } else {
2257            None
2258        };
2259
2260        if direction != self.drag_split_direction {
2261            self.drag_split_direction = direction;
2262        }
2263    }
2264
2265    fn handle_tab_drop(
2266        &mut self,
2267        dragged_tab: &DraggedTab,
2268        ix: usize,
2269        cx: &mut ViewContext<'_, Self>,
2270    ) {
2271        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2272            if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_tab, cx) {
2273                return;
2274            }
2275        }
2276        let mut to_pane = cx.view().clone();
2277        let split_direction = self.drag_split_direction;
2278        let item_id = dragged_tab.item.item_id();
2279        if let Some(preview_item_id) = self.preview_item_id {
2280            if item_id == preview_item_id {
2281                self.set_preview_item_id(None, cx);
2282            }
2283        }
2284
2285        let from_pane = dragged_tab.pane.clone();
2286        self.workspace
2287            .update(cx, |_, cx| {
2288                cx.defer(move |workspace, cx| {
2289                    if let Some(split_direction) = split_direction {
2290                        to_pane = workspace.split_pane(to_pane, split_direction, cx);
2291                    }
2292                    let old_ix = from_pane.read(cx).index_for_item_id(item_id);
2293                    if to_pane == from_pane {
2294                        if let Some(old_index) = old_ix {
2295                            to_pane.update(cx, |this, _| {
2296                                if old_index < this.pinned_tab_count
2297                                    && (ix == this.items.len() || ix > this.pinned_tab_count)
2298                                {
2299                                    this.pinned_tab_count -= 1;
2300                                } else if this.has_pinned_tabs()
2301                                    && old_index >= this.pinned_tab_count
2302                                    && ix < this.pinned_tab_count
2303                                {
2304                                    this.pinned_tab_count += 1;
2305                                }
2306                            });
2307                        }
2308                    } else {
2309                        to_pane.update(cx, |this, _| {
2310                            if this.has_pinned_tabs() && ix < this.pinned_tab_count {
2311                                this.pinned_tab_count += 1;
2312                            }
2313                        });
2314                        from_pane.update(cx, |this, _| {
2315                            if let Some(index) = old_ix {
2316                                if this.pinned_tab_count > index {
2317                                    this.pinned_tab_count -= 1;
2318                                }
2319                            }
2320                        })
2321                    }
2322                    workspace.move_item(from_pane.clone(), to_pane.clone(), item_id, ix, cx);
2323                });
2324            })
2325            .log_err();
2326    }
2327
2328    fn handle_dragged_selection_drop(
2329        &mut self,
2330        dragged_selection: &DraggedSelection,
2331        cx: &mut ViewContext<'_, Self>,
2332    ) {
2333        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2334            if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_selection, cx) {
2335                return;
2336            }
2337        }
2338        self.handle_project_entry_drop(&dragged_selection.active_selection.entry_id, cx);
2339    }
2340
2341    fn handle_project_entry_drop(
2342        &mut self,
2343        project_entry_id: &ProjectEntryId,
2344        cx: &mut ViewContext<'_, Self>,
2345    ) {
2346        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2347            if let ControlFlow::Break(()) = custom_drop_handle(self, project_entry_id, cx) {
2348                return;
2349            }
2350        }
2351        let mut to_pane = cx.view().clone();
2352        let split_direction = self.drag_split_direction;
2353        let project_entry_id = *project_entry_id;
2354        self.workspace
2355            .update(cx, |_, cx| {
2356                cx.defer(move |workspace, cx| {
2357                    if let Some(path) = workspace
2358                        .project()
2359                        .read(cx)
2360                        .path_for_entry(project_entry_id, cx)
2361                    {
2362                        let load_path_task = workspace.load_path(path, cx);
2363                        cx.spawn(|workspace, mut cx| async move {
2364                            if let Some((project_entry_id, build_item)) =
2365                                load_path_task.await.notify_async_err(&mut cx)
2366                            {
2367                                let (to_pane, new_item_handle) = workspace
2368                                    .update(&mut cx, |workspace, cx| {
2369                                        if let Some(split_direction) = split_direction {
2370                                            to_pane =
2371                                                workspace.split_pane(to_pane, split_direction, cx);
2372                                        }
2373                                        let new_item_handle = to_pane.update(cx, |pane, cx| {
2374                                            pane.open_item(
2375                                                project_entry_id,
2376                                                true,
2377                                                false,
2378                                                cx,
2379                                                build_item,
2380                                            )
2381                                        });
2382                                        (to_pane, new_item_handle)
2383                                    })
2384                                    .log_err()?;
2385                                to_pane
2386                                    .update(&mut cx, |this, cx| {
2387                                        let Some(index) = this.index_for_item(&*new_item_handle)
2388                                        else {
2389                                            return;
2390                                        };
2391                                        if !this.is_tab_pinned(index) {
2392                                            this.pin_tab_at(index, cx);
2393                                        }
2394                                    })
2395                                    .ok()?
2396                            }
2397                            Some(())
2398                        })
2399                        .detach();
2400                    };
2401                });
2402            })
2403            .log_err();
2404    }
2405
2406    fn handle_external_paths_drop(
2407        &mut self,
2408        paths: &ExternalPaths,
2409        cx: &mut ViewContext<'_, Self>,
2410    ) {
2411        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2412            if let ControlFlow::Break(()) = custom_drop_handle(self, paths, cx) {
2413                return;
2414            }
2415        }
2416        let mut to_pane = cx.view().clone();
2417        let mut split_direction = self.drag_split_direction;
2418        let paths = paths.paths().to_vec();
2419        let is_remote = self
2420            .workspace
2421            .update(cx, |workspace, cx| {
2422                if workspace.project().read(cx).is_via_collab() {
2423                    workspace.show_error(
2424                        &anyhow::anyhow!("Cannot drop files on a remote project"),
2425                        cx,
2426                    );
2427                    true
2428                } else {
2429                    false
2430                }
2431            })
2432            .unwrap_or(true);
2433        if is_remote {
2434            return;
2435        }
2436
2437        self.workspace
2438            .update(cx, |workspace, cx| {
2439                let fs = Arc::clone(workspace.project().read(cx).fs());
2440                cx.spawn(|workspace, mut cx| async move {
2441                    let mut is_file_checks = FuturesUnordered::new();
2442                    for path in &paths {
2443                        is_file_checks.push(fs.is_file(path))
2444                    }
2445                    let mut has_files_to_open = false;
2446                    while let Some(is_file) = is_file_checks.next().await {
2447                        if is_file {
2448                            has_files_to_open = true;
2449                            break;
2450                        }
2451                    }
2452                    drop(is_file_checks);
2453                    if !has_files_to_open {
2454                        split_direction = None;
2455                    }
2456
2457                    if let Some(open_task) = workspace
2458                        .update(&mut cx, |workspace, cx| {
2459                            if let Some(split_direction) = split_direction {
2460                                to_pane = workspace.split_pane(to_pane, split_direction, cx);
2461                            }
2462                            workspace.open_paths(
2463                                paths,
2464                                OpenVisible::OnlyDirectories,
2465                                Some(to_pane.downgrade()),
2466                                cx,
2467                            )
2468                        })
2469                        .ok()
2470                    {
2471                        let opened_items: Vec<_> = open_task.await;
2472                        _ = workspace.update(&mut cx, |workspace, cx| {
2473                            for item in opened_items.into_iter().flatten() {
2474                                if let Err(e) = item {
2475                                    workspace.show_error(&e, cx);
2476                                }
2477                            }
2478                        });
2479                    }
2480                })
2481                .detach();
2482            })
2483            .log_err();
2484    }
2485
2486    pub fn display_nav_history_buttons(&mut self, display: Option<bool>) {
2487        self.display_nav_history_buttons = display;
2488    }
2489}
2490
2491impl FocusableView for Pane {
2492    fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
2493        self.focus_handle.clone()
2494    }
2495}
2496
2497impl Render for Pane {
2498    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2499        let mut key_context = KeyContext::new_with_defaults();
2500        key_context.add("Pane");
2501        if self.active_item().is_none() {
2502            key_context.add("EmptyPane");
2503        }
2504
2505        let should_display_tab_bar = self.should_display_tab_bar.clone();
2506        let display_tab_bar = should_display_tab_bar(cx);
2507
2508        v_flex()
2509            .key_context(key_context)
2510            .track_focus(&self.focus_handle)
2511            .size_full()
2512            .flex_none()
2513            .overflow_hidden()
2514            .on_action(cx.listener(|pane, _: &AlternateFile, cx| {
2515                pane.alternate_file(cx);
2516            }))
2517            .on_action(cx.listener(|pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx)))
2518            .on_action(cx.listener(|pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx)))
2519            .on_action(cx.listener(|pane, _: &SplitHorizontal, cx| {
2520                pane.split(SplitDirection::horizontal(cx), cx)
2521            }))
2522            .on_action(cx.listener(|pane, _: &SplitVertical, cx| {
2523                pane.split(SplitDirection::vertical(cx), cx)
2524            }))
2525            .on_action(
2526                cx.listener(|pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx)),
2527            )
2528            .on_action(cx.listener(|pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx)))
2529            .on_action(cx.listener(|pane, _: &GoBack, cx| pane.navigate_backward(cx)))
2530            .on_action(cx.listener(|pane, _: &GoForward, cx| pane.navigate_forward(cx)))
2531            .on_action(cx.listener(|pane, _: &JoinIntoNext, cx| pane.join_into_next(cx)))
2532            .on_action(cx.listener(Pane::toggle_zoom))
2533            .on_action(cx.listener(|pane: &mut Pane, action: &ActivateItem, cx| {
2534                pane.activate_item(action.0, true, true, cx);
2535            }))
2536            .on_action(cx.listener(|pane: &mut Pane, _: &ActivateLastItem, cx| {
2537                pane.activate_item(pane.items.len() - 1, true, true, cx);
2538            }))
2539            .on_action(cx.listener(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
2540                pane.activate_prev_item(true, cx);
2541            }))
2542            .on_action(cx.listener(|pane: &mut Pane, _: &ActivateNextItem, cx| {
2543                pane.activate_next_item(true, cx);
2544            }))
2545            .on_action(cx.listener(|pane, action, cx| {
2546                pane.toggle_pin_tab(action, cx);
2547            }))
2548            .when(PreviewTabsSettings::get_global(cx).enabled, |this| {
2549                this.on_action(cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, cx| {
2550                    if let Some(active_item_id) = pane.active_item().map(|i| i.item_id()) {
2551                        if pane.is_active_preview_item(active_item_id) {
2552                            pane.set_preview_item_id(None, cx);
2553                        } else {
2554                            pane.set_preview_item_id(Some(active_item_id), cx);
2555                        }
2556                    }
2557                }))
2558            })
2559            .on_action(
2560                cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
2561                    if let Some(task) = pane.close_active_item(action, cx) {
2562                        task.detach_and_log_err(cx)
2563                    }
2564                }),
2565            )
2566            .on_action(
2567                cx.listener(|pane: &mut Self, action: &CloseInactiveItems, cx| {
2568                    if let Some(task) = pane.close_inactive_items(action, cx) {
2569                        task.detach_and_log_err(cx)
2570                    }
2571                }),
2572            )
2573            .on_action(
2574                cx.listener(|pane: &mut Self, action: &CloseCleanItems, cx| {
2575                    if let Some(task) = pane.close_clean_items(action, cx) {
2576                        task.detach_and_log_err(cx)
2577                    }
2578                }),
2579            )
2580            .on_action(
2581                cx.listener(|pane: &mut Self, action: &CloseItemsToTheLeft, cx| {
2582                    if let Some(task) = pane.close_items_to_the_left(action, cx) {
2583                        task.detach_and_log_err(cx)
2584                    }
2585                }),
2586            )
2587            .on_action(
2588                cx.listener(|pane: &mut Self, action: &CloseItemsToTheRight, cx| {
2589                    if let Some(task) = pane.close_items_to_the_right(action, cx) {
2590                        task.detach_and_log_err(cx)
2591                    }
2592                }),
2593            )
2594            .on_action(cx.listener(|pane: &mut Self, action: &CloseAllItems, cx| {
2595                if let Some(task) = pane.close_all_items(action, cx) {
2596                    task.detach_and_log_err(cx)
2597                }
2598            }))
2599            .on_action(
2600                cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
2601                    if let Some(task) = pane.close_active_item(action, cx) {
2602                        task.detach_and_log_err(cx)
2603                    }
2604                }),
2605            )
2606            .on_action(
2607                cx.listener(|pane: &mut Self, action: &RevealInProjectPanel, cx| {
2608                    let entry_id = action
2609                        .entry_id
2610                        .map(ProjectEntryId::from_proto)
2611                        .or_else(|| pane.active_item()?.project_entry_ids(cx).first().copied());
2612                    if let Some(entry_id) = entry_id {
2613                        pane.project.update(cx, |_, cx| {
2614                            cx.emit(project::Event::RevealInProjectPanel(entry_id))
2615                        });
2616                    }
2617                }),
2618            )
2619            .when(self.active_item().is_some() && display_tab_bar, |pane| {
2620                pane.child(self.render_tab_bar(cx))
2621            })
2622            .child({
2623                let has_worktrees = self.project.read(cx).worktrees(cx).next().is_some();
2624                // main content
2625                div()
2626                    .flex_1()
2627                    .relative()
2628                    .group("")
2629                    .on_drag_move::<DraggedTab>(cx.listener(Self::handle_drag_move))
2630                    .on_drag_move::<DraggedSelection>(cx.listener(Self::handle_drag_move))
2631                    .on_drag_move::<ExternalPaths>(cx.listener(Self::handle_drag_move))
2632                    .map(|div| {
2633                        if let Some(item) = self.active_item() {
2634                            div.v_flex()
2635                                .child(self.toolbar.clone())
2636                                .child(item.to_any())
2637                        } else {
2638                            let placeholder = div.h_flex().size_full().justify_center();
2639                            if has_worktrees {
2640                                placeholder
2641                            } else {
2642                                placeholder.child(
2643                                    Label::new("Open a file or project to get started.")
2644                                        .color(Color::Muted),
2645                                )
2646                            }
2647                        }
2648                    })
2649                    .child(
2650                        // drag target
2651                        div()
2652                            .invisible()
2653                            .absolute()
2654                            .bg(cx.theme().colors().drop_target_background)
2655                            .group_drag_over::<DraggedTab>("", |style| style.visible())
2656                            .group_drag_over::<DraggedSelection>("", |style| style.visible())
2657                            .group_drag_over::<ExternalPaths>("", |style| style.visible())
2658                            .when_some(self.can_drop_predicate.clone(), |this, p| {
2659                                this.can_drop(move |a, cx| p(a, cx))
2660                            })
2661                            .on_drop(cx.listener(move |this, dragged_tab, cx| {
2662                                this.handle_tab_drop(dragged_tab, this.active_item_index(), cx)
2663                            }))
2664                            .on_drop(cx.listener(move |this, selection: &DraggedSelection, cx| {
2665                                this.handle_dragged_selection_drop(selection, cx)
2666                            }))
2667                            .on_drop(cx.listener(move |this, paths, cx| {
2668                                this.handle_external_paths_drop(paths, cx)
2669                            }))
2670                            .map(|div| {
2671                                let size = DefiniteLength::Fraction(0.5);
2672                                match self.drag_split_direction {
2673                                    None => div.top_0().right_0().bottom_0().left_0(),
2674                                    Some(SplitDirection::Up) => {
2675                                        div.top_0().left_0().right_0().h(size)
2676                                    }
2677                                    Some(SplitDirection::Down) => {
2678                                        div.left_0().bottom_0().right_0().h(size)
2679                                    }
2680                                    Some(SplitDirection::Left) => {
2681                                        div.top_0().left_0().bottom_0().w(size)
2682                                    }
2683                                    Some(SplitDirection::Right) => {
2684                                        div.top_0().bottom_0().right_0().w(size)
2685                                    }
2686                                }
2687                            }),
2688                    )
2689            })
2690            .on_mouse_down(
2691                MouseButton::Navigate(NavigationDirection::Back),
2692                cx.listener(|pane, _, cx| {
2693                    if let Some(workspace) = pane.workspace.upgrade() {
2694                        let pane = cx.view().downgrade();
2695                        cx.window_context().defer(move |cx| {
2696                            workspace.update(cx, |workspace, cx| {
2697                                workspace.go_back(pane, cx).detach_and_log_err(cx)
2698                            })
2699                        })
2700                    }
2701                }),
2702            )
2703            .on_mouse_down(
2704                MouseButton::Navigate(NavigationDirection::Forward),
2705                cx.listener(|pane, _, cx| {
2706                    if let Some(workspace) = pane.workspace.upgrade() {
2707                        let pane = cx.view().downgrade();
2708                        cx.window_context().defer(move |cx| {
2709                            workspace.update(cx, |workspace, cx| {
2710                                workspace.go_forward(pane, cx).detach_and_log_err(cx)
2711                            })
2712                        })
2713                    }
2714                }),
2715            )
2716    }
2717}
2718
2719impl ItemNavHistory {
2720    pub fn push<D: 'static + Send + Any>(&mut self, data: Option<D>, cx: &mut WindowContext) {
2721        self.history
2722            .push(data, self.item.clone(), self.is_preview, cx);
2723    }
2724
2725    pub fn pop_backward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
2726        self.history.pop(NavigationMode::GoingBack, cx)
2727    }
2728
2729    pub fn pop_forward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
2730        self.history.pop(NavigationMode::GoingForward, cx)
2731    }
2732}
2733
2734impl NavHistory {
2735    pub fn for_each_entry(
2736        &self,
2737        cx: &AppContext,
2738        mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
2739    ) {
2740        let borrowed_history = self.0.lock();
2741        borrowed_history
2742            .forward_stack
2743            .iter()
2744            .chain(borrowed_history.backward_stack.iter())
2745            .chain(borrowed_history.closed_stack.iter())
2746            .for_each(|entry| {
2747                if let Some(project_and_abs_path) =
2748                    borrowed_history.paths_by_item.get(&entry.item.id())
2749                {
2750                    f(entry, project_and_abs_path.clone());
2751                } else if let Some(item) = entry.item.upgrade() {
2752                    if let Some(path) = item.project_path(cx) {
2753                        f(entry, (path, None));
2754                    }
2755                }
2756            })
2757    }
2758
2759    pub fn set_mode(&mut self, mode: NavigationMode) {
2760        self.0.lock().mode = mode;
2761    }
2762
2763    pub fn mode(&self) -> NavigationMode {
2764        self.0.lock().mode
2765    }
2766
2767    pub fn disable(&mut self) {
2768        self.0.lock().mode = NavigationMode::Disabled;
2769    }
2770
2771    pub fn enable(&mut self) {
2772        self.0.lock().mode = NavigationMode::Normal;
2773    }
2774
2775    pub fn pop(&mut self, mode: NavigationMode, cx: &mut WindowContext) -> Option<NavigationEntry> {
2776        let mut state = self.0.lock();
2777        let entry = match mode {
2778            NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
2779                return None
2780            }
2781            NavigationMode::GoingBack => &mut state.backward_stack,
2782            NavigationMode::GoingForward => &mut state.forward_stack,
2783            NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
2784        }
2785        .pop_back();
2786        if entry.is_some() {
2787            state.did_update(cx);
2788        }
2789        entry
2790    }
2791
2792    pub fn push<D: 'static + Send + Any>(
2793        &mut self,
2794        data: Option<D>,
2795        item: Arc<dyn WeakItemHandle>,
2796        is_preview: bool,
2797        cx: &mut WindowContext,
2798    ) {
2799        let state = &mut *self.0.lock();
2800        match state.mode {
2801            NavigationMode::Disabled => {}
2802            NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
2803                if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2804                    state.backward_stack.pop_front();
2805                }
2806                state.backward_stack.push_back(NavigationEntry {
2807                    item,
2808                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2809                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2810                    is_preview,
2811                });
2812                state.forward_stack.clear();
2813            }
2814            NavigationMode::GoingBack => {
2815                if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2816                    state.forward_stack.pop_front();
2817                }
2818                state.forward_stack.push_back(NavigationEntry {
2819                    item,
2820                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2821                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2822                    is_preview,
2823                });
2824            }
2825            NavigationMode::GoingForward => {
2826                if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2827                    state.backward_stack.pop_front();
2828                }
2829                state.backward_stack.push_back(NavigationEntry {
2830                    item,
2831                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2832                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2833                    is_preview,
2834                });
2835            }
2836            NavigationMode::ClosingItem => {
2837                if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2838                    state.closed_stack.pop_front();
2839                }
2840                state.closed_stack.push_back(NavigationEntry {
2841                    item,
2842                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2843                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2844                    is_preview,
2845                });
2846            }
2847        }
2848        state.did_update(cx);
2849    }
2850
2851    pub fn remove_item(&mut self, item_id: EntityId) {
2852        let mut state = self.0.lock();
2853        state.paths_by_item.remove(&item_id);
2854        state
2855            .backward_stack
2856            .retain(|entry| entry.item.id() != item_id);
2857        state
2858            .forward_stack
2859            .retain(|entry| entry.item.id() != item_id);
2860        state
2861            .closed_stack
2862            .retain(|entry| entry.item.id() != item_id);
2863    }
2864
2865    pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option<PathBuf>)> {
2866        self.0.lock().paths_by_item.get(&item_id).cloned()
2867    }
2868}
2869
2870impl NavHistoryState {
2871    pub fn did_update(&self, cx: &mut WindowContext) {
2872        if let Some(pane) = self.pane.upgrade() {
2873            cx.defer(move |cx| {
2874                pane.update(cx, |pane, cx| pane.history_updated(cx));
2875            });
2876        }
2877    }
2878}
2879
2880fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
2881    let path = buffer_path
2882        .as_ref()
2883        .and_then(|p| {
2884            p.path
2885                .to_str()
2886                .and_then(|s| if s == "" { None } else { Some(s) })
2887        })
2888        .unwrap_or("This buffer");
2889    let path = truncate_and_remove_front(path, 80);
2890    format!("{path} contains unsaved edits. Do you want to save it?")
2891}
2892
2893pub fn tab_details(items: &Vec<Box<dyn ItemHandle>>, cx: &AppContext) -> Vec<usize> {
2894    let mut tab_details = items.iter().map(|_| 0).collect::<Vec<_>>();
2895    let mut tab_descriptions = HashMap::default();
2896    let mut done = false;
2897    while !done {
2898        done = true;
2899
2900        // Store item indices by their tab description.
2901        for (ix, (item, detail)) in items.iter().zip(&tab_details).enumerate() {
2902            if let Some(description) = item.tab_description(*detail, cx) {
2903                if *detail == 0
2904                    || Some(&description) != item.tab_description(detail - 1, cx).as_ref()
2905                {
2906                    tab_descriptions
2907                        .entry(description)
2908                        .or_insert(Vec::new())
2909                        .push(ix);
2910                }
2911            }
2912        }
2913
2914        // If two or more items have the same tab description, increase their level
2915        // of detail and try again.
2916        for (_, item_ixs) in tab_descriptions.drain() {
2917            if item_ixs.len() > 1 {
2918                done = false;
2919                for ix in item_ixs {
2920                    tab_details[ix] += 1;
2921                }
2922            }
2923        }
2924    }
2925
2926    tab_details
2927}
2928
2929pub fn render_item_indicator(item: Box<dyn ItemHandle>, cx: &WindowContext) -> Option<Indicator> {
2930    maybe!({
2931        let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
2932            (true, _) => Color::Warning,
2933            (_, true) => Color::Accent,
2934            (false, false) => return None,
2935        };
2936
2937        Some(Indicator::dot().color(indicator_color))
2938    })
2939}
2940
2941#[cfg(test)]
2942mod tests {
2943    use super::*;
2944    use crate::item::test::{TestItem, TestProjectItem};
2945    use gpui::{TestAppContext, VisualTestContext};
2946    use project::FakeFs;
2947    use settings::SettingsStore;
2948    use theme::LoadThemes;
2949
2950    #[gpui::test]
2951    async fn test_remove_active_empty(cx: &mut TestAppContext) {
2952        init_test(cx);
2953        let fs = FakeFs::new(cx.executor());
2954
2955        let project = Project::test(fs, None, cx).await;
2956        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2957        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2958
2959        pane.update(cx, |pane, cx| {
2960            assert!(pane
2961                .close_active_item(&CloseActiveItem { save_intent: None }, cx)
2962                .is_none())
2963        });
2964    }
2965
2966    #[gpui::test]
2967    async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
2968        init_test(cx);
2969        let fs = FakeFs::new(cx.executor());
2970
2971        let project = Project::test(fs, None, cx).await;
2972        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2973        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2974
2975        // 1. Add with a destination index
2976        //   a. Add before the active item
2977        set_labeled_items(&pane, ["A", "B*", "C"], cx);
2978        pane.update(cx, |pane, cx| {
2979            pane.add_item(
2980                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2981                false,
2982                false,
2983                Some(0),
2984                cx,
2985            );
2986        });
2987        assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
2988
2989        //   b. Add after the active item
2990        set_labeled_items(&pane, ["A", "B*", "C"], cx);
2991        pane.update(cx, |pane, cx| {
2992            pane.add_item(
2993                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2994                false,
2995                false,
2996                Some(2),
2997                cx,
2998            );
2999        });
3000        assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3001
3002        //   c. Add at the end of the item list (including off the length)
3003        set_labeled_items(&pane, ["A", "B*", "C"], cx);
3004        pane.update(cx, |pane, cx| {
3005            pane.add_item(
3006                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3007                false,
3008                false,
3009                Some(5),
3010                cx,
3011            );
3012        });
3013        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3014
3015        // 2. Add without a destination index
3016        //   a. Add with active item at the start of the item list
3017        set_labeled_items(&pane, ["A*", "B", "C"], cx);
3018        pane.update(cx, |pane, cx| {
3019            pane.add_item(
3020                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3021                false,
3022                false,
3023                None,
3024                cx,
3025            );
3026        });
3027        set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
3028
3029        //   b. Add with active item at the end of the item list
3030        set_labeled_items(&pane, ["A", "B", "C*"], cx);
3031        pane.update(cx, |pane, cx| {
3032            pane.add_item(
3033                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3034                false,
3035                false,
3036                None,
3037                cx,
3038            );
3039        });
3040        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3041    }
3042
3043    #[gpui::test]
3044    async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
3045        init_test(cx);
3046        let fs = FakeFs::new(cx.executor());
3047
3048        let project = Project::test(fs, None, cx).await;
3049        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3050        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3051
3052        // 1. Add with a destination index
3053        //   1a. Add before the active item
3054        let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3055        pane.update(cx, |pane, cx| {
3056            pane.add_item(d, false, false, Some(0), cx);
3057        });
3058        assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
3059
3060        //   1b. Add after the active item
3061        let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3062        pane.update(cx, |pane, cx| {
3063            pane.add_item(d, false, false, Some(2), cx);
3064        });
3065        assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3066
3067        //   1c. Add at the end of the item list (including off the length)
3068        let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3069        pane.update(cx, |pane, cx| {
3070            pane.add_item(a, false, false, Some(5), cx);
3071        });
3072        assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3073
3074        //   1d. Add same item to active index
3075        let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3076        pane.update(cx, |pane, cx| {
3077            pane.add_item(b, false, false, Some(1), cx);
3078        });
3079        assert_item_labels(&pane, ["A", "B*", "C"], cx);
3080
3081        //   1e. Add item to index after same item in last position
3082        let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3083        pane.update(cx, |pane, cx| {
3084            pane.add_item(c, false, false, Some(2), cx);
3085        });
3086        assert_item_labels(&pane, ["A", "B", "C*"], cx);
3087
3088        // 2. Add without a destination index
3089        //   2a. Add with active item at the start of the item list
3090        let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
3091        pane.update(cx, |pane, cx| {
3092            pane.add_item(d, false, false, None, cx);
3093        });
3094        assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
3095
3096        //   2b. Add with active item at the end of the item list
3097        let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
3098        pane.update(cx, |pane, cx| {
3099            pane.add_item(a, false, false, None, cx);
3100        });
3101        assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3102
3103        //   2c. Add active item to active item at end of list
3104        let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
3105        pane.update(cx, |pane, cx| {
3106            pane.add_item(c, false, false, None, cx);
3107        });
3108        assert_item_labels(&pane, ["A", "B", "C*"], cx);
3109
3110        //   2d. Add active item to active item at start of list
3111        let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
3112        pane.update(cx, |pane, cx| {
3113            pane.add_item(a, false, false, None, cx);
3114        });
3115        assert_item_labels(&pane, ["A*", "B", "C"], cx);
3116    }
3117
3118    #[gpui::test]
3119    async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
3120        init_test(cx);
3121        let fs = FakeFs::new(cx.executor());
3122
3123        let project = Project::test(fs, None, cx).await;
3124        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3125        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3126
3127        // singleton view
3128        pane.update(cx, |pane, cx| {
3129            pane.add_item(
3130                Box::new(cx.new_view(|cx| {
3131                    TestItem::new(cx)
3132                        .with_singleton(true)
3133                        .with_label("buffer 1")
3134                        .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
3135                })),
3136                false,
3137                false,
3138                None,
3139                cx,
3140            );
3141        });
3142        assert_item_labels(&pane, ["buffer 1*"], cx);
3143
3144        // new singleton view with the same project entry
3145        pane.update(cx, |pane, cx| {
3146            pane.add_item(
3147                Box::new(cx.new_view(|cx| {
3148                    TestItem::new(cx)
3149                        .with_singleton(true)
3150                        .with_label("buffer 1")
3151                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3152                })),
3153                false,
3154                false,
3155                None,
3156                cx,
3157            );
3158        });
3159        assert_item_labels(&pane, ["buffer 1*"], cx);
3160
3161        // new singleton view with different project entry
3162        pane.update(cx, |pane, cx| {
3163            pane.add_item(
3164                Box::new(cx.new_view(|cx| {
3165                    TestItem::new(cx)
3166                        .with_singleton(true)
3167                        .with_label("buffer 2")
3168                        .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
3169                })),
3170                false,
3171                false,
3172                None,
3173                cx,
3174            );
3175        });
3176        assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
3177
3178        // new multibuffer view with the same project entry
3179        pane.update(cx, |pane, cx| {
3180            pane.add_item(
3181                Box::new(cx.new_view(|cx| {
3182                    TestItem::new(cx)
3183                        .with_singleton(false)
3184                        .with_label("multibuffer 1")
3185                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3186                })),
3187                false,
3188                false,
3189                None,
3190                cx,
3191            );
3192        });
3193        assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
3194
3195        // another multibuffer view with the same project entry
3196        pane.update(cx, |pane, cx| {
3197            pane.add_item(
3198                Box::new(cx.new_view(|cx| {
3199                    TestItem::new(cx)
3200                        .with_singleton(false)
3201                        .with_label("multibuffer 1b")
3202                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3203                })),
3204                false,
3205                false,
3206                None,
3207                cx,
3208            );
3209        });
3210        assert_item_labels(
3211            &pane,
3212            ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
3213            cx,
3214        );
3215    }
3216
3217    #[gpui::test]
3218    async fn test_remove_item_ordering(cx: &mut TestAppContext) {
3219        init_test(cx);
3220        let fs = FakeFs::new(cx.executor());
3221
3222        let project = Project::test(fs, None, cx).await;
3223        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3224        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3225
3226        add_labeled_item(&pane, "A", false, cx);
3227        add_labeled_item(&pane, "B", false, cx);
3228        add_labeled_item(&pane, "C", false, cx);
3229        add_labeled_item(&pane, "D", false, cx);
3230        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3231
3232        pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
3233        add_labeled_item(&pane, "1", false, cx);
3234        assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
3235
3236        pane.update(cx, |pane, cx| {
3237            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3238        })
3239        .unwrap()
3240        .await
3241        .unwrap();
3242        assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
3243
3244        pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
3245        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3246
3247        pane.update(cx, |pane, cx| {
3248            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3249        })
3250        .unwrap()
3251        .await
3252        .unwrap();
3253        assert_item_labels(&pane, ["A", "B*", "C"], cx);
3254
3255        pane.update(cx, |pane, cx| {
3256            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3257        })
3258        .unwrap()
3259        .await
3260        .unwrap();
3261        assert_item_labels(&pane, ["A", "C*"], cx);
3262
3263        pane.update(cx, |pane, cx| {
3264            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3265        })
3266        .unwrap()
3267        .await
3268        .unwrap();
3269        assert_item_labels(&pane, ["A*"], cx);
3270    }
3271
3272    #[gpui::test]
3273    async fn test_close_inactive_items(cx: &mut TestAppContext) {
3274        init_test(cx);
3275        let fs = FakeFs::new(cx.executor());
3276
3277        let project = Project::test(fs, None, cx).await;
3278        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3279        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3280
3281        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
3282
3283        pane.update(cx, |pane, cx| {
3284            pane.close_inactive_items(&CloseInactiveItems { save_intent: None }, cx)
3285        })
3286        .unwrap()
3287        .await
3288        .unwrap();
3289        assert_item_labels(&pane, ["C*"], cx);
3290    }
3291
3292    #[gpui::test]
3293    async fn test_close_clean_items(cx: &mut TestAppContext) {
3294        init_test(cx);
3295        let fs = FakeFs::new(cx.executor());
3296
3297        let project = Project::test(fs, None, cx).await;
3298        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3299        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3300
3301        add_labeled_item(&pane, "A", true, cx);
3302        add_labeled_item(&pane, "B", false, cx);
3303        add_labeled_item(&pane, "C", true, cx);
3304        add_labeled_item(&pane, "D", false, cx);
3305        add_labeled_item(&pane, "E", false, cx);
3306        assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
3307
3308        pane.update(cx, |pane, cx| pane.close_clean_items(&CloseCleanItems, cx))
3309            .unwrap()
3310            .await
3311            .unwrap();
3312        assert_item_labels(&pane, ["A^", "C*^"], cx);
3313    }
3314
3315    #[gpui::test]
3316    async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
3317        init_test(cx);
3318        let fs = FakeFs::new(cx.executor());
3319
3320        let project = Project::test(fs, None, cx).await;
3321        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3322        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3323
3324        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
3325
3326        pane.update(cx, |pane, cx| {
3327            pane.close_items_to_the_left(&CloseItemsToTheLeft, cx)
3328        })
3329        .unwrap()
3330        .await
3331        .unwrap();
3332        assert_item_labels(&pane, ["C*", "D", "E"], cx);
3333    }
3334
3335    #[gpui::test]
3336    async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
3337        init_test(cx);
3338        let fs = FakeFs::new(cx.executor());
3339
3340        let project = Project::test(fs, None, cx).await;
3341        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3342        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3343
3344        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
3345
3346        pane.update(cx, |pane, cx| {
3347            pane.close_items_to_the_right(&CloseItemsToTheRight, cx)
3348        })
3349        .unwrap()
3350        .await
3351        .unwrap();
3352        assert_item_labels(&pane, ["A", "B", "C*"], cx);
3353    }
3354
3355    #[gpui::test]
3356    async fn test_close_all_items(cx: &mut TestAppContext) {
3357        init_test(cx);
3358        let fs = FakeFs::new(cx.executor());
3359
3360        let project = Project::test(fs, None, cx).await;
3361        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3362        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3363
3364        add_labeled_item(&pane, "A", false, cx);
3365        add_labeled_item(&pane, "B", false, cx);
3366        add_labeled_item(&pane, "C", false, cx);
3367        assert_item_labels(&pane, ["A", "B", "C*"], cx);
3368
3369        pane.update(cx, |pane, cx| {
3370            pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
3371        })
3372        .unwrap()
3373        .await
3374        .unwrap();
3375        assert_item_labels(&pane, [], cx);
3376
3377        add_labeled_item(&pane, "A", true, cx);
3378        add_labeled_item(&pane, "B", true, cx);
3379        add_labeled_item(&pane, "C", true, cx);
3380        assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
3381
3382        let save = pane
3383            .update(cx, |pane, cx| {
3384                pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
3385            })
3386            .unwrap();
3387
3388        cx.executor().run_until_parked();
3389        cx.simulate_prompt_answer(2);
3390        save.await.unwrap();
3391        assert_item_labels(&pane, [], cx);
3392    }
3393
3394    fn init_test(cx: &mut TestAppContext) {
3395        cx.update(|cx| {
3396            let settings_store = SettingsStore::test(cx);
3397            cx.set_global(settings_store);
3398            theme::init(LoadThemes::JustBase, cx);
3399            crate::init_settings(cx);
3400            Project::init_settings(cx);
3401        });
3402    }
3403
3404    fn add_labeled_item(
3405        pane: &View<Pane>,
3406        label: &str,
3407        is_dirty: bool,
3408        cx: &mut VisualTestContext,
3409    ) -> Box<View<TestItem>> {
3410        pane.update(cx, |pane, cx| {
3411            let labeled_item = Box::new(
3412                cx.new_view(|cx| TestItem::new(cx).with_label(label).with_dirty(is_dirty)),
3413            );
3414            pane.add_item(labeled_item.clone(), false, false, None, cx);
3415            labeled_item
3416        })
3417    }
3418
3419    fn set_labeled_items<const COUNT: usize>(
3420        pane: &View<Pane>,
3421        labels: [&str; COUNT],
3422        cx: &mut VisualTestContext,
3423    ) -> [Box<View<TestItem>>; COUNT] {
3424        pane.update(cx, |pane, cx| {
3425            pane.items.clear();
3426            let mut active_item_index = 0;
3427
3428            let mut index = 0;
3429            let items = labels.map(|mut label| {
3430                if label.ends_with('*') {
3431                    label = label.trim_end_matches('*');
3432                    active_item_index = index;
3433                }
3434
3435                let labeled_item = Box::new(cx.new_view(|cx| TestItem::new(cx).with_label(label)));
3436                pane.add_item(labeled_item.clone(), false, false, None, cx);
3437                index += 1;
3438                labeled_item
3439            });
3440
3441            pane.activate_item(active_item_index, false, false, cx);
3442
3443            items
3444        })
3445    }
3446
3447    // Assert the item label, with the active item label suffixed with a '*'
3448    fn assert_item_labels<const COUNT: usize>(
3449        pane: &View<Pane>,
3450        expected_states: [&str; COUNT],
3451        cx: &mut VisualTestContext,
3452    ) {
3453        pane.update(cx, |pane, cx| {
3454            let actual_states = pane
3455                .items
3456                .iter()
3457                .enumerate()
3458                .map(|(ix, item)| {
3459                    let mut state = item
3460                        .to_any()
3461                        .downcast::<TestItem>()
3462                        .unwrap()
3463                        .read(cx)
3464                        .label
3465                        .clone();
3466                    if ix == pane.active_item_index {
3467                        state.push('*');
3468                    }
3469                    if item.is_dirty(cx) {
3470                        state.push('^');
3471                    }
3472                    state
3473                })
3474                .collect::<Vec<_>>();
3475
3476            assert_eq!(
3477                actual_states, expected_states,
3478                "pane items do not match expectation"
3479            );
3480        })
3481    }
3482}
3483
3484impl Render for DraggedTab {
3485    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3486        let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
3487        let label = self.item.tab_content(
3488            TabContentParams {
3489                detail: Some(self.detail),
3490                selected: false,
3491                preview: false,
3492            },
3493            cx,
3494        );
3495        Tab::new("")
3496            .selected(self.is_active)
3497            .child(label)
3498            .render(cx)
3499            .font(ui_font)
3500    }
3501}