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            if !self.is_tab_pinned(index) {
1017                self.tab_bar_scroll_handle
1018                    .scroll_to_item(index - self.pinned_tab_count);
1019            }
1020
1021            cx.notify();
1022        }
1023    }
1024
1025    pub fn activate_prev_item(&mut self, activate_pane: bool, cx: &mut ViewContext<Self>) {
1026        let mut index = self.active_item_index;
1027        if index > 0 {
1028            index -= 1;
1029        } else if !self.items.is_empty() {
1030            index = self.items.len() - 1;
1031        }
1032        self.activate_item(index, activate_pane, activate_pane, cx);
1033    }
1034
1035    pub fn activate_next_item(&mut self, activate_pane: bool, cx: &mut ViewContext<Self>) {
1036        let mut index = self.active_item_index;
1037        if index + 1 < self.items.len() {
1038            index += 1;
1039        } else {
1040            index = 0;
1041        }
1042        self.activate_item(index, activate_pane, activate_pane, cx);
1043    }
1044
1045    pub fn close_active_item(
1046        &mut self,
1047        action: &CloseActiveItem,
1048        cx: &mut ViewContext<Self>,
1049    ) -> Option<Task<Result<()>>> {
1050        if self.items.is_empty() {
1051            // Close the window when there's no active items to close, if configured
1052            if WorkspaceSettings::get_global(cx)
1053                .when_closing_with_no_tabs
1054                .should_close()
1055            {
1056                cx.dispatch_action(Box::new(CloseWindow));
1057            }
1058
1059            return None;
1060        }
1061        let active_item_id = self.items[self.active_item_index].item_id();
1062        Some(self.close_item_by_id(
1063            active_item_id,
1064            action.save_intent.unwrap_or(SaveIntent::Close),
1065            cx,
1066        ))
1067    }
1068
1069    pub fn close_item_by_id(
1070        &mut self,
1071        item_id_to_close: EntityId,
1072        save_intent: SaveIntent,
1073        cx: &mut ViewContext<Self>,
1074    ) -> Task<Result<()>> {
1075        self.close_items(cx, save_intent, move |view_id| view_id == item_id_to_close)
1076    }
1077
1078    pub fn close_inactive_items(
1079        &mut self,
1080        action: &CloseInactiveItems,
1081        cx: &mut ViewContext<Self>,
1082    ) -> Option<Task<Result<()>>> {
1083        if self.items.is_empty() {
1084            return None;
1085        }
1086
1087        let active_item_id = self.items[self.active_item_index].item_id();
1088        Some(self.close_items(
1089            cx,
1090            action.save_intent.unwrap_or(SaveIntent::Close),
1091            move |item_id| item_id != active_item_id,
1092        ))
1093    }
1094
1095    pub fn close_clean_items(
1096        &mut self,
1097        _: &CloseCleanItems,
1098        cx: &mut ViewContext<Self>,
1099    ) -> Option<Task<Result<()>>> {
1100        let item_ids: Vec<_> = self
1101            .items()
1102            .filter(|item| !item.is_dirty(cx))
1103            .map(|item| item.item_id())
1104            .collect();
1105        Some(self.close_items(cx, SaveIntent::Close, move |item_id| {
1106            item_ids.contains(&item_id)
1107        }))
1108    }
1109
1110    pub fn close_items_to_the_left(
1111        &mut self,
1112        _: &CloseItemsToTheLeft,
1113        cx: &mut ViewContext<Self>,
1114    ) -> Option<Task<Result<()>>> {
1115        if self.items.is_empty() {
1116            return None;
1117        }
1118        let active_item_id = self.items[self.active_item_index].item_id();
1119        Some(self.close_items_to_the_left_by_id(active_item_id, cx))
1120    }
1121
1122    pub fn close_items_to_the_left_by_id(
1123        &mut self,
1124        item_id: EntityId,
1125        cx: &mut ViewContext<Self>,
1126    ) -> Task<Result<()>> {
1127        let item_ids: Vec<_> = self
1128            .items()
1129            .take_while(|item| item.item_id() != item_id)
1130            .map(|item| item.item_id())
1131            .collect();
1132        self.close_items(cx, SaveIntent::Close, move |item_id| {
1133            item_ids.contains(&item_id)
1134        })
1135    }
1136
1137    pub fn close_items_to_the_right(
1138        &mut self,
1139        _: &CloseItemsToTheRight,
1140        cx: &mut ViewContext<Self>,
1141    ) -> Option<Task<Result<()>>> {
1142        if self.items.is_empty() {
1143            return None;
1144        }
1145        let active_item_id = self.items[self.active_item_index].item_id();
1146        Some(self.close_items_to_the_right_by_id(active_item_id, cx))
1147    }
1148
1149    pub fn close_items_to_the_right_by_id(
1150        &mut self,
1151        item_id: EntityId,
1152        cx: &mut ViewContext<Self>,
1153    ) -> Task<Result<()>> {
1154        let item_ids: Vec<_> = self
1155            .items()
1156            .rev()
1157            .take_while(|item| item.item_id() != item_id)
1158            .map(|item| item.item_id())
1159            .collect();
1160        self.close_items(cx, SaveIntent::Close, move |item_id| {
1161            item_ids.contains(&item_id)
1162        })
1163    }
1164
1165    pub fn close_all_items(
1166        &mut self,
1167        action: &CloseAllItems,
1168        cx: &mut ViewContext<Self>,
1169    ) -> Option<Task<Result<()>>> {
1170        if self.items.is_empty() {
1171            return None;
1172        }
1173
1174        Some(
1175            self.close_items(cx, action.save_intent.unwrap_or(SaveIntent::Close), |_| {
1176                true
1177            }),
1178        )
1179    }
1180
1181    pub(super) fn file_names_for_prompt(
1182        items: &mut dyn Iterator<Item = &Box<dyn ItemHandle>>,
1183        all_dirty_items: usize,
1184        cx: &AppContext,
1185    ) -> (String, String) {
1186        /// Quantity of item paths displayed in prompt prior to cutoff..
1187        const FILE_NAMES_CUTOFF_POINT: usize = 10;
1188        let mut file_names: Vec<_> = items
1189            .filter_map(|item| {
1190                item.project_path(cx).and_then(|project_path| {
1191                    project_path
1192                        .path
1193                        .file_name()
1194                        .and_then(|name| name.to_str().map(ToOwned::to_owned))
1195                })
1196            })
1197            .take(FILE_NAMES_CUTOFF_POINT)
1198            .collect();
1199        let should_display_followup_text =
1200            all_dirty_items > FILE_NAMES_CUTOFF_POINT || file_names.len() != all_dirty_items;
1201        if should_display_followup_text {
1202            let not_shown_files = all_dirty_items - file_names.len();
1203            if not_shown_files == 1 {
1204                file_names.push(".. 1 file not shown".into());
1205            } else {
1206                file_names.push(format!(".. {} files not shown", not_shown_files));
1207            }
1208        }
1209        (
1210            format!(
1211                "Do you want to save changes to the following {} files?",
1212                all_dirty_items
1213            ),
1214            file_names.join("\n"),
1215        )
1216    }
1217
1218    pub fn close_items(
1219        &mut self,
1220        cx: &mut ViewContext<Pane>,
1221        mut save_intent: SaveIntent,
1222        should_close: impl Fn(EntityId) -> bool,
1223    ) -> Task<Result<()>> {
1224        // Find the items to close.
1225        let mut items_to_close = Vec::new();
1226        let mut dirty_items = Vec::new();
1227        for item in &self.items {
1228            if should_close(item.item_id()) {
1229                items_to_close.push(item.boxed_clone());
1230                if item.is_dirty(cx) {
1231                    dirty_items.push(item.boxed_clone());
1232                }
1233            }
1234        }
1235
1236        let active_item_id = self.active_item().map(|item| item.item_id());
1237
1238        items_to_close.sort_by_key(|item| {
1239            // Put the currently active item at the end, because if the currently active item is not closed last
1240            // closing the currently active item will cause the focus to switch to another item
1241            // This will cause Zed to expand the content of the currently active item
1242            active_item_id.filter(|&id| id == item.item_id()).is_some()
1243              // If a buffer is open both in a singleton editor and in a multibuffer, make sure
1244              // to focus the singleton buffer when prompting to save that buffer, as opposed
1245              // to focusing the multibuffer, because this gives the user a more clear idea
1246              // of what content they would be saving.
1247              || !item.is_singleton(cx)
1248        });
1249
1250        let workspace = self.workspace.clone();
1251        cx.spawn(|pane, mut cx| async move {
1252            if save_intent == SaveIntent::Close && dirty_items.len() > 1 {
1253                let answer = pane.update(&mut cx, |_, cx| {
1254                    let (prompt, detail) =
1255                        Self::file_names_for_prompt(&mut dirty_items.iter(), dirty_items.len(), cx);
1256                    cx.prompt(
1257                        PromptLevel::Warning,
1258                        &prompt,
1259                        Some(&detail),
1260                        &["Save all", "Discard all", "Cancel"],
1261                    )
1262                })?;
1263                match answer.await {
1264                    Ok(0) => save_intent = SaveIntent::SaveAll,
1265                    Ok(1) => save_intent = SaveIntent::Skip,
1266                    _ => {}
1267                }
1268            }
1269            let mut saved_project_items_ids = HashSet::default();
1270            for item in items_to_close.clone() {
1271                // Find the item's current index and its set of project item models. Avoid
1272                // storing these in advance, in case they have changed since this task
1273                // was started.
1274                let (item_ix, mut project_item_ids) = pane.update(&mut cx, |pane, cx| {
1275                    (pane.index_for_item(&*item), item.project_item_model_ids(cx))
1276                })?;
1277                let item_ix = if let Some(ix) = item_ix {
1278                    ix
1279                } else {
1280                    continue;
1281                };
1282
1283                // Check if this view has any project items that are not open anywhere else
1284                // in the workspace, AND that the user has not already been prompted to save.
1285                // If there are any such project entries, prompt the user to save this item.
1286                let project = workspace.update(&mut cx, |workspace, cx| {
1287                    for item in workspace.items(cx) {
1288                        if !items_to_close
1289                            .iter()
1290                            .any(|item_to_close| item_to_close.item_id() == item.item_id())
1291                        {
1292                            let other_project_item_ids = item.project_item_model_ids(cx);
1293                            project_item_ids.retain(|id| !other_project_item_ids.contains(id));
1294                        }
1295                    }
1296                    workspace.project().clone()
1297                })?;
1298                let should_save = project_item_ids
1299                    .iter()
1300                    .any(|id| saved_project_items_ids.insert(*id));
1301
1302                if should_save
1303                    && !Self::save_item(
1304                        project.clone(),
1305                        &pane,
1306                        item_ix,
1307                        &*item,
1308                        save_intent,
1309                        &mut cx,
1310                    )
1311                    .await?
1312                {
1313                    break;
1314                }
1315
1316                // Remove the item from the pane.
1317                pane.update(&mut cx, |pane, cx| {
1318                    if let Some(item_ix) = pane
1319                        .items
1320                        .iter()
1321                        .position(|i| i.item_id() == item.item_id())
1322                    {
1323                        pane.remove_item(item_ix, false, true, cx);
1324                    }
1325                })
1326                .ok();
1327            }
1328
1329            pane.update(&mut cx, |_, cx| cx.notify()).ok();
1330            Ok(())
1331        })
1332    }
1333
1334    pub fn remove_item(
1335        &mut self,
1336        item_index: usize,
1337        activate_pane: bool,
1338        close_pane_if_empty: bool,
1339        cx: &mut ViewContext<Self>,
1340    ) {
1341        self._remove_item(item_index, activate_pane, close_pane_if_empty, None, cx)
1342    }
1343
1344    pub fn remove_item_and_focus_on_pane(
1345        &mut self,
1346        item_index: usize,
1347        activate_pane: bool,
1348        focus_on_pane_if_closed: View<Pane>,
1349        cx: &mut ViewContext<Self>,
1350    ) {
1351        self._remove_item(
1352            item_index,
1353            activate_pane,
1354            true,
1355            Some(focus_on_pane_if_closed),
1356            cx,
1357        )
1358    }
1359
1360    fn _remove_item(
1361        &mut self,
1362        item_index: usize,
1363        activate_pane: bool,
1364        close_pane_if_empty: bool,
1365        focus_on_pane_if_closed: Option<View<Pane>>,
1366        cx: &mut ViewContext<Self>,
1367    ) {
1368        self.activation_history
1369            .retain(|entry| entry.entity_id != self.items[item_index].item_id());
1370
1371        if item_index == self.active_item_index {
1372            let index_to_activate = self
1373                .activation_history
1374                .pop()
1375                .and_then(|last_activated_item| {
1376                    self.items.iter().enumerate().find_map(|(index, item)| {
1377                        (item.item_id() == last_activated_item.entity_id).then_some(index)
1378                    })
1379                })
1380                // We didn't have a valid activation history entry, so fallback
1381                // to activating the item to the left
1382                .unwrap_or_else(|| item_index.min(self.items.len()).saturating_sub(1));
1383
1384            let should_activate = activate_pane || self.has_focus(cx);
1385            if self.items.len() == 1 && should_activate {
1386                self.focus_handle.focus(cx);
1387            } else {
1388                self.activate_item(index_to_activate, should_activate, should_activate, cx);
1389            }
1390        }
1391
1392        cx.emit(Event::RemoveItem { idx: item_index });
1393
1394        let item = self.items.remove(item_index);
1395
1396        cx.emit(Event::RemovedItem {
1397            item_id: item.item_id(),
1398        });
1399        if self.items.is_empty() {
1400            item.deactivated(cx);
1401            if close_pane_if_empty {
1402                self.update_toolbar(cx);
1403                cx.emit(Event::Remove {
1404                    focus_on_pane: focus_on_pane_if_closed,
1405                });
1406            }
1407        }
1408
1409        if item_index < self.active_item_index {
1410            self.active_item_index -= 1;
1411        }
1412
1413        let mode = self.nav_history.mode();
1414        self.nav_history.set_mode(NavigationMode::ClosingItem);
1415        item.deactivated(cx);
1416        self.nav_history.set_mode(mode);
1417
1418        if self.is_active_preview_item(item.item_id()) {
1419            self.set_preview_item_id(None, cx);
1420        }
1421
1422        if let Some(path) = item.project_path(cx) {
1423            let abs_path = self
1424                .nav_history
1425                .0
1426                .lock()
1427                .paths_by_item
1428                .get(&item.item_id())
1429                .and_then(|(_, abs_path)| abs_path.clone());
1430
1431            self.nav_history
1432                .0
1433                .lock()
1434                .paths_by_item
1435                .insert(item.item_id(), (path, abs_path));
1436        } else {
1437            self.nav_history
1438                .0
1439                .lock()
1440                .paths_by_item
1441                .remove(&item.item_id());
1442        }
1443
1444        if self.items.is_empty() && close_pane_if_empty && self.zoomed {
1445            cx.emit(Event::ZoomOut);
1446        }
1447
1448        cx.notify();
1449    }
1450
1451    pub async fn save_item(
1452        project: Model<Project>,
1453        pane: &WeakView<Pane>,
1454        item_ix: usize,
1455        item: &dyn ItemHandle,
1456        save_intent: SaveIntent,
1457        cx: &mut AsyncWindowContext,
1458    ) -> Result<bool> {
1459        const CONFLICT_MESSAGE: &str =
1460                "This file has changed on disk since you started editing it. Do you want to overwrite it?";
1461
1462        if save_intent == SaveIntent::Skip {
1463            return Ok(true);
1464        }
1465
1466        let (mut has_conflict, mut is_dirty, mut can_save, can_save_as) = cx.update(|cx| {
1467            (
1468                item.has_conflict(cx),
1469                item.is_dirty(cx),
1470                item.can_save(cx),
1471                item.is_singleton(cx),
1472            )
1473        })?;
1474
1475        // when saving a single buffer, we ignore whether or not it's dirty.
1476        if save_intent == SaveIntent::Save || save_intent == SaveIntent::SaveWithoutFormat {
1477            is_dirty = true;
1478        }
1479
1480        if save_intent == SaveIntent::SaveAs {
1481            is_dirty = true;
1482            has_conflict = false;
1483            can_save = false;
1484        }
1485
1486        if save_intent == SaveIntent::Overwrite {
1487            has_conflict = false;
1488        }
1489
1490        let should_format = save_intent != SaveIntent::SaveWithoutFormat;
1491
1492        if has_conflict && can_save {
1493            let answer = pane.update(cx, |pane, cx| {
1494                pane.activate_item(item_ix, true, true, cx);
1495                cx.prompt(
1496                    PromptLevel::Warning,
1497                    CONFLICT_MESSAGE,
1498                    None,
1499                    &["Overwrite", "Discard", "Cancel"],
1500                )
1501            })?;
1502            match answer.await {
1503                Ok(0) => {
1504                    pane.update(cx, |_, cx| item.save(should_format, project, cx))?
1505                        .await?
1506                }
1507                Ok(1) => pane.update(cx, |_, cx| item.reload(project, cx))?.await?,
1508                _ => return Ok(false),
1509            }
1510        } else if is_dirty && (can_save || can_save_as) {
1511            if save_intent == SaveIntent::Close {
1512                let will_autosave = cx.update(|cx| {
1513                    matches!(
1514                        item.workspace_settings(cx).autosave,
1515                        AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange
1516                    ) && Self::can_autosave_item(item, cx)
1517                })?;
1518                if !will_autosave {
1519                    let item_id = item.item_id();
1520                    let answer_task = pane.update(cx, |pane, cx| {
1521                        if pane.save_modals_spawned.insert(item_id) {
1522                            pane.activate_item(item_ix, true, true, cx);
1523                            let prompt = dirty_message_for(item.project_path(cx));
1524                            Some(cx.prompt(
1525                                PromptLevel::Warning,
1526                                &prompt,
1527                                None,
1528                                &["Save", "Don't Save", "Cancel"],
1529                            ))
1530                        } else {
1531                            None
1532                        }
1533                    })?;
1534                    if let Some(answer_task) = answer_task {
1535                        let answer = answer_task.await;
1536                        pane.update(cx, |pane, _| {
1537                            if !pane.save_modals_spawned.remove(&item_id) {
1538                                debug_panic!(
1539                                    "save modal was not present in spawned modals after awaiting for its answer"
1540                                )
1541                            }
1542                        })?;
1543                        match answer {
1544                            Ok(0) => {}
1545                            Ok(1) => {
1546                                // Don't save this file
1547                                pane.update(cx, |_, cx| item.discarded(project, cx))
1548                                    .log_err();
1549                                return Ok(true);
1550                            }
1551                            _ => return Ok(false), // Cancel
1552                        }
1553                    } else {
1554                        return Ok(false);
1555                    }
1556                }
1557            }
1558
1559            if can_save {
1560                pane.update(cx, |_, cx| item.save(should_format, project, cx))?
1561                    .await?;
1562            } else if can_save_as {
1563                let abs_path = pane.update(cx, |pane, cx| {
1564                    pane.workspace
1565                        .update(cx, |workspace, cx| workspace.prompt_for_new_path(cx))
1566                })??;
1567                if let Some(abs_path) = abs_path.await.ok().flatten() {
1568                    pane.update(cx, |pane, cx| {
1569                        if let Some(item) = pane.item_for_path(abs_path.clone(), cx) {
1570                            if let Some(idx) = pane.index_for_item(&*item) {
1571                                pane.remove_item(idx, false, false, cx);
1572                            }
1573                        }
1574
1575                        item.save_as(project, abs_path, cx)
1576                    })?
1577                    .await?;
1578                } else {
1579                    return Ok(false);
1580                }
1581            }
1582        }
1583
1584        pane.update(cx, |_, cx| {
1585            cx.emit(Event::UserSavedItem {
1586                item: item.downgrade_item(),
1587                save_intent,
1588            });
1589            true
1590        })
1591    }
1592
1593    fn can_autosave_item(item: &dyn ItemHandle, cx: &AppContext) -> bool {
1594        let is_deleted = item.project_entry_ids(cx).is_empty();
1595        item.is_dirty(cx) && !item.has_conflict(cx) && item.can_save(cx) && !is_deleted
1596    }
1597
1598    pub fn autosave_item(
1599        item: &dyn ItemHandle,
1600        project: Model<Project>,
1601        cx: &mut WindowContext,
1602    ) -> Task<Result<()>> {
1603        let format =
1604            if let AutosaveSetting::AfterDelay { .. } = item.workspace_settings(cx).autosave {
1605                false
1606            } else {
1607                true
1608            };
1609        if Self::can_autosave_item(item, cx) {
1610            item.save(format, project, cx)
1611        } else {
1612            Task::ready(Ok(()))
1613        }
1614    }
1615
1616    pub fn focus(&mut self, cx: &mut ViewContext<Pane>) {
1617        cx.focus(&self.focus_handle);
1618    }
1619
1620    pub fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
1621        if let Some(active_item) = self.active_item() {
1622            let focus_handle = active_item.focus_handle(cx);
1623            cx.focus(&focus_handle);
1624        }
1625    }
1626
1627    pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext<Self>) {
1628        cx.emit(Event::Split(direction));
1629    }
1630
1631    pub fn toolbar(&self) -> &View<Toolbar> {
1632        &self.toolbar
1633    }
1634
1635    pub fn handle_deleted_project_item(
1636        &mut self,
1637        entry_id: ProjectEntryId,
1638        cx: &mut ViewContext<Pane>,
1639    ) -> Option<()> {
1640        let (item_index_to_delete, item_id) = self.items().enumerate().find_map(|(i, item)| {
1641            if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
1642                Some((i, item.item_id()))
1643            } else {
1644                None
1645            }
1646        })?;
1647
1648        self.remove_item(item_index_to_delete, false, true, cx);
1649        self.nav_history.remove_item(item_id);
1650
1651        Some(())
1652    }
1653
1654    fn update_toolbar(&mut self, cx: &mut ViewContext<Self>) {
1655        let active_item = self
1656            .items
1657            .get(self.active_item_index)
1658            .map(|item| item.as_ref());
1659        self.toolbar.update(cx, |toolbar, cx| {
1660            toolbar.set_active_item(active_item, cx);
1661        });
1662    }
1663
1664    fn update_status_bar(&mut self, cx: &mut ViewContext<Self>) {
1665        let workspace = self.workspace.clone();
1666        let pane = cx.view().clone();
1667
1668        cx.window_context().defer(move |cx| {
1669            let Ok(status_bar) = workspace.update(cx, |workspace, _| workspace.status_bar.clone())
1670            else {
1671                return;
1672            };
1673
1674            status_bar.update(cx, move |status_bar, cx| {
1675                status_bar.set_active_pane(&pane, cx);
1676            });
1677        });
1678    }
1679
1680    fn entry_abs_path(&self, entry: ProjectEntryId, cx: &WindowContext) -> Option<PathBuf> {
1681        let worktree = self
1682            .workspace
1683            .upgrade()?
1684            .read(cx)
1685            .project()
1686            .read(cx)
1687            .worktree_for_entry(entry, cx)?
1688            .read(cx);
1689        let entry = worktree.entry_for_id(entry)?;
1690        let abs_path = worktree.absolutize(&entry.path).ok()?;
1691        if entry.is_symlink {
1692            abs_path.canonicalize().ok()
1693        } else {
1694            Some(abs_path)
1695        }
1696    }
1697
1698    fn copy_relative_path(&mut self, _: &CopyRelativePath, cx: &mut ViewContext<Self>) {
1699        if let Some(clipboard_text) = self
1700            .active_item()
1701            .as_ref()
1702            .and_then(|entry| entry.project_path(cx))
1703            .map(|p| p.path.to_string_lossy().to_string())
1704        {
1705            cx.write_to_clipboard(ClipboardItem::new_string(clipboard_text));
1706        }
1707    }
1708
1709    pub fn icon_color(selected: bool) -> Color {
1710        if selected {
1711            Color::Default
1712        } else {
1713            Color::Muted
1714        }
1715    }
1716
1717    pub fn git_aware_icon_color(
1718        git_status: Option<GitFileStatus>,
1719        ignored: bool,
1720        selected: bool,
1721    ) -> Color {
1722        if ignored {
1723            Color::Ignored
1724        } else {
1725            match git_status {
1726                Some(GitFileStatus::Added) => Color::Created,
1727                Some(GitFileStatus::Modified) => Color::Modified,
1728                Some(GitFileStatus::Conflict) => Color::Conflict,
1729                None => Self::icon_color(selected),
1730            }
1731        }
1732    }
1733
1734    fn toggle_pin_tab(&mut self, _: &TogglePinTab, cx: &mut ViewContext<'_, Self>) {
1735        if self.items.is_empty() {
1736            return;
1737        }
1738        let active_tab_ix = self.active_item_index();
1739        if self.is_tab_pinned(active_tab_ix) {
1740            self.unpin_tab_at(active_tab_ix, cx);
1741        } else {
1742            self.pin_tab_at(active_tab_ix, cx);
1743        }
1744    }
1745
1746    fn pin_tab_at(&mut self, ix: usize, cx: &mut ViewContext<'_, Self>) {
1747        maybe!({
1748            let pane = cx.view().clone();
1749            let destination_index = self.pinned_tab_count;
1750            self.pinned_tab_count += 1;
1751            let id = self.item_for_index(ix)?.item_id();
1752
1753            self.workspace
1754                .update(cx, |_, cx| {
1755                    cx.defer(move |this, cx| {
1756                        this.move_item(pane.clone(), pane, id, destination_index, cx)
1757                    });
1758                })
1759                .ok()?;
1760
1761            Some(())
1762        });
1763    }
1764
1765    fn unpin_tab_at(&mut self, ix: usize, cx: &mut ViewContext<'_, Self>) {
1766        maybe!({
1767            let pane = cx.view().clone();
1768            self.pinned_tab_count = self.pinned_tab_count.checked_sub(1).unwrap();
1769            let destination_index = self.pinned_tab_count;
1770
1771            let id = self.item_for_index(ix)?.item_id();
1772
1773            self.workspace
1774                .update(cx, |_, cx| {
1775                    cx.defer(move |this, cx| {
1776                        this.move_item(pane.clone(), pane, id, destination_index, cx)
1777                    });
1778                })
1779                .ok()?;
1780
1781            Some(())
1782        });
1783    }
1784
1785    fn is_tab_pinned(&self, ix: usize) -> bool {
1786        self.pinned_tab_count > ix
1787    }
1788
1789    fn has_pinned_tabs(&self) -> bool {
1790        self.pinned_tab_count != 0
1791    }
1792
1793    fn render_tab(
1794        &self,
1795        ix: usize,
1796        item: &dyn ItemHandle,
1797        detail: usize,
1798        cx: &mut ViewContext<'_, Pane>,
1799    ) -> impl IntoElement {
1800        let project_path = item.project_path(cx);
1801
1802        let is_active = ix == self.active_item_index;
1803        let is_preview = self
1804            .preview_item_id
1805            .map(|id| id == item.item_id())
1806            .unwrap_or(false);
1807
1808        let label = item.tab_content(
1809            TabContentParams {
1810                detail: Some(detail),
1811                selected: is_active,
1812                preview: is_preview,
1813            },
1814            cx,
1815        );
1816
1817        let icon_color = if ItemSettings::get_global(cx).git_status {
1818            project_path
1819                .as_ref()
1820                .and_then(|path| self.project.read(cx).entry_for_path(&path, cx))
1821                .map(|entry| {
1822                    Self::git_aware_icon_color(entry.git_status, entry.is_ignored, is_active)
1823                })
1824                .unwrap_or_else(|| Self::icon_color(is_active))
1825        } else {
1826            Self::icon_color(is_active)
1827        };
1828
1829        let icon = item.tab_icon(cx);
1830        let close_side = &ItemSettings::get_global(cx).close_position;
1831        let indicator = render_item_indicator(item.boxed_clone(), cx);
1832        let item_id = item.item_id();
1833        let is_first_item = ix == 0;
1834        let is_last_item = ix == self.items.len() - 1;
1835        let is_pinned = self.is_tab_pinned(ix);
1836        let position_relative_to_active_item = ix.cmp(&self.active_item_index);
1837
1838        let tab = Tab::new(ix)
1839            .position(if is_first_item {
1840                TabPosition::First
1841            } else if is_last_item {
1842                TabPosition::Last
1843            } else {
1844                TabPosition::Middle(position_relative_to_active_item)
1845            })
1846            .close_side(match close_side {
1847                ClosePosition::Left => ui::TabCloseSide::Start,
1848                ClosePosition::Right => ui::TabCloseSide::End,
1849            })
1850            .selected(is_active)
1851            .on_click(
1852                cx.listener(move |pane: &mut Self, _, cx| pane.activate_item(ix, true, true, cx)),
1853            )
1854            // TODO: This should be a click listener with the middle mouse button instead of a mouse down listener.
1855            .on_mouse_down(
1856                MouseButton::Middle,
1857                cx.listener(move |pane, _event, cx| {
1858                    pane.close_item_by_id(item_id, SaveIntent::Close, cx)
1859                        .detach_and_log_err(cx);
1860                }),
1861            )
1862            .on_mouse_down(
1863                MouseButton::Left,
1864                cx.listener(move |pane, event: &MouseDownEvent, cx| {
1865                    if let Some(id) = pane.preview_item_id {
1866                        if id == item_id && event.click_count > 1 {
1867                            pane.set_preview_item_id(None, cx);
1868                        }
1869                    }
1870                }),
1871            )
1872            .on_drag(
1873                DraggedTab {
1874                    item: item.boxed_clone(),
1875                    pane: cx.view().clone(),
1876                    detail,
1877                    is_active,
1878                    ix,
1879                },
1880                |tab, cx| cx.new_view(|_| tab.clone()),
1881            )
1882            .drag_over::<DraggedTab>(|tab, _, cx| {
1883                tab.bg(cx.theme().colors().drop_target_background)
1884            })
1885            .drag_over::<DraggedSelection>(|tab, _, cx| {
1886                tab.bg(cx.theme().colors().drop_target_background)
1887            })
1888            .when_some(self.can_drop_predicate.clone(), |this, p| {
1889                this.can_drop(move |a, cx| p(a, cx))
1890            })
1891            .on_drop(cx.listener(move |this, dragged_tab: &DraggedTab, cx| {
1892                this.drag_split_direction = None;
1893                this.handle_tab_drop(dragged_tab, ix, cx)
1894            }))
1895            .on_drop(cx.listener(move |this, selection: &DraggedSelection, cx| {
1896                this.drag_split_direction = None;
1897                this.handle_dragged_selection_drop(selection, cx)
1898            }))
1899            .on_drop(cx.listener(move |this, paths, cx| {
1900                this.drag_split_direction = None;
1901                this.handle_external_paths_drop(paths, cx)
1902            }))
1903            .when_some(item.tab_tooltip_text(cx), |tab, text| {
1904                tab.tooltip(move |cx| Tooltip::text(text.clone(), cx))
1905            })
1906            .start_slot::<Indicator>(indicator)
1907            .map(|this| {
1908                let end_slot = if is_pinned {
1909                    IconButton::new("unpin tab", IconName::Pin)
1910                        .shape(IconButtonShape::Square)
1911                        .icon_color(Color::Muted)
1912                        .size(ButtonSize::None)
1913                        .icon_size(IconSize::XSmall)
1914                        .on_click(cx.listener(move |pane, _, cx| {
1915                            pane.unpin_tab_at(ix, cx);
1916                        }))
1917                        .tooltip(|cx| Tooltip::text("Unpin Tab", cx))
1918                } else {
1919                    IconButton::new("close tab", IconName::Close)
1920                        .visible_on_hover("")
1921                        .shape(IconButtonShape::Square)
1922                        .icon_color(Color::Muted)
1923                        .size(ButtonSize::None)
1924                        .icon_size(IconSize::XSmall)
1925                        .on_click(cx.listener(move |pane, _, cx| {
1926                            pane.close_item_by_id(item_id, SaveIntent::Close, cx)
1927                                .detach_and_log_err(cx);
1928                        }))
1929                };
1930                this.end_slot(end_slot)
1931            })
1932            .child(
1933                h_flex()
1934                    .gap_1()
1935                    .children(icon.map(|icon| icon.size(IconSize::Small).color(icon_color)))
1936                    .child(label),
1937            );
1938
1939        let single_entry_to_resolve = {
1940            let item_entries = self.items[ix].project_entry_ids(cx);
1941            if item_entries.len() == 1 {
1942                Some(item_entries[0])
1943            } else {
1944                None
1945            }
1946        };
1947
1948        let is_pinned = self.is_tab_pinned(ix);
1949        let pane = cx.view().downgrade();
1950        right_click_menu(ix).trigger(tab).menu(move |cx| {
1951            let pane = pane.clone();
1952            ContextMenu::build(cx, move |mut menu, cx| {
1953                if let Some(pane) = pane.upgrade() {
1954                    menu = menu
1955                        .entry(
1956                            "Close",
1957                            Some(Box::new(CloseActiveItem { save_intent: None })),
1958                            cx.handler_for(&pane, move |pane, cx| {
1959                                pane.close_item_by_id(item_id, SaveIntent::Close, cx)
1960                                    .detach_and_log_err(cx);
1961                            }),
1962                        )
1963                        .entry(
1964                            "Close Others",
1965                            Some(Box::new(CloseInactiveItems { save_intent: None })),
1966                            cx.handler_for(&pane, move |pane, cx| {
1967                                pane.close_items(cx, SaveIntent::Close, |id| id != item_id)
1968                                    .detach_and_log_err(cx);
1969                            }),
1970                        )
1971                        .separator()
1972                        .entry(
1973                            "Close Left",
1974                            Some(Box::new(CloseItemsToTheLeft)),
1975                            cx.handler_for(&pane, move |pane, cx| {
1976                                pane.close_items_to_the_left_by_id(item_id, cx)
1977                                    .detach_and_log_err(cx);
1978                            }),
1979                        )
1980                        .entry(
1981                            "Close Right",
1982                            Some(Box::new(CloseItemsToTheRight)),
1983                            cx.handler_for(&pane, move |pane, cx| {
1984                                pane.close_items_to_the_right_by_id(item_id, cx)
1985                                    .detach_and_log_err(cx);
1986                            }),
1987                        )
1988                        .separator()
1989                        .entry(
1990                            "Close Clean",
1991                            Some(Box::new(CloseCleanItems)),
1992                            cx.handler_for(&pane, move |pane, cx| {
1993                                if let Some(task) = pane.close_clean_items(&CloseCleanItems, cx) {
1994                                    task.detach_and_log_err(cx)
1995                                }
1996                            }),
1997                        )
1998                        .entry(
1999                            "Close All",
2000                            Some(Box::new(CloseAllItems { save_intent: None })),
2001                            cx.handler_for(&pane, |pane, cx| {
2002                                if let Some(task) =
2003                                    pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
2004                                {
2005                                    task.detach_and_log_err(cx)
2006                                }
2007                            }),
2008                        );
2009
2010                    let pin_tab_entries = |menu: ContextMenu| {
2011                        menu.separator().map(|this| {
2012                            if is_pinned {
2013                                this.entry(
2014                                    "Unpin Tab",
2015                                    Some(TogglePinTab.boxed_clone()),
2016                                    cx.handler_for(&pane, move |pane, cx| {
2017                                        pane.unpin_tab_at(ix, cx);
2018                                    }),
2019                                )
2020                            } else {
2021                                this.entry(
2022                                    "Pin Tab",
2023                                    Some(TogglePinTab.boxed_clone()),
2024                                    cx.handler_for(&pane, move |pane, cx| {
2025                                        pane.pin_tab_at(ix, cx);
2026                                    }),
2027                                )
2028                            }
2029                        })
2030                    };
2031                    if let Some(entry) = single_entry_to_resolve {
2032                        let entry_abs_path = pane.read(cx).entry_abs_path(entry, cx);
2033                        let parent_abs_path = entry_abs_path
2034                            .as_deref()
2035                            .and_then(|abs_path| Some(abs_path.parent()?.to_path_buf()));
2036
2037                        let entry_id = entry.to_proto();
2038                        menu = menu
2039                            .separator()
2040                            .when_some(entry_abs_path, |menu, abs_path| {
2041                                menu.entry(
2042                                    "Copy Path",
2043                                    Some(Box::new(CopyPath)),
2044                                    cx.handler_for(&pane, move |_, cx| {
2045                                        cx.write_to_clipboard(ClipboardItem::new_string(
2046                                            abs_path.to_string_lossy().to_string(),
2047                                        ));
2048                                    }),
2049                                )
2050                            })
2051                            .entry(
2052                                "Copy Relative Path",
2053                                Some(Box::new(CopyRelativePath)),
2054                                cx.handler_for(&pane, move |pane, cx| {
2055                                    pane.copy_relative_path(&CopyRelativePath, cx);
2056                                }),
2057                            )
2058                            .map(pin_tab_entries)
2059                            .separator()
2060                            .entry(
2061                                "Reveal In Project Panel",
2062                                Some(Box::new(RevealInProjectPanel {
2063                                    entry_id: Some(entry_id),
2064                                })),
2065                                cx.handler_for(&pane, move |pane, cx| {
2066                                    pane.project.update(cx, |_, cx| {
2067                                        cx.emit(project::Event::RevealInProjectPanel(
2068                                            ProjectEntryId::from_proto(entry_id),
2069                                        ))
2070                                    });
2071                                }),
2072                            )
2073                            .when_some(parent_abs_path, |menu, parent_abs_path| {
2074                                menu.entry(
2075                                    "Open in Terminal",
2076                                    Some(Box::new(OpenInTerminal)),
2077                                    cx.handler_for(&pane, move |_, cx| {
2078                                        cx.dispatch_action(
2079                                            OpenTerminal {
2080                                                working_directory: parent_abs_path.clone(),
2081                                            }
2082                                            .boxed_clone(),
2083                                        );
2084                                    }),
2085                                )
2086                            });
2087                    } else {
2088                        menu = menu.map(pin_tab_entries);
2089                    }
2090                }
2091
2092                menu
2093            })
2094        })
2095    }
2096
2097    fn render_tab_bar(&mut self, cx: &mut ViewContext<'_, Pane>) -> impl IntoElement {
2098        let focus_handle = self.focus_handle.clone();
2099        let navigate_backward = IconButton::new("navigate_backward", IconName::ArrowLeft)
2100            .shape(IconButtonShape::Square)
2101            .icon_size(IconSize::Small)
2102            .on_click({
2103                let view = cx.view().clone();
2104                move |_, cx| view.update(cx, Self::navigate_backward)
2105            })
2106            .disabled(!self.can_navigate_backward())
2107            .tooltip({
2108                let focus_handle = focus_handle.clone();
2109                move |cx| Tooltip::for_action_in("Go Back", &GoBack, &focus_handle, cx)
2110            });
2111
2112        let navigate_forward = IconButton::new("navigate_forward", IconName::ArrowRight)
2113            .shape(IconButtonShape::Square)
2114            .icon_size(IconSize::Small)
2115            .on_click({
2116                let view = cx.view().clone();
2117                move |_, cx| view.update(cx, Self::navigate_forward)
2118            })
2119            .disabled(!self.can_navigate_forward())
2120            .tooltip({
2121                let focus_handle = focus_handle.clone();
2122                move |cx| Tooltip::for_action_in("Go Forward", &GoForward, &focus_handle, cx)
2123            });
2124
2125        let mut tab_items = self
2126            .items
2127            .iter()
2128            .enumerate()
2129            .zip(tab_details(&self.items, cx))
2130            .map(|((ix, item), detail)| self.render_tab(ix, &**item, detail, cx))
2131            .collect::<Vec<_>>();
2132
2133        let unpinned_tabs = tab_items.split_off(self.pinned_tab_count);
2134        let pinned_tabs = tab_items;
2135        TabBar::new("tab_bar")
2136            .when(
2137                self.display_nav_history_buttons.unwrap_or_default(),
2138                |tab_bar| {
2139                    tab_bar
2140                        .start_child(navigate_backward)
2141                        .start_child(navigate_forward)
2142                },
2143            )
2144            .map(|tab_bar| {
2145                let render_tab_buttons = self.render_tab_bar_buttons.clone();
2146                let (left_children, right_children) = render_tab_buttons(self, cx);
2147
2148                tab_bar
2149                    .start_children(left_children)
2150                    .end_children(right_children)
2151            })
2152            .children(pinned_tabs.len().ne(&0).then(|| {
2153                h_flex()
2154                    .children(pinned_tabs)
2155                    .border_r_2()
2156                    .border_color(cx.theme().colors().border)
2157            }))
2158            .child(
2159                h_flex()
2160                    .id("unpinned tabs")
2161                    .overflow_x_scroll()
2162                    .w_full()
2163                    .track_scroll(&self.tab_bar_scroll_handle)
2164                    .children(unpinned_tabs)
2165                    .child(
2166                        div()
2167                            .id("tab_bar_drop_target")
2168                            .min_w_6()
2169                            // HACK: This empty child is currently necessary to force the drop target to appear
2170                            // despite us setting a min width above.
2171                            .child("")
2172                            .h_full()
2173                            .flex_grow()
2174                            .drag_over::<DraggedTab>(|bar, _, cx| {
2175                                bar.bg(cx.theme().colors().drop_target_background)
2176                            })
2177                            .drag_over::<DraggedSelection>(|bar, _, cx| {
2178                                bar.bg(cx.theme().colors().drop_target_background)
2179                            })
2180                            .on_drop(cx.listener(move |this, dragged_tab: &DraggedTab, cx| {
2181                                this.drag_split_direction = None;
2182                                this.handle_tab_drop(dragged_tab, this.items.len(), cx)
2183                            }))
2184                            .on_drop(cx.listener(move |this, selection: &DraggedSelection, cx| {
2185                                this.drag_split_direction = None;
2186                                this.handle_project_entry_drop(
2187                                    &selection.active_selection.entry_id,
2188                                    cx,
2189                                )
2190                            }))
2191                            .on_drop(cx.listener(move |this, paths, cx| {
2192                                this.drag_split_direction = None;
2193                                this.handle_external_paths_drop(paths, cx)
2194                            }))
2195                            .on_click(cx.listener(move |this, event: &ClickEvent, cx| {
2196                                if event.up.click_count == 2 {
2197                                    cx.dispatch_action(
2198                                        this.double_click_dispatch_action.boxed_clone(),
2199                                    )
2200                                }
2201                            })),
2202                    ),
2203            )
2204    }
2205
2206    pub fn render_menu_overlay(menu: &View<ContextMenu>) -> Div {
2207        div().absolute().bottom_0().right_0().size_0().child(
2208            deferred(
2209                anchored()
2210                    .anchor(AnchorCorner::TopRight)
2211                    .child(menu.clone()),
2212            )
2213            .with_priority(1),
2214        )
2215    }
2216
2217    pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
2218        self.zoomed = zoomed;
2219        cx.notify();
2220    }
2221
2222    pub fn is_zoomed(&self) -> bool {
2223        self.zoomed
2224    }
2225
2226    fn handle_drag_move<T>(&mut self, event: &DragMoveEvent<T>, cx: &mut ViewContext<Self>) {
2227        if !self.can_split {
2228            return;
2229        }
2230
2231        let rect = event.bounds.size;
2232
2233        let size = event.bounds.size.width.min(event.bounds.size.height)
2234            * WorkspaceSettings::get_global(cx).drop_target_size;
2235
2236        let relative_cursor = Point::new(
2237            event.event.position.x - event.bounds.left(),
2238            event.event.position.y - event.bounds.top(),
2239        );
2240
2241        let direction = if relative_cursor.x < size
2242            || relative_cursor.x > rect.width - size
2243            || relative_cursor.y < size
2244            || relative_cursor.y > rect.height - size
2245        {
2246            [
2247                SplitDirection::Up,
2248                SplitDirection::Right,
2249                SplitDirection::Down,
2250                SplitDirection::Left,
2251            ]
2252            .iter()
2253            .min_by_key(|side| match side {
2254                SplitDirection::Up => relative_cursor.y,
2255                SplitDirection::Right => rect.width - relative_cursor.x,
2256                SplitDirection::Down => rect.height - relative_cursor.y,
2257                SplitDirection::Left => relative_cursor.x,
2258            })
2259            .cloned()
2260        } else {
2261            None
2262        };
2263
2264        if direction != self.drag_split_direction {
2265            self.drag_split_direction = direction;
2266        }
2267    }
2268
2269    fn handle_tab_drop(
2270        &mut self,
2271        dragged_tab: &DraggedTab,
2272        ix: usize,
2273        cx: &mut ViewContext<'_, Self>,
2274    ) {
2275        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2276            if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_tab, cx) {
2277                return;
2278            }
2279        }
2280        let mut to_pane = cx.view().clone();
2281        let split_direction = self.drag_split_direction;
2282        let item_id = dragged_tab.item.item_id();
2283        if let Some(preview_item_id) = self.preview_item_id {
2284            if item_id == preview_item_id {
2285                self.set_preview_item_id(None, cx);
2286            }
2287        }
2288
2289        let from_pane = dragged_tab.pane.clone();
2290        self.workspace
2291            .update(cx, |_, cx| {
2292                cx.defer(move |workspace, cx| {
2293                    if let Some(split_direction) = split_direction {
2294                        to_pane = workspace.split_pane(to_pane, split_direction, cx);
2295                    }
2296                    let old_ix = from_pane.read(cx).index_for_item_id(item_id);
2297                    if to_pane == from_pane {
2298                        if let Some(old_index) = old_ix {
2299                            to_pane.update(cx, |this, _| {
2300                                if old_index < this.pinned_tab_count
2301                                    && (ix == this.items.len() || ix > this.pinned_tab_count)
2302                                {
2303                                    this.pinned_tab_count -= 1;
2304                                } else if this.has_pinned_tabs()
2305                                    && old_index >= this.pinned_tab_count
2306                                    && ix < this.pinned_tab_count
2307                                {
2308                                    this.pinned_tab_count += 1;
2309                                }
2310                            });
2311                        }
2312                    } else {
2313                        to_pane.update(cx, |this, _| {
2314                            if this.has_pinned_tabs() && ix < this.pinned_tab_count {
2315                                this.pinned_tab_count += 1;
2316                            }
2317                        });
2318                        from_pane.update(cx, |this, _| {
2319                            if let Some(index) = old_ix {
2320                                if this.pinned_tab_count > index {
2321                                    this.pinned_tab_count -= 1;
2322                                }
2323                            }
2324                        })
2325                    }
2326                    workspace.move_item(from_pane.clone(), to_pane.clone(), item_id, ix, cx);
2327                });
2328            })
2329            .log_err();
2330    }
2331
2332    fn handle_dragged_selection_drop(
2333        &mut self,
2334        dragged_selection: &DraggedSelection,
2335        cx: &mut ViewContext<'_, Self>,
2336    ) {
2337        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2338            if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_selection, cx) {
2339                return;
2340            }
2341        }
2342        self.handle_project_entry_drop(&dragged_selection.active_selection.entry_id, cx);
2343    }
2344
2345    fn handle_project_entry_drop(
2346        &mut self,
2347        project_entry_id: &ProjectEntryId,
2348        cx: &mut ViewContext<'_, Self>,
2349    ) {
2350        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2351            if let ControlFlow::Break(()) = custom_drop_handle(self, project_entry_id, cx) {
2352                return;
2353            }
2354        }
2355        let mut to_pane = cx.view().clone();
2356        let split_direction = self.drag_split_direction;
2357        let project_entry_id = *project_entry_id;
2358        self.workspace
2359            .update(cx, |_, cx| {
2360                cx.defer(move |workspace, cx| {
2361                    if let Some(path) = workspace
2362                        .project()
2363                        .read(cx)
2364                        .path_for_entry(project_entry_id, cx)
2365                    {
2366                        let load_path_task = workspace.load_path(path, cx);
2367                        cx.spawn(|workspace, mut cx| async move {
2368                            if let Some((project_entry_id, build_item)) =
2369                                load_path_task.await.notify_async_err(&mut cx)
2370                            {
2371                                let (to_pane, new_item_handle) = workspace
2372                                    .update(&mut cx, |workspace, cx| {
2373                                        if let Some(split_direction) = split_direction {
2374                                            to_pane =
2375                                                workspace.split_pane(to_pane, split_direction, cx);
2376                                        }
2377                                        let new_item_handle = to_pane.update(cx, |pane, cx| {
2378                                            pane.open_item(
2379                                                project_entry_id,
2380                                                true,
2381                                                false,
2382                                                cx,
2383                                                build_item,
2384                                            )
2385                                        });
2386                                        (to_pane, new_item_handle)
2387                                    })
2388                                    .log_err()?;
2389                                to_pane
2390                                    .update(&mut cx, |this, cx| {
2391                                        let Some(index) = this.index_for_item(&*new_item_handle)
2392                                        else {
2393                                            return;
2394                                        };
2395                                        if !this.is_tab_pinned(index) {
2396                                            this.pin_tab_at(index, cx);
2397                                        }
2398                                    })
2399                                    .ok()?
2400                            }
2401                            Some(())
2402                        })
2403                        .detach();
2404                    };
2405                });
2406            })
2407            .log_err();
2408    }
2409
2410    fn handle_external_paths_drop(
2411        &mut self,
2412        paths: &ExternalPaths,
2413        cx: &mut ViewContext<'_, Self>,
2414    ) {
2415        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2416            if let ControlFlow::Break(()) = custom_drop_handle(self, paths, cx) {
2417                return;
2418            }
2419        }
2420        let mut to_pane = cx.view().clone();
2421        let mut split_direction = self.drag_split_direction;
2422        let paths = paths.paths().to_vec();
2423        let is_remote = self
2424            .workspace
2425            .update(cx, |workspace, cx| {
2426                if workspace.project().read(cx).is_via_collab() {
2427                    workspace.show_error(
2428                        &anyhow::anyhow!("Cannot drop files on a remote project"),
2429                        cx,
2430                    );
2431                    true
2432                } else {
2433                    false
2434                }
2435            })
2436            .unwrap_or(true);
2437        if is_remote {
2438            return;
2439        }
2440
2441        self.workspace
2442            .update(cx, |workspace, cx| {
2443                let fs = Arc::clone(workspace.project().read(cx).fs());
2444                cx.spawn(|workspace, mut cx| async move {
2445                    let mut is_file_checks = FuturesUnordered::new();
2446                    for path in &paths {
2447                        is_file_checks.push(fs.is_file(path))
2448                    }
2449                    let mut has_files_to_open = false;
2450                    while let Some(is_file) = is_file_checks.next().await {
2451                        if is_file {
2452                            has_files_to_open = true;
2453                            break;
2454                        }
2455                    }
2456                    drop(is_file_checks);
2457                    if !has_files_to_open {
2458                        split_direction = None;
2459                    }
2460
2461                    if let Some(open_task) = workspace
2462                        .update(&mut cx, |workspace, cx| {
2463                            if let Some(split_direction) = split_direction {
2464                                to_pane = workspace.split_pane(to_pane, split_direction, cx);
2465                            }
2466                            workspace.open_paths(
2467                                paths,
2468                                OpenVisible::OnlyDirectories,
2469                                Some(to_pane.downgrade()),
2470                                cx,
2471                            )
2472                        })
2473                        .ok()
2474                    {
2475                        let opened_items: Vec<_> = open_task.await;
2476                        _ = workspace.update(&mut cx, |workspace, cx| {
2477                            for item in opened_items.into_iter().flatten() {
2478                                if let Err(e) = item {
2479                                    workspace.show_error(&e, cx);
2480                                }
2481                            }
2482                        });
2483                    }
2484                })
2485                .detach();
2486            })
2487            .log_err();
2488    }
2489
2490    pub fn display_nav_history_buttons(&mut self, display: Option<bool>) {
2491        self.display_nav_history_buttons = display;
2492    }
2493}
2494
2495impl FocusableView for Pane {
2496    fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
2497        self.focus_handle.clone()
2498    }
2499}
2500
2501impl Render for Pane {
2502    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2503        let mut key_context = KeyContext::new_with_defaults();
2504        key_context.add("Pane");
2505        if self.active_item().is_none() {
2506            key_context.add("EmptyPane");
2507        }
2508
2509        let should_display_tab_bar = self.should_display_tab_bar.clone();
2510        let display_tab_bar = should_display_tab_bar(cx);
2511
2512        v_flex()
2513            .key_context(key_context)
2514            .track_focus(&self.focus_handle)
2515            .size_full()
2516            .flex_none()
2517            .overflow_hidden()
2518            .on_action(cx.listener(|pane, _: &AlternateFile, cx| {
2519                pane.alternate_file(cx);
2520            }))
2521            .on_action(cx.listener(|pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx)))
2522            .on_action(cx.listener(|pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx)))
2523            .on_action(cx.listener(|pane, _: &SplitHorizontal, cx| {
2524                pane.split(SplitDirection::horizontal(cx), cx)
2525            }))
2526            .on_action(cx.listener(|pane, _: &SplitVertical, cx| {
2527                pane.split(SplitDirection::vertical(cx), cx)
2528            }))
2529            .on_action(
2530                cx.listener(|pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx)),
2531            )
2532            .on_action(cx.listener(|pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx)))
2533            .on_action(cx.listener(|pane, _: &GoBack, cx| pane.navigate_backward(cx)))
2534            .on_action(cx.listener(|pane, _: &GoForward, cx| pane.navigate_forward(cx)))
2535            .on_action(cx.listener(|pane, _: &JoinIntoNext, cx| pane.join_into_next(cx)))
2536            .on_action(cx.listener(Pane::toggle_zoom))
2537            .on_action(cx.listener(|pane: &mut Pane, action: &ActivateItem, cx| {
2538                pane.activate_item(action.0, true, true, cx);
2539            }))
2540            .on_action(cx.listener(|pane: &mut Pane, _: &ActivateLastItem, cx| {
2541                pane.activate_item(pane.items.len() - 1, true, true, cx);
2542            }))
2543            .on_action(cx.listener(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
2544                pane.activate_prev_item(true, cx);
2545            }))
2546            .on_action(cx.listener(|pane: &mut Pane, _: &ActivateNextItem, cx| {
2547                pane.activate_next_item(true, cx);
2548            }))
2549            .on_action(cx.listener(|pane, action, cx| {
2550                pane.toggle_pin_tab(action, cx);
2551            }))
2552            .when(PreviewTabsSettings::get_global(cx).enabled, |this| {
2553                this.on_action(cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, cx| {
2554                    if let Some(active_item_id) = pane.active_item().map(|i| i.item_id()) {
2555                        if pane.is_active_preview_item(active_item_id) {
2556                            pane.set_preview_item_id(None, cx);
2557                        } else {
2558                            pane.set_preview_item_id(Some(active_item_id), cx);
2559                        }
2560                    }
2561                }))
2562            })
2563            .on_action(
2564                cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
2565                    if let Some(task) = pane.close_active_item(action, cx) {
2566                        task.detach_and_log_err(cx)
2567                    }
2568                }),
2569            )
2570            .on_action(
2571                cx.listener(|pane: &mut Self, action: &CloseInactiveItems, cx| {
2572                    if let Some(task) = pane.close_inactive_items(action, cx) {
2573                        task.detach_and_log_err(cx)
2574                    }
2575                }),
2576            )
2577            .on_action(
2578                cx.listener(|pane: &mut Self, action: &CloseCleanItems, cx| {
2579                    if let Some(task) = pane.close_clean_items(action, cx) {
2580                        task.detach_and_log_err(cx)
2581                    }
2582                }),
2583            )
2584            .on_action(
2585                cx.listener(|pane: &mut Self, action: &CloseItemsToTheLeft, cx| {
2586                    if let Some(task) = pane.close_items_to_the_left(action, cx) {
2587                        task.detach_and_log_err(cx)
2588                    }
2589                }),
2590            )
2591            .on_action(
2592                cx.listener(|pane: &mut Self, action: &CloseItemsToTheRight, cx| {
2593                    if let Some(task) = pane.close_items_to_the_right(action, cx) {
2594                        task.detach_and_log_err(cx)
2595                    }
2596                }),
2597            )
2598            .on_action(cx.listener(|pane: &mut Self, action: &CloseAllItems, cx| {
2599                if let Some(task) = pane.close_all_items(action, cx) {
2600                    task.detach_and_log_err(cx)
2601                }
2602            }))
2603            .on_action(
2604                cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
2605                    if let Some(task) = pane.close_active_item(action, cx) {
2606                        task.detach_and_log_err(cx)
2607                    }
2608                }),
2609            )
2610            .on_action(
2611                cx.listener(|pane: &mut Self, action: &RevealInProjectPanel, cx| {
2612                    let entry_id = action
2613                        .entry_id
2614                        .map(ProjectEntryId::from_proto)
2615                        .or_else(|| pane.active_item()?.project_entry_ids(cx).first().copied());
2616                    if let Some(entry_id) = entry_id {
2617                        pane.project.update(cx, |_, cx| {
2618                            cx.emit(project::Event::RevealInProjectPanel(entry_id))
2619                        });
2620                    }
2621                }),
2622            )
2623            .when(self.active_item().is_some() && display_tab_bar, |pane| {
2624                pane.child(self.render_tab_bar(cx))
2625            })
2626            .child({
2627                let has_worktrees = self.project.read(cx).worktrees(cx).next().is_some();
2628                // main content
2629                div()
2630                    .flex_1()
2631                    .relative()
2632                    .group("")
2633                    .on_drag_move::<DraggedTab>(cx.listener(Self::handle_drag_move))
2634                    .on_drag_move::<DraggedSelection>(cx.listener(Self::handle_drag_move))
2635                    .on_drag_move::<ExternalPaths>(cx.listener(Self::handle_drag_move))
2636                    .map(|div| {
2637                        if let Some(item) = self.active_item() {
2638                            div.v_flex()
2639                                .child(self.toolbar.clone())
2640                                .child(item.to_any())
2641                        } else {
2642                            let placeholder = div.h_flex().size_full().justify_center();
2643                            if has_worktrees {
2644                                placeholder
2645                            } else {
2646                                placeholder.child(
2647                                    Label::new("Open a file or project to get started.")
2648                                        .color(Color::Muted),
2649                                )
2650                            }
2651                        }
2652                    })
2653                    .child(
2654                        // drag target
2655                        div()
2656                            .invisible()
2657                            .absolute()
2658                            .bg(cx.theme().colors().drop_target_background)
2659                            .group_drag_over::<DraggedTab>("", |style| style.visible())
2660                            .group_drag_over::<DraggedSelection>("", |style| style.visible())
2661                            .group_drag_over::<ExternalPaths>("", |style| style.visible())
2662                            .when_some(self.can_drop_predicate.clone(), |this, p| {
2663                                this.can_drop(move |a, cx| p(a, cx))
2664                            })
2665                            .on_drop(cx.listener(move |this, dragged_tab, cx| {
2666                                this.handle_tab_drop(dragged_tab, this.active_item_index(), cx)
2667                            }))
2668                            .on_drop(cx.listener(move |this, selection: &DraggedSelection, cx| {
2669                                this.handle_dragged_selection_drop(selection, cx)
2670                            }))
2671                            .on_drop(cx.listener(move |this, paths, cx| {
2672                                this.handle_external_paths_drop(paths, cx)
2673                            }))
2674                            .map(|div| {
2675                                let size = DefiniteLength::Fraction(0.5);
2676                                match self.drag_split_direction {
2677                                    None => div.top_0().right_0().bottom_0().left_0(),
2678                                    Some(SplitDirection::Up) => {
2679                                        div.top_0().left_0().right_0().h(size)
2680                                    }
2681                                    Some(SplitDirection::Down) => {
2682                                        div.left_0().bottom_0().right_0().h(size)
2683                                    }
2684                                    Some(SplitDirection::Left) => {
2685                                        div.top_0().left_0().bottom_0().w(size)
2686                                    }
2687                                    Some(SplitDirection::Right) => {
2688                                        div.top_0().bottom_0().right_0().w(size)
2689                                    }
2690                                }
2691                            }),
2692                    )
2693            })
2694            .on_mouse_down(
2695                MouseButton::Navigate(NavigationDirection::Back),
2696                cx.listener(|pane, _, cx| {
2697                    if let Some(workspace) = pane.workspace.upgrade() {
2698                        let pane = cx.view().downgrade();
2699                        cx.window_context().defer(move |cx| {
2700                            workspace.update(cx, |workspace, cx| {
2701                                workspace.go_back(pane, cx).detach_and_log_err(cx)
2702                            })
2703                        })
2704                    }
2705                }),
2706            )
2707            .on_mouse_down(
2708                MouseButton::Navigate(NavigationDirection::Forward),
2709                cx.listener(|pane, _, cx| {
2710                    if let Some(workspace) = pane.workspace.upgrade() {
2711                        let pane = cx.view().downgrade();
2712                        cx.window_context().defer(move |cx| {
2713                            workspace.update(cx, |workspace, cx| {
2714                                workspace.go_forward(pane, cx).detach_and_log_err(cx)
2715                            })
2716                        })
2717                    }
2718                }),
2719            )
2720    }
2721}
2722
2723impl ItemNavHistory {
2724    pub fn push<D: 'static + Send + Any>(&mut self, data: Option<D>, cx: &mut WindowContext) {
2725        self.history
2726            .push(data, self.item.clone(), self.is_preview, cx);
2727    }
2728
2729    pub fn pop_backward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
2730        self.history.pop(NavigationMode::GoingBack, cx)
2731    }
2732
2733    pub fn pop_forward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
2734        self.history.pop(NavigationMode::GoingForward, cx)
2735    }
2736}
2737
2738impl NavHistory {
2739    pub fn for_each_entry(
2740        &self,
2741        cx: &AppContext,
2742        mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
2743    ) {
2744        let borrowed_history = self.0.lock();
2745        borrowed_history
2746            .forward_stack
2747            .iter()
2748            .chain(borrowed_history.backward_stack.iter())
2749            .chain(borrowed_history.closed_stack.iter())
2750            .for_each(|entry| {
2751                if let Some(project_and_abs_path) =
2752                    borrowed_history.paths_by_item.get(&entry.item.id())
2753                {
2754                    f(entry, project_and_abs_path.clone());
2755                } else if let Some(item) = entry.item.upgrade() {
2756                    if let Some(path) = item.project_path(cx) {
2757                        f(entry, (path, None));
2758                    }
2759                }
2760            })
2761    }
2762
2763    pub fn set_mode(&mut self, mode: NavigationMode) {
2764        self.0.lock().mode = mode;
2765    }
2766
2767    pub fn mode(&self) -> NavigationMode {
2768        self.0.lock().mode
2769    }
2770
2771    pub fn disable(&mut self) {
2772        self.0.lock().mode = NavigationMode::Disabled;
2773    }
2774
2775    pub fn enable(&mut self) {
2776        self.0.lock().mode = NavigationMode::Normal;
2777    }
2778
2779    pub fn pop(&mut self, mode: NavigationMode, cx: &mut WindowContext) -> Option<NavigationEntry> {
2780        let mut state = self.0.lock();
2781        let entry = match mode {
2782            NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
2783                return None
2784            }
2785            NavigationMode::GoingBack => &mut state.backward_stack,
2786            NavigationMode::GoingForward => &mut state.forward_stack,
2787            NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
2788        }
2789        .pop_back();
2790        if entry.is_some() {
2791            state.did_update(cx);
2792        }
2793        entry
2794    }
2795
2796    pub fn push<D: 'static + Send + Any>(
2797        &mut self,
2798        data: Option<D>,
2799        item: Arc<dyn WeakItemHandle>,
2800        is_preview: bool,
2801        cx: &mut WindowContext,
2802    ) {
2803        let state = &mut *self.0.lock();
2804        match state.mode {
2805            NavigationMode::Disabled => {}
2806            NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
2807                if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2808                    state.backward_stack.pop_front();
2809                }
2810                state.backward_stack.push_back(NavigationEntry {
2811                    item,
2812                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2813                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2814                    is_preview,
2815                });
2816                state.forward_stack.clear();
2817            }
2818            NavigationMode::GoingBack => {
2819                if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2820                    state.forward_stack.pop_front();
2821                }
2822                state.forward_stack.push_back(NavigationEntry {
2823                    item,
2824                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2825                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2826                    is_preview,
2827                });
2828            }
2829            NavigationMode::GoingForward => {
2830                if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2831                    state.backward_stack.pop_front();
2832                }
2833                state.backward_stack.push_back(NavigationEntry {
2834                    item,
2835                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2836                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2837                    is_preview,
2838                });
2839            }
2840            NavigationMode::ClosingItem => {
2841                if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2842                    state.closed_stack.pop_front();
2843                }
2844                state.closed_stack.push_back(NavigationEntry {
2845                    item,
2846                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2847                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2848                    is_preview,
2849                });
2850            }
2851        }
2852        state.did_update(cx);
2853    }
2854
2855    pub fn remove_item(&mut self, item_id: EntityId) {
2856        let mut state = self.0.lock();
2857        state.paths_by_item.remove(&item_id);
2858        state
2859            .backward_stack
2860            .retain(|entry| entry.item.id() != item_id);
2861        state
2862            .forward_stack
2863            .retain(|entry| entry.item.id() != item_id);
2864        state
2865            .closed_stack
2866            .retain(|entry| entry.item.id() != item_id);
2867    }
2868
2869    pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option<PathBuf>)> {
2870        self.0.lock().paths_by_item.get(&item_id).cloned()
2871    }
2872}
2873
2874impl NavHistoryState {
2875    pub fn did_update(&self, cx: &mut WindowContext) {
2876        if let Some(pane) = self.pane.upgrade() {
2877            cx.defer(move |cx| {
2878                pane.update(cx, |pane, cx| pane.history_updated(cx));
2879            });
2880        }
2881    }
2882}
2883
2884fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
2885    let path = buffer_path
2886        .as_ref()
2887        .and_then(|p| {
2888            p.path
2889                .to_str()
2890                .and_then(|s| if s == "" { None } else { Some(s) })
2891        })
2892        .unwrap_or("This buffer");
2893    let path = truncate_and_remove_front(path, 80);
2894    format!("{path} contains unsaved edits. Do you want to save it?")
2895}
2896
2897pub fn tab_details(items: &Vec<Box<dyn ItemHandle>>, cx: &AppContext) -> Vec<usize> {
2898    let mut tab_details = items.iter().map(|_| 0).collect::<Vec<_>>();
2899    let mut tab_descriptions = HashMap::default();
2900    let mut done = false;
2901    while !done {
2902        done = true;
2903
2904        // Store item indices by their tab description.
2905        for (ix, (item, detail)) in items.iter().zip(&tab_details).enumerate() {
2906            if let Some(description) = item.tab_description(*detail, cx) {
2907                if *detail == 0
2908                    || Some(&description) != item.tab_description(detail - 1, cx).as_ref()
2909                {
2910                    tab_descriptions
2911                        .entry(description)
2912                        .or_insert(Vec::new())
2913                        .push(ix);
2914                }
2915            }
2916        }
2917
2918        // If two or more items have the same tab description, increase their level
2919        // of detail and try again.
2920        for (_, item_ixs) in tab_descriptions.drain() {
2921            if item_ixs.len() > 1 {
2922                done = false;
2923                for ix in item_ixs {
2924                    tab_details[ix] += 1;
2925                }
2926            }
2927        }
2928    }
2929
2930    tab_details
2931}
2932
2933pub fn render_item_indicator(item: Box<dyn ItemHandle>, cx: &WindowContext) -> Option<Indicator> {
2934    maybe!({
2935        let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
2936            (true, _) => Color::Warning,
2937            (_, true) => Color::Accent,
2938            (false, false) => return None,
2939        };
2940
2941        Some(Indicator::dot().color(indicator_color))
2942    })
2943}
2944
2945#[cfg(test)]
2946mod tests {
2947    use super::*;
2948    use crate::item::test::{TestItem, TestProjectItem};
2949    use gpui::{TestAppContext, VisualTestContext};
2950    use project::FakeFs;
2951    use settings::SettingsStore;
2952    use theme::LoadThemes;
2953
2954    #[gpui::test]
2955    async fn test_remove_active_empty(cx: &mut TestAppContext) {
2956        init_test(cx);
2957        let fs = FakeFs::new(cx.executor());
2958
2959        let project = Project::test(fs, None, cx).await;
2960        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2961        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2962
2963        pane.update(cx, |pane, cx| {
2964            assert!(pane
2965                .close_active_item(&CloseActiveItem { save_intent: None }, cx)
2966                .is_none())
2967        });
2968    }
2969
2970    #[gpui::test]
2971    async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
2972        init_test(cx);
2973        let fs = FakeFs::new(cx.executor());
2974
2975        let project = Project::test(fs, None, cx).await;
2976        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2977        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2978
2979        // 1. Add with a destination index
2980        //   a. Add before the active item
2981        set_labeled_items(&pane, ["A", "B*", "C"], cx);
2982        pane.update(cx, |pane, cx| {
2983            pane.add_item(
2984                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2985                false,
2986                false,
2987                Some(0),
2988                cx,
2989            );
2990        });
2991        assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
2992
2993        //   b. Add after the active item
2994        set_labeled_items(&pane, ["A", "B*", "C"], cx);
2995        pane.update(cx, |pane, cx| {
2996            pane.add_item(
2997                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2998                false,
2999                false,
3000                Some(2),
3001                cx,
3002            );
3003        });
3004        assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3005
3006        //   c. Add at the end of the item list (including off the length)
3007        set_labeled_items(&pane, ["A", "B*", "C"], cx);
3008        pane.update(cx, |pane, cx| {
3009            pane.add_item(
3010                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3011                false,
3012                false,
3013                Some(5),
3014                cx,
3015            );
3016        });
3017        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3018
3019        // 2. Add without a destination index
3020        //   a. Add with active item at the start of the item list
3021        set_labeled_items(&pane, ["A*", "B", "C"], cx);
3022        pane.update(cx, |pane, cx| {
3023            pane.add_item(
3024                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3025                false,
3026                false,
3027                None,
3028                cx,
3029            );
3030        });
3031        set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
3032
3033        //   b. Add with active item at the end of the item list
3034        set_labeled_items(&pane, ["A", "B", "C*"], cx);
3035        pane.update(cx, |pane, cx| {
3036            pane.add_item(
3037                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3038                false,
3039                false,
3040                None,
3041                cx,
3042            );
3043        });
3044        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3045    }
3046
3047    #[gpui::test]
3048    async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
3049        init_test(cx);
3050        let fs = FakeFs::new(cx.executor());
3051
3052        let project = Project::test(fs, None, cx).await;
3053        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3054        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3055
3056        // 1. Add with a destination index
3057        //   1a. Add before the active item
3058        let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3059        pane.update(cx, |pane, cx| {
3060            pane.add_item(d, false, false, Some(0), cx);
3061        });
3062        assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
3063
3064        //   1b. Add after the active item
3065        let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3066        pane.update(cx, |pane, cx| {
3067            pane.add_item(d, false, false, Some(2), cx);
3068        });
3069        assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3070
3071        //   1c. Add at the end of the item list (including off the length)
3072        let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3073        pane.update(cx, |pane, cx| {
3074            pane.add_item(a, false, false, Some(5), cx);
3075        });
3076        assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3077
3078        //   1d. Add same item to active index
3079        let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3080        pane.update(cx, |pane, cx| {
3081            pane.add_item(b, false, false, Some(1), cx);
3082        });
3083        assert_item_labels(&pane, ["A", "B*", "C"], cx);
3084
3085        //   1e. Add item to index after same item in last position
3086        let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3087        pane.update(cx, |pane, cx| {
3088            pane.add_item(c, false, false, Some(2), cx);
3089        });
3090        assert_item_labels(&pane, ["A", "B", "C*"], cx);
3091
3092        // 2. Add without a destination index
3093        //   2a. Add with active item at the start of the item list
3094        let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
3095        pane.update(cx, |pane, cx| {
3096            pane.add_item(d, false, false, None, cx);
3097        });
3098        assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
3099
3100        //   2b. Add with active item at the end of the item list
3101        let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
3102        pane.update(cx, |pane, cx| {
3103            pane.add_item(a, false, false, None, cx);
3104        });
3105        assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3106
3107        //   2c. Add active item to active item at end of list
3108        let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
3109        pane.update(cx, |pane, cx| {
3110            pane.add_item(c, false, false, None, cx);
3111        });
3112        assert_item_labels(&pane, ["A", "B", "C*"], cx);
3113
3114        //   2d. Add active item to active item at start of list
3115        let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
3116        pane.update(cx, |pane, cx| {
3117            pane.add_item(a, false, false, None, cx);
3118        });
3119        assert_item_labels(&pane, ["A*", "B", "C"], cx);
3120    }
3121
3122    #[gpui::test]
3123    async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
3124        init_test(cx);
3125        let fs = FakeFs::new(cx.executor());
3126
3127        let project = Project::test(fs, None, cx).await;
3128        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3129        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3130
3131        // singleton view
3132        pane.update(cx, |pane, cx| {
3133            pane.add_item(
3134                Box::new(cx.new_view(|cx| {
3135                    TestItem::new(cx)
3136                        .with_singleton(true)
3137                        .with_label("buffer 1")
3138                        .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
3139                })),
3140                false,
3141                false,
3142                None,
3143                cx,
3144            );
3145        });
3146        assert_item_labels(&pane, ["buffer 1*"], cx);
3147
3148        // new singleton view with the same project entry
3149        pane.update(cx, |pane, cx| {
3150            pane.add_item(
3151                Box::new(cx.new_view(|cx| {
3152                    TestItem::new(cx)
3153                        .with_singleton(true)
3154                        .with_label("buffer 1")
3155                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3156                })),
3157                false,
3158                false,
3159                None,
3160                cx,
3161            );
3162        });
3163        assert_item_labels(&pane, ["buffer 1*"], cx);
3164
3165        // new singleton view with different project entry
3166        pane.update(cx, |pane, cx| {
3167            pane.add_item(
3168                Box::new(cx.new_view(|cx| {
3169                    TestItem::new(cx)
3170                        .with_singleton(true)
3171                        .with_label("buffer 2")
3172                        .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
3173                })),
3174                false,
3175                false,
3176                None,
3177                cx,
3178            );
3179        });
3180        assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
3181
3182        // new multibuffer view with the same project entry
3183        pane.update(cx, |pane, cx| {
3184            pane.add_item(
3185                Box::new(cx.new_view(|cx| {
3186                    TestItem::new(cx)
3187                        .with_singleton(false)
3188                        .with_label("multibuffer 1")
3189                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3190                })),
3191                false,
3192                false,
3193                None,
3194                cx,
3195            );
3196        });
3197        assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
3198
3199        // another multibuffer view with the same project entry
3200        pane.update(cx, |pane, cx| {
3201            pane.add_item(
3202                Box::new(cx.new_view(|cx| {
3203                    TestItem::new(cx)
3204                        .with_singleton(false)
3205                        .with_label("multibuffer 1b")
3206                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3207                })),
3208                false,
3209                false,
3210                None,
3211                cx,
3212            );
3213        });
3214        assert_item_labels(
3215            &pane,
3216            ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
3217            cx,
3218        );
3219    }
3220
3221    #[gpui::test]
3222    async fn test_remove_item_ordering(cx: &mut TestAppContext) {
3223        init_test(cx);
3224        let fs = FakeFs::new(cx.executor());
3225
3226        let project = Project::test(fs, None, cx).await;
3227        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3228        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3229
3230        add_labeled_item(&pane, "A", false, cx);
3231        add_labeled_item(&pane, "B", false, cx);
3232        add_labeled_item(&pane, "C", false, cx);
3233        add_labeled_item(&pane, "D", false, cx);
3234        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3235
3236        pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
3237        add_labeled_item(&pane, "1", false, cx);
3238        assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
3239
3240        pane.update(cx, |pane, cx| {
3241            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3242        })
3243        .unwrap()
3244        .await
3245        .unwrap();
3246        assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
3247
3248        pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
3249        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3250
3251        pane.update(cx, |pane, cx| {
3252            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3253        })
3254        .unwrap()
3255        .await
3256        .unwrap();
3257        assert_item_labels(&pane, ["A", "B*", "C"], cx);
3258
3259        pane.update(cx, |pane, cx| {
3260            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3261        })
3262        .unwrap()
3263        .await
3264        .unwrap();
3265        assert_item_labels(&pane, ["A", "C*"], cx);
3266
3267        pane.update(cx, |pane, cx| {
3268            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3269        })
3270        .unwrap()
3271        .await
3272        .unwrap();
3273        assert_item_labels(&pane, ["A*"], cx);
3274    }
3275
3276    #[gpui::test]
3277    async fn test_close_inactive_items(cx: &mut TestAppContext) {
3278        init_test(cx);
3279        let fs = FakeFs::new(cx.executor());
3280
3281        let project = Project::test(fs, None, cx).await;
3282        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3283        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3284
3285        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
3286
3287        pane.update(cx, |pane, cx| {
3288            pane.close_inactive_items(&CloseInactiveItems { save_intent: None }, cx)
3289        })
3290        .unwrap()
3291        .await
3292        .unwrap();
3293        assert_item_labels(&pane, ["C*"], cx);
3294    }
3295
3296    #[gpui::test]
3297    async fn test_close_clean_items(cx: &mut TestAppContext) {
3298        init_test(cx);
3299        let fs = FakeFs::new(cx.executor());
3300
3301        let project = Project::test(fs, None, cx).await;
3302        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3303        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3304
3305        add_labeled_item(&pane, "A", true, cx);
3306        add_labeled_item(&pane, "B", false, cx);
3307        add_labeled_item(&pane, "C", true, cx);
3308        add_labeled_item(&pane, "D", false, cx);
3309        add_labeled_item(&pane, "E", false, cx);
3310        assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
3311
3312        pane.update(cx, |pane, cx| pane.close_clean_items(&CloseCleanItems, cx))
3313            .unwrap()
3314            .await
3315            .unwrap();
3316        assert_item_labels(&pane, ["A^", "C*^"], cx);
3317    }
3318
3319    #[gpui::test]
3320    async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
3321        init_test(cx);
3322        let fs = FakeFs::new(cx.executor());
3323
3324        let project = Project::test(fs, None, cx).await;
3325        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3326        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3327
3328        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
3329
3330        pane.update(cx, |pane, cx| {
3331            pane.close_items_to_the_left(&CloseItemsToTheLeft, cx)
3332        })
3333        .unwrap()
3334        .await
3335        .unwrap();
3336        assert_item_labels(&pane, ["C*", "D", "E"], cx);
3337    }
3338
3339    #[gpui::test]
3340    async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
3341        init_test(cx);
3342        let fs = FakeFs::new(cx.executor());
3343
3344        let project = Project::test(fs, None, cx).await;
3345        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3346        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3347
3348        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
3349
3350        pane.update(cx, |pane, cx| {
3351            pane.close_items_to_the_right(&CloseItemsToTheRight, cx)
3352        })
3353        .unwrap()
3354        .await
3355        .unwrap();
3356        assert_item_labels(&pane, ["A", "B", "C*"], cx);
3357    }
3358
3359    #[gpui::test]
3360    async fn test_close_all_items(cx: &mut TestAppContext) {
3361        init_test(cx);
3362        let fs = FakeFs::new(cx.executor());
3363
3364        let project = Project::test(fs, None, cx).await;
3365        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3366        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3367
3368        add_labeled_item(&pane, "A", false, cx);
3369        add_labeled_item(&pane, "B", false, cx);
3370        add_labeled_item(&pane, "C", false, cx);
3371        assert_item_labels(&pane, ["A", "B", "C*"], cx);
3372
3373        pane.update(cx, |pane, cx| {
3374            pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
3375        })
3376        .unwrap()
3377        .await
3378        .unwrap();
3379        assert_item_labels(&pane, [], cx);
3380
3381        add_labeled_item(&pane, "A", true, cx);
3382        add_labeled_item(&pane, "B", true, cx);
3383        add_labeled_item(&pane, "C", true, cx);
3384        assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
3385
3386        let save = pane
3387            .update(cx, |pane, cx| {
3388                pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
3389            })
3390            .unwrap();
3391
3392        cx.executor().run_until_parked();
3393        cx.simulate_prompt_answer(2);
3394        save.await.unwrap();
3395        assert_item_labels(&pane, [], cx);
3396    }
3397
3398    fn init_test(cx: &mut TestAppContext) {
3399        cx.update(|cx| {
3400            let settings_store = SettingsStore::test(cx);
3401            cx.set_global(settings_store);
3402            theme::init(LoadThemes::JustBase, cx);
3403            crate::init_settings(cx);
3404            Project::init_settings(cx);
3405        });
3406    }
3407
3408    fn add_labeled_item(
3409        pane: &View<Pane>,
3410        label: &str,
3411        is_dirty: bool,
3412        cx: &mut VisualTestContext,
3413    ) -> Box<View<TestItem>> {
3414        pane.update(cx, |pane, cx| {
3415            let labeled_item = Box::new(
3416                cx.new_view(|cx| TestItem::new(cx).with_label(label).with_dirty(is_dirty)),
3417            );
3418            pane.add_item(labeled_item.clone(), false, false, None, cx);
3419            labeled_item
3420        })
3421    }
3422
3423    fn set_labeled_items<const COUNT: usize>(
3424        pane: &View<Pane>,
3425        labels: [&str; COUNT],
3426        cx: &mut VisualTestContext,
3427    ) -> [Box<View<TestItem>>; COUNT] {
3428        pane.update(cx, |pane, cx| {
3429            pane.items.clear();
3430            let mut active_item_index = 0;
3431
3432            let mut index = 0;
3433            let items = labels.map(|mut label| {
3434                if label.ends_with('*') {
3435                    label = label.trim_end_matches('*');
3436                    active_item_index = index;
3437                }
3438
3439                let labeled_item = Box::new(cx.new_view(|cx| TestItem::new(cx).with_label(label)));
3440                pane.add_item(labeled_item.clone(), false, false, None, cx);
3441                index += 1;
3442                labeled_item
3443            });
3444
3445            pane.activate_item(active_item_index, false, false, cx);
3446
3447            items
3448        })
3449    }
3450
3451    // Assert the item label, with the active item label suffixed with a '*'
3452    fn assert_item_labels<const COUNT: usize>(
3453        pane: &View<Pane>,
3454        expected_states: [&str; COUNT],
3455        cx: &mut VisualTestContext,
3456    ) {
3457        pane.update(cx, |pane, cx| {
3458            let actual_states = pane
3459                .items
3460                .iter()
3461                .enumerate()
3462                .map(|(ix, item)| {
3463                    let mut state = item
3464                        .to_any()
3465                        .downcast::<TestItem>()
3466                        .unwrap()
3467                        .read(cx)
3468                        .label
3469                        .clone();
3470                    if ix == pane.active_item_index {
3471                        state.push('*');
3472                    }
3473                    if item.is_dirty(cx) {
3474                        state.push('^');
3475                    }
3476                    state
3477                })
3478                .collect::<Vec<_>>();
3479
3480            assert_eq!(
3481                actual_states, expected_states,
3482                "pane items do not match expectation"
3483            );
3484        })
3485    }
3486}
3487
3488impl Render for DraggedTab {
3489    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3490        let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
3491        let label = self.item.tab_content(
3492            TabContentParams {
3493                detail: Some(self.detail),
3494                selected: false,
3495                preview: false,
3496            },
3497            cx,
3498        );
3499        Tab::new("")
3500            .selected(self.is_active)
3501            .child(label)
3502            .render(cx)
3503            .font(ui_font)
3504    }
3505}