pane.rs

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