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