pane.rs

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