pane.rs

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