pane.rs

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