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