pane.rs

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