pane.rs

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