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