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