pane.rs

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