pane.rs

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