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)]
  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        if save_intent == SaveIntent::Skip {
1545            return Ok(true);
1546        }
1547
1548        let (mut has_conflict, mut is_dirty, mut can_save, can_save_as) = cx.update(|cx| {
1549            (
1550                item.has_conflict(cx),
1551                item.is_dirty(cx),
1552                item.can_save(cx),
1553                item.is_singleton(cx),
1554            )
1555        })?;
1556
1557        // when saving a single buffer, we ignore whether or not it's dirty.
1558        if save_intent == SaveIntent::Save || save_intent == SaveIntent::SaveWithoutFormat {
1559            is_dirty = true;
1560        }
1561
1562        if save_intent == SaveIntent::SaveAs {
1563            is_dirty = true;
1564            has_conflict = false;
1565            can_save = false;
1566        }
1567
1568        if save_intent == SaveIntent::Overwrite {
1569            has_conflict = false;
1570        }
1571
1572        let should_format = save_intent != SaveIntent::SaveWithoutFormat;
1573
1574        if has_conflict && can_save {
1575            let answer = pane.update(cx, |pane, cx| {
1576                pane.activate_item(item_ix, true, true, cx);
1577                cx.prompt(
1578                    PromptLevel::Warning,
1579                    CONFLICT_MESSAGE,
1580                    None,
1581                    &["Overwrite", "Discard", "Cancel"],
1582                )
1583            })?;
1584            match answer.await {
1585                Ok(0) => {
1586                    pane.update(cx, |_, cx| item.save(should_format, project, cx))?
1587                        .await?
1588                }
1589                Ok(1) => pane.update(cx, |_, cx| item.reload(project, cx))?.await?,
1590                _ => return Ok(false),
1591            }
1592        } else if is_dirty && (can_save || can_save_as) {
1593            if save_intent == SaveIntent::Close {
1594                let will_autosave = cx.update(|cx| {
1595                    matches!(
1596                        item.workspace_settings(cx).autosave,
1597                        AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange
1598                    ) && Self::can_autosave_item(item, cx)
1599                })?;
1600                if !will_autosave {
1601                    let item_id = item.item_id();
1602                    let answer_task = pane.update(cx, |pane, cx| {
1603                        if pane.save_modals_spawned.insert(item_id) {
1604                            pane.activate_item(item_ix, true, true, cx);
1605                            let prompt = dirty_message_for(item.project_path(cx));
1606                            Some(cx.prompt(
1607                                PromptLevel::Warning,
1608                                &prompt,
1609                                None,
1610                                &["Save", "Don't Save", "Cancel"],
1611                            ))
1612                        } else {
1613                            None
1614                        }
1615                    })?;
1616                    if let Some(answer_task) = answer_task {
1617                        let answer = answer_task.await;
1618                        pane.update(cx, |pane, _| {
1619                            if !pane.save_modals_spawned.remove(&item_id) {
1620                                debug_panic!(
1621                                    "save modal was not present in spawned modals after awaiting for its answer"
1622                                )
1623                            }
1624                        })?;
1625                        match answer {
1626                            Ok(0) => {}
1627                            Ok(1) => {
1628                                // Don't save this file
1629                                pane.update(cx, |pane, cx| {
1630                                    if pane.is_tab_pinned(item_ix) && !item.can_save(cx) {
1631                                        pane.pinned_tab_count -= 1;
1632                                    }
1633                                    item.discarded(project, cx)
1634                                })
1635                                .log_err();
1636                                return Ok(true);
1637                            }
1638                            _ => return Ok(false), // Cancel
1639                        }
1640                    } else {
1641                        return Ok(false);
1642                    }
1643                }
1644            }
1645
1646            if can_save {
1647                pane.update(cx, |pane, cx| {
1648                    if pane.is_active_preview_item(item.item_id()) {
1649                        pane.set_preview_item_id(None, cx);
1650                    }
1651                    item.save(should_format, project, cx)
1652                })?
1653                .await?;
1654            } else if can_save_as {
1655                let abs_path = pane.update(cx, |pane, cx| {
1656                    pane.workspace
1657                        .update(cx, |workspace, cx| workspace.prompt_for_new_path(cx))
1658                })??;
1659                if let Some(abs_path) = abs_path.await.ok().flatten() {
1660                    pane.update(cx, |pane, cx| {
1661                        if let Some(item) = pane.item_for_path(abs_path.clone(), cx) {
1662                            if let Some(idx) = pane.index_for_item(&*item) {
1663                                pane.remove_item(idx, false, false, cx);
1664                            }
1665                        }
1666
1667                        item.save_as(project, abs_path, cx)
1668                    })?
1669                    .await?;
1670                } else {
1671                    return Ok(false);
1672                }
1673            }
1674        }
1675
1676        pane.update(cx, |_, cx| {
1677            cx.emit(Event::UserSavedItem {
1678                item: item.downgrade_item(),
1679                save_intent,
1680            });
1681            true
1682        })
1683    }
1684
1685    fn can_autosave_item(item: &dyn ItemHandle, cx: &AppContext) -> bool {
1686        let is_deleted = item.project_entry_ids(cx).is_empty();
1687        item.is_dirty(cx) && !item.has_conflict(cx) && item.can_save(cx) && !is_deleted
1688    }
1689
1690    pub fn autosave_item(
1691        item: &dyn ItemHandle,
1692        project: Model<Project>,
1693        cx: &mut WindowContext,
1694    ) -> Task<Result<()>> {
1695        let format = !matches!(
1696            item.workspace_settings(cx).autosave,
1697            AutosaveSetting::AfterDelay { .. }
1698        );
1699        if Self::can_autosave_item(item, cx) {
1700            item.save(format, project, cx)
1701        } else {
1702            Task::ready(Ok(()))
1703        }
1704    }
1705
1706    pub fn focus(&mut self, cx: &mut ViewContext<Pane>) {
1707        cx.focus(&self.focus_handle);
1708    }
1709
1710    pub fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
1711        if let Some(active_item) = self.active_item() {
1712            let focus_handle = active_item.focus_handle(cx);
1713            cx.focus(&focus_handle);
1714        }
1715    }
1716
1717    pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext<Self>) {
1718        cx.emit(Event::Split(direction));
1719    }
1720
1721    pub fn toolbar(&self) -> &View<Toolbar> {
1722        &self.toolbar
1723    }
1724
1725    pub fn handle_deleted_project_item(
1726        &mut self,
1727        entry_id: ProjectEntryId,
1728        cx: &mut ViewContext<Pane>,
1729    ) -> Option<()> {
1730        let (item_index_to_delete, item_id) = self.items().enumerate().find_map(|(i, item)| {
1731            if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
1732                Some((i, item.item_id()))
1733            } else {
1734                None
1735            }
1736        })?;
1737
1738        self.remove_item(item_index_to_delete, false, true, cx);
1739        self.nav_history.remove_item(item_id);
1740
1741        Some(())
1742    }
1743
1744    fn update_toolbar(&mut self, cx: &mut ViewContext<Self>) {
1745        let active_item = self
1746            .items
1747            .get(self.active_item_index)
1748            .map(|item| item.as_ref());
1749        self.toolbar.update(cx, |toolbar, cx| {
1750            toolbar.set_active_item(active_item, cx);
1751        });
1752    }
1753
1754    fn update_status_bar(&mut self, cx: &mut ViewContext<Self>) {
1755        let workspace = self.workspace.clone();
1756        let pane = cx.view().clone();
1757
1758        cx.window_context().defer(move |cx| {
1759            let Ok(status_bar) = workspace.update(cx, |workspace, _| workspace.status_bar.clone())
1760            else {
1761                return;
1762            };
1763
1764            status_bar.update(cx, move |status_bar, cx| {
1765                status_bar.set_active_pane(&pane, cx);
1766            });
1767        });
1768    }
1769
1770    fn entry_abs_path(&self, entry: ProjectEntryId, cx: &WindowContext) -> Option<PathBuf> {
1771        let worktree = self
1772            .workspace
1773            .upgrade()?
1774            .read(cx)
1775            .project()
1776            .read(cx)
1777            .worktree_for_entry(entry, cx)?
1778            .read(cx);
1779        let entry = worktree.entry_for_id(entry)?;
1780        match &entry.canonical_path {
1781            Some(canonical_path) => Some(canonical_path.to_path_buf()),
1782            None => worktree.absolutize(&entry.path).ok(),
1783        }
1784    }
1785
1786    pub fn icon_color(selected: bool) -> Color {
1787        if selected {
1788            Color::Default
1789        } else {
1790            Color::Muted
1791        }
1792    }
1793
1794    pub fn git_aware_icon_color(
1795        git_status: Option<GitFileStatus>,
1796        ignored: bool,
1797        selected: bool,
1798    ) -> Color {
1799        if ignored {
1800            Color::Ignored
1801        } else {
1802            match git_status {
1803                Some(GitFileStatus::Added) => Color::Created,
1804                Some(GitFileStatus::Modified) => Color::Modified,
1805                Some(GitFileStatus::Conflict) => Color::Conflict,
1806                None => Self::icon_color(selected),
1807            }
1808        }
1809    }
1810
1811    fn toggle_pin_tab(&mut self, _: &TogglePinTab, cx: &mut ViewContext<'_, Self>) {
1812        if self.items.is_empty() {
1813            return;
1814        }
1815        let active_tab_ix = self.active_item_index();
1816        if self.is_tab_pinned(active_tab_ix) {
1817            self.unpin_tab_at(active_tab_ix, cx);
1818        } else {
1819            self.pin_tab_at(active_tab_ix, cx);
1820        }
1821    }
1822
1823    fn pin_tab_at(&mut self, ix: usize, cx: &mut ViewContext<'_, Self>) {
1824        maybe!({
1825            let pane = cx.view().clone();
1826            let destination_index = self.pinned_tab_count.min(ix);
1827            self.pinned_tab_count += 1;
1828            let id = self.item_for_index(ix)?.item_id();
1829
1830            self.workspace
1831                .update(cx, |_, cx| {
1832                    cx.defer(move |_, cx| move_item(&pane, &pane, id, destination_index, cx));
1833                })
1834                .ok()?;
1835
1836            Some(())
1837        });
1838    }
1839
1840    fn unpin_tab_at(&mut self, ix: usize, cx: &mut ViewContext<'_, Self>) {
1841        maybe!({
1842            let pane = cx.view().clone();
1843            self.pinned_tab_count = self.pinned_tab_count.checked_sub(1).unwrap();
1844            let destination_index = self.pinned_tab_count;
1845
1846            let id = self.item_for_index(ix)?.item_id();
1847
1848            self.workspace
1849                .update(cx, |_, cx| {
1850                    cx.defer(move |_, cx| move_item(&pane, &pane, id, destination_index, cx));
1851                })
1852                .ok()?;
1853
1854            Some(())
1855        });
1856    }
1857
1858    fn is_tab_pinned(&self, ix: usize) -> bool {
1859        self.pinned_tab_count > ix
1860    }
1861
1862    fn has_pinned_tabs(&self) -> bool {
1863        self.pinned_tab_count != 0
1864    }
1865
1866    fn render_tab(
1867        &self,
1868        ix: usize,
1869        item: &dyn ItemHandle,
1870        detail: usize,
1871        focus_handle: &FocusHandle,
1872        cx: &mut ViewContext<'_, Pane>,
1873    ) -> impl IntoElement {
1874        let project_path = item.project_path(cx);
1875
1876        let is_active = ix == self.active_item_index;
1877        let is_preview = self
1878            .preview_item_id
1879            .map(|id| id == item.item_id())
1880            .unwrap_or(false);
1881
1882        let label = item.tab_content(
1883            TabContentParams {
1884                detail: Some(detail),
1885                selected: is_active,
1886                preview: is_preview,
1887            },
1888            cx,
1889        );
1890
1891        let icon_color = if ItemSettings::get_global(cx).git_status {
1892            project_path
1893                .as_ref()
1894                .and_then(|path| self.project.read(cx).entry_for_path(path, cx))
1895                .map(|entry| {
1896                    Self::git_aware_icon_color(entry.git_status, entry.is_ignored, is_active)
1897                })
1898                .unwrap_or_else(|| Self::icon_color(is_active))
1899        } else {
1900            Self::icon_color(is_active)
1901        };
1902
1903        let icon = item.tab_icon(cx);
1904        let close_side = &ItemSettings::get_global(cx).close_position;
1905        let indicator = render_item_indicator(item.boxed_clone(), cx);
1906        let item_id = item.item_id();
1907        let is_first_item = ix == 0;
1908        let is_last_item = ix == self.items.len() - 1;
1909        let is_pinned = self.is_tab_pinned(ix);
1910        let position_relative_to_active_item = ix.cmp(&self.active_item_index);
1911
1912        let tab = Tab::new(ix)
1913            .position(if is_first_item {
1914                TabPosition::First
1915            } else if is_last_item {
1916                TabPosition::Last
1917            } else {
1918                TabPosition::Middle(position_relative_to_active_item)
1919            })
1920            .close_side(match close_side {
1921                ClosePosition::Left => ui::TabCloseSide::Start,
1922                ClosePosition::Right => ui::TabCloseSide::End,
1923            })
1924            .selected(is_active)
1925            .on_click(
1926                cx.listener(move |pane: &mut Self, _, cx| pane.activate_item(ix, true, true, cx)),
1927            )
1928            // TODO: This should be a click listener with the middle mouse button instead of a mouse down listener.
1929            .on_mouse_down(
1930                MouseButton::Middle,
1931                cx.listener(move |pane, _event, cx| {
1932                    pane.close_item_by_id(item_id, SaveIntent::Close, cx)
1933                        .detach_and_log_err(cx);
1934                }),
1935            )
1936            .on_mouse_down(
1937                MouseButton::Left,
1938                cx.listener(move |pane, event: &MouseDownEvent, cx| {
1939                    if let Some(id) = pane.preview_item_id {
1940                        if id == item_id && event.click_count > 1 {
1941                            pane.set_preview_item_id(None, cx);
1942                        }
1943                    }
1944                }),
1945            )
1946            .on_drag(
1947                DraggedTab {
1948                    item: item.boxed_clone(),
1949                    pane: cx.view().clone(),
1950                    detail,
1951                    is_active,
1952                    ix,
1953                },
1954                |tab, _, cx| cx.new_view(|_| tab.clone()),
1955            )
1956            .drag_over::<DraggedTab>(|tab, _, cx| {
1957                tab.bg(cx.theme().colors().drop_target_background)
1958            })
1959            .drag_over::<DraggedSelection>(|tab, _, cx| {
1960                tab.bg(cx.theme().colors().drop_target_background)
1961            })
1962            .when_some(self.can_drop_predicate.clone(), |this, p| {
1963                this.can_drop(move |a, cx| p(a, cx))
1964            })
1965            .on_drop(cx.listener(move |this, dragged_tab: &DraggedTab, cx| {
1966                this.drag_split_direction = None;
1967                this.handle_tab_drop(dragged_tab, ix, cx)
1968            }))
1969            .on_drop(cx.listener(move |this, selection: &DraggedSelection, cx| {
1970                this.drag_split_direction = None;
1971                this.handle_dragged_selection_drop(selection, Some(ix), cx)
1972            }))
1973            .on_drop(cx.listener(move |this, paths, cx| {
1974                this.drag_split_direction = None;
1975                this.handle_external_paths_drop(paths, cx)
1976            }))
1977            .when_some(item.tab_tooltip_text(cx), |tab, text| {
1978                tab.tooltip(move |cx| Tooltip::text(text.clone(), cx))
1979            })
1980            .start_slot::<Indicator>(indicator)
1981            .map(|this| {
1982                let end_slot_action: &'static dyn Action;
1983                let end_slot_tooltip_text: &'static str;
1984                let end_slot = if is_pinned {
1985                    end_slot_action = &TogglePinTab;
1986                    end_slot_tooltip_text = "Unpin Tab";
1987                    IconButton::new("unpin tab", IconName::Pin)
1988                        .shape(IconButtonShape::Square)
1989                        .icon_color(Color::Muted)
1990                        .size(ButtonSize::None)
1991                        .icon_size(IconSize::XSmall)
1992                        .on_click(cx.listener(move |pane, _, cx| {
1993                            pane.unpin_tab_at(ix, cx);
1994                        }))
1995                } else {
1996                    end_slot_action = &CloseActiveItem { save_intent: None };
1997                    end_slot_tooltip_text = "Close Tab";
1998                    IconButton::new("close tab", IconName::Close)
1999                        .visible_on_hover("")
2000                        .shape(IconButtonShape::Square)
2001                        .icon_color(Color::Muted)
2002                        .size(ButtonSize::None)
2003                        .icon_size(IconSize::XSmall)
2004                        .on_click(cx.listener(move |pane, _, cx| {
2005                            pane.close_item_by_id(item_id, SaveIntent::Close, cx)
2006                                .detach_and_log_err(cx);
2007                        }))
2008                }
2009                .map(|this| {
2010                    if is_active {
2011                        let focus_handle = focus_handle.clone();
2012                        this.tooltip(move |cx| {
2013                            Tooltip::for_action_in(
2014                                end_slot_tooltip_text,
2015                                end_slot_action,
2016                                &focus_handle,
2017                                cx,
2018                            )
2019                        })
2020                    } else {
2021                        this.tooltip(move |cx| Tooltip::text(end_slot_tooltip_text, cx))
2022                    }
2023                });
2024                this.end_slot(end_slot)
2025            })
2026            .child(
2027                h_flex()
2028                    .gap_1()
2029                    .children(icon.map(|icon| icon.size(IconSize::Small).color(icon_color)))
2030                    .child(label),
2031            );
2032
2033        let single_entry_to_resolve = {
2034            let item_entries = self.items[ix].project_entry_ids(cx);
2035            if item_entries.len() == 1 {
2036                Some(item_entries[0])
2037            } else {
2038                None
2039            }
2040        };
2041
2042        let is_pinned = self.is_tab_pinned(ix);
2043        let pane = cx.view().downgrade();
2044        right_click_menu(ix).trigger(tab).menu(move |cx| {
2045            let pane = pane.clone();
2046            ContextMenu::build(cx, move |mut menu, cx| {
2047                if let Some(pane) = pane.upgrade() {
2048                    menu = menu
2049                        .entry(
2050                            "Close",
2051                            Some(Box::new(CloseActiveItem { save_intent: None })),
2052                            cx.handler_for(&pane, move |pane, cx| {
2053                                pane.close_item_by_id(item_id, SaveIntent::Close, cx)
2054                                    .detach_and_log_err(cx);
2055                            }),
2056                        )
2057                        .entry(
2058                            "Close Others",
2059                            Some(Box::new(CloseInactiveItems {
2060                                save_intent: None,
2061                                close_pinned: false,
2062                            })),
2063                            cx.handler_for(&pane, move |pane, cx| {
2064                                pane.close_items(cx, SaveIntent::Close, |id| id != item_id)
2065                                    .detach_and_log_err(cx);
2066                            }),
2067                        )
2068                        .separator()
2069                        .entry(
2070                            "Close Left",
2071                            Some(Box::new(CloseItemsToTheLeft {
2072                                close_pinned: false,
2073                            })),
2074                            cx.handler_for(&pane, move |pane, cx| {
2075                                pane.close_items_to_the_left_by_id(
2076                                    item_id,
2077                                    pane.get_non_closeable_item_ids(false),
2078                                    cx,
2079                                )
2080                                .detach_and_log_err(cx);
2081                            }),
2082                        )
2083                        .entry(
2084                            "Close Right",
2085                            Some(Box::new(CloseItemsToTheRight {
2086                                close_pinned: false,
2087                            })),
2088                            cx.handler_for(&pane, move |pane, cx| {
2089                                pane.close_items_to_the_right_by_id(
2090                                    item_id,
2091                                    pane.get_non_closeable_item_ids(false),
2092                                    cx,
2093                                )
2094                                .detach_and_log_err(cx);
2095                            }),
2096                        )
2097                        .separator()
2098                        .entry(
2099                            "Close Clean",
2100                            Some(Box::new(CloseCleanItems {
2101                                close_pinned: false,
2102                            })),
2103                            cx.handler_for(&pane, move |pane, cx| {
2104                                if let Some(task) = pane.close_clean_items(
2105                                    &CloseCleanItems {
2106                                        close_pinned: false,
2107                                    },
2108                                    cx,
2109                                ) {
2110                                    task.detach_and_log_err(cx)
2111                                }
2112                            }),
2113                        )
2114                        .entry(
2115                            "Close All",
2116                            Some(Box::new(CloseAllItems {
2117                                save_intent: None,
2118                                close_pinned: false,
2119                            })),
2120                            cx.handler_for(&pane, |pane, cx| {
2121                                if let Some(task) = pane.close_all_items(
2122                                    &CloseAllItems {
2123                                        save_intent: None,
2124                                        close_pinned: false,
2125                                    },
2126                                    cx,
2127                                ) {
2128                                    task.detach_and_log_err(cx)
2129                                }
2130                            }),
2131                        );
2132
2133                    let pin_tab_entries = |menu: ContextMenu| {
2134                        menu.separator().map(|this| {
2135                            if is_pinned {
2136                                this.entry(
2137                                    "Unpin Tab",
2138                                    Some(TogglePinTab.boxed_clone()),
2139                                    cx.handler_for(&pane, move |pane, cx| {
2140                                        pane.unpin_tab_at(ix, cx);
2141                                    }),
2142                                )
2143                            } else {
2144                                this.entry(
2145                                    "Pin Tab",
2146                                    Some(TogglePinTab.boxed_clone()),
2147                                    cx.handler_for(&pane, move |pane, cx| {
2148                                        pane.pin_tab_at(ix, cx);
2149                                    }),
2150                                )
2151                            }
2152                        })
2153                    };
2154                    if let Some(entry) = single_entry_to_resolve {
2155                        let entry_abs_path = pane.read(cx).entry_abs_path(entry, cx);
2156                        let parent_abs_path = entry_abs_path
2157                            .as_deref()
2158                            .and_then(|abs_path| Some(abs_path.parent()?.to_path_buf()));
2159                        let relative_path = pane
2160                            .read(cx)
2161                            .item_for_entry(entry, cx)
2162                            .and_then(|item| item.project_path(cx))
2163                            .map(|project_path| project_path.path);
2164
2165                        let entry_id = entry.to_proto();
2166                        menu = menu
2167                            .separator()
2168                            .when_some(entry_abs_path, |menu, abs_path| {
2169                                menu.entry(
2170                                    "Copy Path",
2171                                    Some(Box::new(CopyPath)),
2172                                    cx.handler_for(&pane, move |_, cx| {
2173                                        cx.write_to_clipboard(ClipboardItem::new_string(
2174                                            abs_path.to_string_lossy().to_string(),
2175                                        ));
2176                                    }),
2177                                )
2178                            })
2179                            .when_some(relative_path, |menu, relative_path| {
2180                                menu.entry(
2181                                    "Copy Relative Path",
2182                                    Some(Box::new(CopyRelativePath)),
2183                                    cx.handler_for(&pane, move |_, cx| {
2184                                        cx.write_to_clipboard(ClipboardItem::new_string(
2185                                            relative_path.to_string_lossy().to_string(),
2186                                        ));
2187                                    }),
2188                                )
2189                            })
2190                            .map(pin_tab_entries)
2191                            .separator()
2192                            .entry(
2193                                "Reveal In Project Panel",
2194                                Some(Box::new(RevealInProjectPanel {
2195                                    entry_id: Some(entry_id),
2196                                })),
2197                                cx.handler_for(&pane, move |pane, cx| {
2198                                    pane.project.update(cx, |_, cx| {
2199                                        cx.emit(project::Event::RevealInProjectPanel(
2200                                            ProjectEntryId::from_proto(entry_id),
2201                                        ))
2202                                    });
2203                                }),
2204                            )
2205                            .when_some(parent_abs_path, |menu, parent_abs_path| {
2206                                menu.entry(
2207                                    "Open in Terminal",
2208                                    Some(Box::new(OpenInTerminal)),
2209                                    cx.handler_for(&pane, move |_, cx| {
2210                                        cx.dispatch_action(
2211                                            OpenTerminal {
2212                                                working_directory: parent_abs_path.clone(),
2213                                            }
2214                                            .boxed_clone(),
2215                                        );
2216                                    }),
2217                                )
2218                            });
2219                    } else {
2220                        menu = menu.map(pin_tab_entries);
2221                    }
2222                }
2223
2224                menu
2225            })
2226        })
2227    }
2228
2229    fn render_tab_bar(&mut self, cx: &mut ViewContext<'_, Pane>) -> impl IntoElement {
2230        let focus_handle = self.focus_handle.clone();
2231        let navigate_backward = IconButton::new("navigate_backward", IconName::ArrowLeft)
2232            .shape(IconButtonShape::Square)
2233            .icon_size(IconSize::Small)
2234            .on_click({
2235                let view = cx.view().clone();
2236                move |_, cx| view.update(cx, Self::navigate_backward)
2237            })
2238            .disabled(!self.can_navigate_backward())
2239            .tooltip({
2240                let focus_handle = focus_handle.clone();
2241                move |cx| Tooltip::for_action_in("Go Back", &GoBack, &focus_handle, cx)
2242            });
2243
2244        let navigate_forward = IconButton::new("navigate_forward", IconName::ArrowRight)
2245            .shape(IconButtonShape::Square)
2246            .icon_size(IconSize::Small)
2247            .on_click({
2248                let view = cx.view().clone();
2249                move |_, cx| view.update(cx, Self::navigate_forward)
2250            })
2251            .disabled(!self.can_navigate_forward())
2252            .tooltip({
2253                let focus_handle = focus_handle.clone();
2254                move |cx| Tooltip::for_action_in("Go Forward", &GoForward, &focus_handle, cx)
2255            });
2256
2257        let mut tab_items = self
2258            .items
2259            .iter()
2260            .enumerate()
2261            .zip(tab_details(&self.items, cx))
2262            .map(|((ix, item), detail)| self.render_tab(ix, &**item, detail, &focus_handle, cx))
2263            .collect::<Vec<_>>();
2264        let tab_count = tab_items.len();
2265        let unpinned_tabs = tab_items.split_off(self.pinned_tab_count);
2266        let pinned_tabs = tab_items;
2267        TabBar::new("tab_bar")
2268            .when(
2269                self.display_nav_history_buttons.unwrap_or_default(),
2270                |tab_bar| {
2271                    tab_bar
2272                        .start_child(navigate_backward)
2273                        .start_child(navigate_forward)
2274                },
2275            )
2276            .map(|tab_bar| {
2277                let render_tab_buttons = self.render_tab_bar_buttons.clone();
2278                let (left_children, right_children) = render_tab_buttons(self, cx);
2279
2280                tab_bar
2281                    .start_children(left_children)
2282                    .end_children(right_children)
2283            })
2284            .children(pinned_tabs.len().ne(&0).then(|| {
2285                h_flex()
2286                    .children(pinned_tabs)
2287                    .border_r_2()
2288                    .border_color(cx.theme().colors().border)
2289            }))
2290            .child(
2291                h_flex()
2292                    .id("unpinned tabs")
2293                    .overflow_x_scroll()
2294                    .w_full()
2295                    .track_scroll(&self.tab_bar_scroll_handle)
2296                    .children(unpinned_tabs)
2297                    .child(
2298                        div()
2299                            .id("tab_bar_drop_target")
2300                            .min_w_6()
2301                            // HACK: This empty child is currently necessary to force the drop target to appear
2302                            // despite us setting a min width above.
2303                            .child("")
2304                            .h_full()
2305                            .flex_grow()
2306                            .drag_over::<DraggedTab>(|bar, _, cx| {
2307                                bar.bg(cx.theme().colors().drop_target_background)
2308                            })
2309                            .drag_over::<DraggedSelection>(|bar, _, cx| {
2310                                bar.bg(cx.theme().colors().drop_target_background)
2311                            })
2312                            .on_drop(cx.listener(move |this, dragged_tab: &DraggedTab, cx| {
2313                                this.drag_split_direction = None;
2314                                this.handle_tab_drop(dragged_tab, this.items.len(), cx)
2315                            }))
2316                            .on_drop(cx.listener(move |this, selection: &DraggedSelection, cx| {
2317                                this.drag_split_direction = None;
2318                                this.handle_project_entry_drop(
2319                                    &selection.active_selection.entry_id,
2320                                    Some(tab_count),
2321                                    cx,
2322                                )
2323                            }))
2324                            .on_drop(cx.listener(move |this, paths, cx| {
2325                                this.drag_split_direction = None;
2326                                this.handle_external_paths_drop(paths, cx)
2327                            }))
2328                            .on_click(cx.listener(move |this, event: &ClickEvent, cx| {
2329                                if event.up.click_count == 2 {
2330                                    cx.dispatch_action(
2331                                        this.double_click_dispatch_action.boxed_clone(),
2332                                    )
2333                                }
2334                            })),
2335                    ),
2336            )
2337    }
2338
2339    pub fn render_menu_overlay(menu: &View<ContextMenu>) -> Div {
2340        div().absolute().bottom_0().right_0().size_0().child(
2341            deferred(
2342                anchored()
2343                    .anchor(AnchorCorner::TopRight)
2344                    .child(menu.clone()),
2345            )
2346            .with_priority(1),
2347        )
2348    }
2349
2350    pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
2351        self.zoomed = zoomed;
2352        cx.notify();
2353    }
2354
2355    pub fn is_zoomed(&self) -> bool {
2356        self.zoomed
2357    }
2358
2359    fn handle_drag_move<T>(&mut self, event: &DragMoveEvent<T>, cx: &mut ViewContext<Self>) {
2360        if !self.can_split {
2361            return;
2362        }
2363
2364        let rect = event.bounds.size;
2365
2366        let size = event.bounds.size.width.min(event.bounds.size.height)
2367            * WorkspaceSettings::get_global(cx).drop_target_size;
2368
2369        let relative_cursor = Point::new(
2370            event.event.position.x - event.bounds.left(),
2371            event.event.position.y - event.bounds.top(),
2372        );
2373
2374        let direction = if relative_cursor.x < size
2375            || relative_cursor.x > rect.width - size
2376            || relative_cursor.y < size
2377            || relative_cursor.y > rect.height - size
2378        {
2379            [
2380                SplitDirection::Up,
2381                SplitDirection::Right,
2382                SplitDirection::Down,
2383                SplitDirection::Left,
2384            ]
2385            .iter()
2386            .min_by_key(|side| match side {
2387                SplitDirection::Up => relative_cursor.y,
2388                SplitDirection::Right => rect.width - relative_cursor.x,
2389                SplitDirection::Down => rect.height - relative_cursor.y,
2390                SplitDirection::Left => relative_cursor.x,
2391            })
2392            .cloned()
2393        } else {
2394            None
2395        };
2396
2397        if direction != self.drag_split_direction {
2398            self.drag_split_direction = direction;
2399        }
2400    }
2401
2402    fn handle_tab_drop(
2403        &mut self,
2404        dragged_tab: &DraggedTab,
2405        ix: usize,
2406        cx: &mut ViewContext<'_, Self>,
2407    ) {
2408        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2409            if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_tab, cx) {
2410                return;
2411            }
2412        }
2413        let mut to_pane = cx.view().clone();
2414        let split_direction = self.drag_split_direction;
2415        let item_id = dragged_tab.item.item_id();
2416        if let Some(preview_item_id) = self.preview_item_id {
2417            if item_id == preview_item_id {
2418                self.set_preview_item_id(None, cx);
2419            }
2420        }
2421
2422        let from_pane = dragged_tab.pane.clone();
2423        self.workspace
2424            .update(cx, |_, cx| {
2425                cx.defer(move |workspace, cx| {
2426                    if let Some(split_direction) = split_direction {
2427                        to_pane = workspace.split_pane(to_pane, split_direction, cx);
2428                    }
2429                    let old_ix = from_pane.read(cx).index_for_item_id(item_id);
2430                    if to_pane == from_pane {
2431                        if let Some(old_index) = old_ix {
2432                            to_pane.update(cx, |this, _| {
2433                                if old_index < this.pinned_tab_count
2434                                    && (ix == this.items.len() || ix > this.pinned_tab_count)
2435                                {
2436                                    this.pinned_tab_count -= 1;
2437                                } else if this.has_pinned_tabs()
2438                                    && old_index >= this.pinned_tab_count
2439                                    && ix < this.pinned_tab_count
2440                                {
2441                                    this.pinned_tab_count += 1;
2442                                }
2443                            });
2444                        }
2445                    } else {
2446                        to_pane.update(cx, |this, _| {
2447                            if this.has_pinned_tabs() && ix < this.pinned_tab_count {
2448                                this.pinned_tab_count += 1;
2449                            }
2450                        });
2451                        from_pane.update(cx, |this, _| {
2452                            if let Some(index) = old_ix {
2453                                if this.pinned_tab_count > index {
2454                                    this.pinned_tab_count -= 1;
2455                                }
2456                            }
2457                        })
2458                    }
2459                    move_item(&from_pane, &to_pane, item_id, ix, cx);
2460                });
2461            })
2462            .log_err();
2463    }
2464
2465    fn handle_dragged_selection_drop(
2466        &mut self,
2467        dragged_selection: &DraggedSelection,
2468        dragged_onto: Option<usize>,
2469        cx: &mut ViewContext<'_, Self>,
2470    ) {
2471        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2472            if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_selection, cx) {
2473                return;
2474            }
2475        }
2476        self.handle_project_entry_drop(
2477            &dragged_selection.active_selection.entry_id,
2478            dragged_onto,
2479            cx,
2480        );
2481    }
2482
2483    fn handle_project_entry_drop(
2484        &mut self,
2485        project_entry_id: &ProjectEntryId,
2486        target: Option<usize>,
2487        cx: &mut ViewContext<'_, Self>,
2488    ) {
2489        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2490            if let ControlFlow::Break(()) = custom_drop_handle(self, project_entry_id, cx) {
2491                return;
2492            }
2493        }
2494        let mut to_pane = cx.view().clone();
2495        let split_direction = self.drag_split_direction;
2496        let project_entry_id = *project_entry_id;
2497        self.workspace
2498            .update(cx, |_, cx| {
2499                cx.defer(move |workspace, cx| {
2500                    if let Some(path) = workspace
2501                        .project()
2502                        .read(cx)
2503                        .path_for_entry(project_entry_id, cx)
2504                    {
2505                        let load_path_task = workspace.load_path(path, cx);
2506                        cx.spawn(|workspace, mut cx| async move {
2507                            if let Some((project_entry_id, build_item)) =
2508                                load_path_task.await.notify_async_err(&mut cx)
2509                            {
2510                                let (to_pane, new_item_handle) = workspace
2511                                    .update(&mut cx, |workspace, cx| {
2512                                        if let Some(split_direction) = split_direction {
2513                                            to_pane =
2514                                                workspace.split_pane(to_pane, split_direction, cx);
2515                                        }
2516                                        let new_item_handle = to_pane.update(cx, |pane, cx| {
2517                                            pane.open_item(
2518                                                project_entry_id,
2519                                                true,
2520                                                false,
2521                                                target,
2522                                                cx,
2523                                                build_item,
2524                                            )
2525                                        });
2526                                        (to_pane, new_item_handle)
2527                                    })
2528                                    .log_err()?;
2529                                to_pane
2530                                    .update(&mut cx, |this, cx| {
2531                                        let Some(index) = this.index_for_item(&*new_item_handle)
2532                                        else {
2533                                            return;
2534                                        };
2535
2536                                        if target.map_or(false, |target| this.is_tab_pinned(target))
2537                                        {
2538                                            this.pin_tab_at(index, cx);
2539                                        }
2540                                    })
2541                                    .ok()?
2542                            }
2543                            Some(())
2544                        })
2545                        .detach();
2546                    };
2547                });
2548            })
2549            .log_err();
2550    }
2551
2552    fn handle_external_paths_drop(
2553        &mut self,
2554        paths: &ExternalPaths,
2555        cx: &mut ViewContext<'_, Self>,
2556    ) {
2557        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2558            if let ControlFlow::Break(()) = custom_drop_handle(self, paths, cx) {
2559                return;
2560            }
2561        }
2562        let mut to_pane = cx.view().clone();
2563        let mut split_direction = self.drag_split_direction;
2564        let paths = paths.paths().to_vec();
2565        let is_remote = self
2566            .workspace
2567            .update(cx, |workspace, cx| {
2568                if workspace.project().read(cx).is_via_collab() {
2569                    workspace.show_error(
2570                        &anyhow::anyhow!("Cannot drop files on a remote project"),
2571                        cx,
2572                    );
2573                    true
2574                } else {
2575                    false
2576                }
2577            })
2578            .unwrap_or(true);
2579        if is_remote {
2580            return;
2581        }
2582
2583        self.workspace
2584            .update(cx, |workspace, cx| {
2585                let fs = Arc::clone(workspace.project().read(cx).fs());
2586                cx.spawn(|workspace, mut cx| async move {
2587                    let mut is_file_checks = FuturesUnordered::new();
2588                    for path in &paths {
2589                        is_file_checks.push(fs.is_file(path))
2590                    }
2591                    let mut has_files_to_open = false;
2592                    while let Some(is_file) = is_file_checks.next().await {
2593                        if is_file {
2594                            has_files_to_open = true;
2595                            break;
2596                        }
2597                    }
2598                    drop(is_file_checks);
2599                    if !has_files_to_open {
2600                        split_direction = None;
2601                    }
2602
2603                    if let Ok(open_task) = workspace.update(&mut cx, |workspace, cx| {
2604                        if let Some(split_direction) = split_direction {
2605                            to_pane = workspace.split_pane(to_pane, split_direction, cx);
2606                        }
2607                        workspace.open_paths(
2608                            paths,
2609                            OpenVisible::OnlyDirectories,
2610                            Some(to_pane.downgrade()),
2611                            cx,
2612                        )
2613                    }) {
2614                        let opened_items: Vec<_> = open_task.await;
2615                        _ = workspace.update(&mut cx, |workspace, cx| {
2616                            for item in opened_items.into_iter().flatten() {
2617                                if let Err(e) = item {
2618                                    workspace.show_error(&e, cx);
2619                                }
2620                            }
2621                        });
2622                    }
2623                })
2624                .detach();
2625            })
2626            .log_err();
2627    }
2628
2629    pub fn display_nav_history_buttons(&mut self, display: Option<bool>) {
2630        self.display_nav_history_buttons = display;
2631    }
2632
2633    fn get_non_closeable_item_ids(&self, close_pinned: bool) -> Vec<EntityId> {
2634        if close_pinned {
2635            return vec![];
2636        }
2637
2638        self.items
2639            .iter()
2640            .map(|item| item.item_id())
2641            .filter(|item_id| {
2642                if let Some(ix) = self.index_for_item_id(*item_id) {
2643                    self.is_tab_pinned(ix)
2644                } else {
2645                    true
2646                }
2647            })
2648            .collect()
2649    }
2650}
2651
2652impl FocusableView for Pane {
2653    fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
2654        self.focus_handle.clone()
2655    }
2656}
2657
2658impl Render for Pane {
2659    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2660        let mut key_context = KeyContext::new_with_defaults();
2661        key_context.add("Pane");
2662        if self.active_item().is_none() {
2663            key_context.add("EmptyPane");
2664        }
2665
2666        let should_display_tab_bar = self.should_display_tab_bar.clone();
2667        let display_tab_bar = should_display_tab_bar(cx);
2668        let is_local = self.project.read(cx).is_local();
2669
2670        v_flex()
2671            .key_context(key_context)
2672            .track_focus(&self.focus_handle(cx))
2673            .size_full()
2674            .flex_none()
2675            .overflow_hidden()
2676            .on_action(cx.listener(|pane, _: &AlternateFile, cx| {
2677                pane.alternate_file(cx);
2678            }))
2679            .on_action(cx.listener(|pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx)))
2680            .on_action(cx.listener(|pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx)))
2681            .on_action(cx.listener(|pane, _: &SplitHorizontal, cx| {
2682                pane.split(SplitDirection::horizontal(cx), cx)
2683            }))
2684            .on_action(cx.listener(|pane, _: &SplitVertical, cx| {
2685                pane.split(SplitDirection::vertical(cx), cx)
2686            }))
2687            .on_action(
2688                cx.listener(|pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx)),
2689            )
2690            .on_action(cx.listener(|pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx)))
2691            .on_action(cx.listener(|pane, _: &GoBack, cx| pane.navigate_backward(cx)))
2692            .on_action(cx.listener(|pane, _: &GoForward, cx| pane.navigate_forward(cx)))
2693            .on_action(cx.listener(|pane, _: &JoinIntoNext, cx| pane.join_into_next(cx)))
2694            .on_action(cx.listener(|pane, _: &JoinAll, cx| pane.join_all(cx)))
2695            .on_action(cx.listener(Pane::toggle_zoom))
2696            .on_action(cx.listener(|pane: &mut Pane, action: &ActivateItem, cx| {
2697                pane.activate_item(action.0, true, true, cx);
2698            }))
2699            .on_action(cx.listener(|pane: &mut Pane, _: &ActivateLastItem, cx| {
2700                pane.activate_item(pane.items.len() - 1, true, true, cx);
2701            }))
2702            .on_action(cx.listener(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
2703                pane.activate_prev_item(true, cx);
2704            }))
2705            .on_action(cx.listener(|pane: &mut Pane, _: &ActivateNextItem, cx| {
2706                pane.activate_next_item(true, cx);
2707            }))
2708            .on_action(cx.listener(|pane, _: &SwapItemLeft, cx| pane.swap_item_left(cx)))
2709            .on_action(cx.listener(|pane, _: &SwapItemRight, cx| pane.swap_item_right(cx)))
2710            .on_action(cx.listener(|pane, action, cx| {
2711                pane.toggle_pin_tab(action, cx);
2712            }))
2713            .when(PreviewTabsSettings::get_global(cx).enabled, |this| {
2714                this.on_action(cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, cx| {
2715                    if let Some(active_item_id) = pane.active_item().map(|i| i.item_id()) {
2716                        if pane.is_active_preview_item(active_item_id) {
2717                            pane.set_preview_item_id(None, cx);
2718                        } else {
2719                            pane.set_preview_item_id(Some(active_item_id), cx);
2720                        }
2721                    }
2722                }))
2723            })
2724            .on_action(
2725                cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
2726                    if let Some(task) = pane.close_active_item(action, cx) {
2727                        task.detach_and_log_err(cx)
2728                    }
2729                }),
2730            )
2731            .on_action(
2732                cx.listener(|pane: &mut Self, action: &CloseInactiveItems, cx| {
2733                    if let Some(task) = pane.close_inactive_items(action, cx) {
2734                        task.detach_and_log_err(cx)
2735                    }
2736                }),
2737            )
2738            .on_action(
2739                cx.listener(|pane: &mut Self, action: &CloseCleanItems, cx| {
2740                    if let Some(task) = pane.close_clean_items(action, cx) {
2741                        task.detach_and_log_err(cx)
2742                    }
2743                }),
2744            )
2745            .on_action(
2746                cx.listener(|pane: &mut Self, action: &CloseItemsToTheLeft, cx| {
2747                    if let Some(task) = pane.close_items_to_the_left(action, cx) {
2748                        task.detach_and_log_err(cx)
2749                    }
2750                }),
2751            )
2752            .on_action(
2753                cx.listener(|pane: &mut Self, action: &CloseItemsToTheRight, cx| {
2754                    if let Some(task) = pane.close_items_to_the_right(action, cx) {
2755                        task.detach_and_log_err(cx)
2756                    }
2757                }),
2758            )
2759            .on_action(cx.listener(|pane: &mut Self, action: &CloseAllItems, cx| {
2760                if let Some(task) = pane.close_all_items(action, cx) {
2761                    task.detach_and_log_err(cx)
2762                }
2763            }))
2764            .on_action(
2765                cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
2766                    if let Some(task) = pane.close_active_item(action, cx) {
2767                        task.detach_and_log_err(cx)
2768                    }
2769                }),
2770            )
2771            .on_action(
2772                cx.listener(|pane: &mut Self, action: &RevealInProjectPanel, cx| {
2773                    let entry_id = action
2774                        .entry_id
2775                        .map(ProjectEntryId::from_proto)
2776                        .or_else(|| pane.active_item()?.project_entry_ids(cx).first().copied());
2777                    if let Some(entry_id) = entry_id {
2778                        pane.project.update(cx, |_, cx| {
2779                            cx.emit(project::Event::RevealInProjectPanel(entry_id))
2780                        });
2781                    }
2782                }),
2783            )
2784            .when(self.active_item().is_some() && display_tab_bar, |pane| {
2785                pane.child(self.render_tab_bar(cx))
2786            })
2787            .child({
2788                let has_worktrees = self.project.read(cx).worktrees(cx).next().is_some();
2789                // main content
2790                div()
2791                    .flex_1()
2792                    .relative()
2793                    .group("")
2794                    .overflow_hidden()
2795                    .on_drag_move::<DraggedTab>(cx.listener(Self::handle_drag_move))
2796                    .on_drag_move::<DraggedSelection>(cx.listener(Self::handle_drag_move))
2797                    .when(is_local, |div| {
2798                        div.on_drag_move::<ExternalPaths>(cx.listener(Self::handle_drag_move))
2799                    })
2800                    .map(|div| {
2801                        if let Some(item) = self.active_item() {
2802                            div.v_flex()
2803                                .size_full()
2804                                .overflow_hidden()
2805                                .child(self.toolbar.clone())
2806                                .child(item.to_any())
2807                        } else {
2808                            let placeholder = div.h_flex().size_full().justify_center();
2809                            if has_worktrees {
2810                                placeholder
2811                            } else {
2812                                placeholder.child(
2813                                    Label::new("Open a file or project to get started.")
2814                                        .color(Color::Muted),
2815                                )
2816                            }
2817                        }
2818                    })
2819                    .child(
2820                        // drag target
2821                        div()
2822                            .invisible()
2823                            .absolute()
2824                            .bg(cx.theme().colors().drop_target_background)
2825                            .group_drag_over::<DraggedTab>("", |style| style.visible())
2826                            .group_drag_over::<DraggedSelection>("", |style| style.visible())
2827                            .when(is_local, |div| {
2828                                div.group_drag_over::<ExternalPaths>("", |style| style.visible())
2829                            })
2830                            .when_some(self.can_drop_predicate.clone(), |this, p| {
2831                                this.can_drop(move |a, cx| p(a, cx))
2832                            })
2833                            .on_drop(cx.listener(move |this, dragged_tab, cx| {
2834                                this.handle_tab_drop(dragged_tab, this.active_item_index(), cx)
2835                            }))
2836                            .on_drop(cx.listener(move |this, selection: &DraggedSelection, cx| {
2837                                this.handle_dragged_selection_drop(selection, None, cx)
2838                            }))
2839                            .on_drop(cx.listener(move |this, paths, cx| {
2840                                this.handle_external_paths_drop(paths, cx)
2841                            }))
2842                            .map(|div| {
2843                                let size = DefiniteLength::Fraction(0.5);
2844                                match self.drag_split_direction {
2845                                    None => div.top_0().right_0().bottom_0().left_0(),
2846                                    Some(SplitDirection::Up) => {
2847                                        div.top_0().left_0().right_0().h(size)
2848                                    }
2849                                    Some(SplitDirection::Down) => {
2850                                        div.left_0().bottom_0().right_0().h(size)
2851                                    }
2852                                    Some(SplitDirection::Left) => {
2853                                        div.top_0().left_0().bottom_0().w(size)
2854                                    }
2855                                    Some(SplitDirection::Right) => {
2856                                        div.top_0().bottom_0().right_0().w(size)
2857                                    }
2858                                }
2859                            }),
2860                    )
2861            })
2862            .on_mouse_down(
2863                MouseButton::Navigate(NavigationDirection::Back),
2864                cx.listener(|pane, _, cx| {
2865                    if let Some(workspace) = pane.workspace.upgrade() {
2866                        let pane = cx.view().downgrade();
2867                        cx.window_context().defer(move |cx| {
2868                            workspace.update(cx, |workspace, cx| {
2869                                workspace.go_back(pane, cx).detach_and_log_err(cx)
2870                            })
2871                        })
2872                    }
2873                }),
2874            )
2875            .on_mouse_down(
2876                MouseButton::Navigate(NavigationDirection::Forward),
2877                cx.listener(|pane, _, cx| {
2878                    if let Some(workspace) = pane.workspace.upgrade() {
2879                        let pane = cx.view().downgrade();
2880                        cx.window_context().defer(move |cx| {
2881                            workspace.update(cx, |workspace, cx| {
2882                                workspace.go_forward(pane, cx).detach_and_log_err(cx)
2883                            })
2884                        })
2885                    }
2886                }),
2887            )
2888    }
2889}
2890
2891impl ItemNavHistory {
2892    pub fn push<D: 'static + Send + Any>(&mut self, data: Option<D>, cx: &mut WindowContext) {
2893        self.history
2894            .push(data, self.item.clone(), self.is_preview, cx);
2895    }
2896
2897    pub fn pop_backward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
2898        self.history.pop(NavigationMode::GoingBack, cx)
2899    }
2900
2901    pub fn pop_forward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
2902        self.history.pop(NavigationMode::GoingForward, cx)
2903    }
2904}
2905
2906impl NavHistory {
2907    pub fn for_each_entry(
2908        &self,
2909        cx: &AppContext,
2910        mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
2911    ) {
2912        let borrowed_history = self.0.lock();
2913        borrowed_history
2914            .forward_stack
2915            .iter()
2916            .chain(borrowed_history.backward_stack.iter())
2917            .chain(borrowed_history.closed_stack.iter())
2918            .for_each(|entry| {
2919                if let Some(project_and_abs_path) =
2920                    borrowed_history.paths_by_item.get(&entry.item.id())
2921                {
2922                    f(entry, project_and_abs_path.clone());
2923                } else if let Some(item) = entry.item.upgrade() {
2924                    if let Some(path) = item.project_path(cx) {
2925                        f(entry, (path, None));
2926                    }
2927                }
2928            })
2929    }
2930
2931    pub fn set_mode(&mut self, mode: NavigationMode) {
2932        self.0.lock().mode = mode;
2933    }
2934
2935    pub fn mode(&self) -> NavigationMode {
2936        self.0.lock().mode
2937    }
2938
2939    pub fn disable(&mut self) {
2940        self.0.lock().mode = NavigationMode::Disabled;
2941    }
2942
2943    pub fn enable(&mut self) {
2944        self.0.lock().mode = NavigationMode::Normal;
2945    }
2946
2947    pub fn pop(&mut self, mode: NavigationMode, cx: &mut WindowContext) -> Option<NavigationEntry> {
2948        let mut state = self.0.lock();
2949        let entry = match mode {
2950            NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
2951                return None
2952            }
2953            NavigationMode::GoingBack => &mut state.backward_stack,
2954            NavigationMode::GoingForward => &mut state.forward_stack,
2955            NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
2956        }
2957        .pop_back();
2958        if entry.is_some() {
2959            state.did_update(cx);
2960        }
2961        entry
2962    }
2963
2964    pub fn push<D: 'static + Send + Any>(
2965        &mut self,
2966        data: Option<D>,
2967        item: Arc<dyn WeakItemHandle>,
2968        is_preview: bool,
2969        cx: &mut WindowContext,
2970    ) {
2971        let state = &mut *self.0.lock();
2972        match state.mode {
2973            NavigationMode::Disabled => {}
2974            NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
2975                if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2976                    state.backward_stack.pop_front();
2977                }
2978                state.backward_stack.push_back(NavigationEntry {
2979                    item,
2980                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2981                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2982                    is_preview,
2983                });
2984                state.forward_stack.clear();
2985            }
2986            NavigationMode::GoingBack => {
2987                if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2988                    state.forward_stack.pop_front();
2989                }
2990                state.forward_stack.push_back(NavigationEntry {
2991                    item,
2992                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2993                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2994                    is_preview,
2995                });
2996            }
2997            NavigationMode::GoingForward => {
2998                if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2999                    state.backward_stack.pop_front();
3000                }
3001                state.backward_stack.push_back(NavigationEntry {
3002                    item,
3003                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3004                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3005                    is_preview,
3006                });
3007            }
3008            NavigationMode::ClosingItem => {
3009                if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3010                    state.closed_stack.pop_front();
3011                }
3012                state.closed_stack.push_back(NavigationEntry {
3013                    item,
3014                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3015                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3016                    is_preview,
3017                });
3018            }
3019        }
3020        state.did_update(cx);
3021    }
3022
3023    pub fn remove_item(&mut self, item_id: EntityId) {
3024        let mut state = self.0.lock();
3025        state.paths_by_item.remove(&item_id);
3026        state
3027            .backward_stack
3028            .retain(|entry| entry.item.id() != item_id);
3029        state
3030            .forward_stack
3031            .retain(|entry| entry.item.id() != item_id);
3032        state
3033            .closed_stack
3034            .retain(|entry| entry.item.id() != item_id);
3035    }
3036
3037    pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option<PathBuf>)> {
3038        self.0.lock().paths_by_item.get(&item_id).cloned()
3039    }
3040}
3041
3042impl NavHistoryState {
3043    pub fn did_update(&self, cx: &mut WindowContext) {
3044        if let Some(pane) = self.pane.upgrade() {
3045            cx.defer(move |cx| {
3046                pane.update(cx, |pane, cx| pane.history_updated(cx));
3047            });
3048        }
3049    }
3050}
3051
3052fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
3053    let path = buffer_path
3054        .as_ref()
3055        .and_then(|p| {
3056            p.path
3057                .to_str()
3058                .and_then(|s| if s.is_empty() { None } else { Some(s) })
3059        })
3060        .unwrap_or("This buffer");
3061    let path = truncate_and_remove_front(path, 80);
3062    format!("{path} contains unsaved edits. Do you want to save it?")
3063}
3064
3065pub fn tab_details(items: &[Box<dyn ItemHandle>], cx: &AppContext) -> Vec<usize> {
3066    let mut tab_details = items.iter().map(|_| 0).collect::<Vec<_>>();
3067    let mut tab_descriptions = HashMap::default();
3068    let mut done = false;
3069    while !done {
3070        done = true;
3071
3072        // Store item indices by their tab description.
3073        for (ix, (item, detail)) in items.iter().zip(&tab_details).enumerate() {
3074            if let Some(description) = item.tab_description(*detail, cx) {
3075                if *detail == 0
3076                    || Some(&description) != item.tab_description(detail - 1, cx).as_ref()
3077                {
3078                    tab_descriptions
3079                        .entry(description)
3080                        .or_insert(Vec::new())
3081                        .push(ix);
3082                }
3083            }
3084        }
3085
3086        // If two or more items have the same tab description, increase their level
3087        // of detail and try again.
3088        for (_, item_ixs) in tab_descriptions.drain() {
3089            if item_ixs.len() > 1 {
3090                done = false;
3091                for ix in item_ixs {
3092                    tab_details[ix] += 1;
3093                }
3094            }
3095        }
3096    }
3097
3098    tab_details
3099}
3100
3101pub fn render_item_indicator(item: Box<dyn ItemHandle>, cx: &WindowContext) -> Option<Indicator> {
3102    maybe!({
3103        let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
3104            (true, _) => Color::Warning,
3105            (_, true) => Color::Accent,
3106            (false, false) => return None,
3107        };
3108
3109        Some(Indicator::dot().color(indicator_color))
3110    })
3111}
3112
3113impl Render for DraggedTab {
3114    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3115        let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
3116        let label = self.item.tab_content(
3117            TabContentParams {
3118                detail: Some(self.detail),
3119                selected: false,
3120                preview: false,
3121            },
3122            cx,
3123        );
3124        Tab::new("")
3125            .selected(self.is_active)
3126            .child(label)
3127            .render(cx)
3128            .font(ui_font)
3129    }
3130}
3131
3132#[cfg(test)]
3133mod tests {
3134    use super::*;
3135    use crate::item::test::{TestItem, TestProjectItem};
3136    use gpui::{TestAppContext, VisualTestContext};
3137    use project::FakeFs;
3138    use settings::SettingsStore;
3139    use theme::LoadThemes;
3140
3141    #[gpui::test]
3142    async fn test_remove_active_empty(cx: &mut TestAppContext) {
3143        init_test(cx);
3144        let fs = FakeFs::new(cx.executor());
3145
3146        let project = Project::test(fs, None, cx).await;
3147        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3148        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3149
3150        pane.update(cx, |pane, cx| {
3151            assert!(pane
3152                .close_active_item(&CloseActiveItem { save_intent: None }, cx)
3153                .is_none())
3154        });
3155    }
3156
3157    #[gpui::test]
3158    async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
3159        init_test(cx);
3160        let fs = FakeFs::new(cx.executor());
3161
3162        let project = Project::test(fs, None, cx).await;
3163        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3164        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3165
3166        // 1. Add with a destination index
3167        //   a. Add before the active item
3168        set_labeled_items(&pane, ["A", "B*", "C"], cx);
3169        pane.update(cx, |pane, cx| {
3170            pane.add_item(
3171                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3172                false,
3173                false,
3174                Some(0),
3175                cx,
3176            );
3177        });
3178        assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
3179
3180        //   b. Add after the active item
3181        set_labeled_items(&pane, ["A", "B*", "C"], cx);
3182        pane.update(cx, |pane, cx| {
3183            pane.add_item(
3184                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3185                false,
3186                false,
3187                Some(2),
3188                cx,
3189            );
3190        });
3191        assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3192
3193        //   c. Add at the end of the item list (including off the length)
3194        set_labeled_items(&pane, ["A", "B*", "C"], cx);
3195        pane.update(cx, |pane, cx| {
3196            pane.add_item(
3197                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3198                false,
3199                false,
3200                Some(5),
3201                cx,
3202            );
3203        });
3204        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3205
3206        // 2. Add without a destination index
3207        //   a. Add with active item at the start of the item list
3208        set_labeled_items(&pane, ["A*", "B", "C"], cx);
3209        pane.update(cx, |pane, cx| {
3210            pane.add_item(
3211                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3212                false,
3213                false,
3214                None,
3215                cx,
3216            );
3217        });
3218        set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
3219
3220        //   b. Add with active item at the end of the item list
3221        set_labeled_items(&pane, ["A", "B", "C*"], cx);
3222        pane.update(cx, |pane, cx| {
3223            pane.add_item(
3224                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3225                false,
3226                false,
3227                None,
3228                cx,
3229            );
3230        });
3231        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3232    }
3233
3234    #[gpui::test]
3235    async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
3236        init_test(cx);
3237        let fs = FakeFs::new(cx.executor());
3238
3239        let project = Project::test(fs, None, cx).await;
3240        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3241        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3242
3243        // 1. Add with a destination index
3244        //   1a. Add before the active item
3245        let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3246        pane.update(cx, |pane, cx| {
3247            pane.add_item(d, false, false, Some(0), cx);
3248        });
3249        assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
3250
3251        //   1b. Add after the active item
3252        let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3253        pane.update(cx, |pane, cx| {
3254            pane.add_item(d, false, false, Some(2), cx);
3255        });
3256        assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3257
3258        //   1c. Add at the end of the item list (including off the length)
3259        let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3260        pane.update(cx, |pane, cx| {
3261            pane.add_item(a, false, false, Some(5), cx);
3262        });
3263        assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3264
3265        //   1d. Add same item to active index
3266        let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3267        pane.update(cx, |pane, cx| {
3268            pane.add_item(b, false, false, Some(1), cx);
3269        });
3270        assert_item_labels(&pane, ["A", "B*", "C"], cx);
3271
3272        //   1e. Add item to index after same item in last position
3273        let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3274        pane.update(cx, |pane, cx| {
3275            pane.add_item(c, false, false, Some(2), cx);
3276        });
3277        assert_item_labels(&pane, ["A", "B", "C*"], cx);
3278
3279        // 2. Add without a destination index
3280        //   2a. Add with active item at the start of the item list
3281        let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
3282        pane.update(cx, |pane, cx| {
3283            pane.add_item(d, false, false, None, cx);
3284        });
3285        assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
3286
3287        //   2b. Add with active item at the end of the item list
3288        let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
3289        pane.update(cx, |pane, cx| {
3290            pane.add_item(a, false, false, None, cx);
3291        });
3292        assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3293
3294        //   2c. Add active item to active item at end of list
3295        let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
3296        pane.update(cx, |pane, cx| {
3297            pane.add_item(c, false, false, None, cx);
3298        });
3299        assert_item_labels(&pane, ["A", "B", "C*"], cx);
3300
3301        //   2d. Add active item to active item at start of list
3302        let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
3303        pane.update(cx, |pane, cx| {
3304            pane.add_item(a, false, false, None, cx);
3305        });
3306        assert_item_labels(&pane, ["A*", "B", "C"], cx);
3307    }
3308
3309    #[gpui::test]
3310    async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
3311        init_test(cx);
3312        let fs = FakeFs::new(cx.executor());
3313
3314        let project = Project::test(fs, None, cx).await;
3315        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3316        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3317
3318        // singleton view
3319        pane.update(cx, |pane, cx| {
3320            pane.add_item(
3321                Box::new(cx.new_view(|cx| {
3322                    TestItem::new(cx)
3323                        .with_singleton(true)
3324                        .with_label("buffer 1")
3325                        .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
3326                })),
3327                false,
3328                false,
3329                None,
3330                cx,
3331            );
3332        });
3333        assert_item_labels(&pane, ["buffer 1*"], cx);
3334
3335        // new singleton view with the same project entry
3336        pane.update(cx, |pane, cx| {
3337            pane.add_item(
3338                Box::new(cx.new_view(|cx| {
3339                    TestItem::new(cx)
3340                        .with_singleton(true)
3341                        .with_label("buffer 1")
3342                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3343                })),
3344                false,
3345                false,
3346                None,
3347                cx,
3348            );
3349        });
3350        assert_item_labels(&pane, ["buffer 1*"], cx);
3351
3352        // new singleton view with different project entry
3353        pane.update(cx, |pane, cx| {
3354            pane.add_item(
3355                Box::new(cx.new_view(|cx| {
3356                    TestItem::new(cx)
3357                        .with_singleton(true)
3358                        .with_label("buffer 2")
3359                        .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
3360                })),
3361                false,
3362                false,
3363                None,
3364                cx,
3365            );
3366        });
3367        assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
3368
3369        // new multibuffer view with the same project entry
3370        pane.update(cx, |pane, cx| {
3371            pane.add_item(
3372                Box::new(cx.new_view(|cx| {
3373                    TestItem::new(cx)
3374                        .with_singleton(false)
3375                        .with_label("multibuffer 1")
3376                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3377                })),
3378                false,
3379                false,
3380                None,
3381                cx,
3382            );
3383        });
3384        assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
3385
3386        // another multibuffer view with the same project entry
3387        pane.update(cx, |pane, cx| {
3388            pane.add_item(
3389                Box::new(cx.new_view(|cx| {
3390                    TestItem::new(cx)
3391                        .with_singleton(false)
3392                        .with_label("multibuffer 1b")
3393                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3394                })),
3395                false,
3396                false,
3397                None,
3398                cx,
3399            );
3400        });
3401        assert_item_labels(
3402            &pane,
3403            ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
3404            cx,
3405        );
3406    }
3407
3408    #[gpui::test]
3409    async fn test_remove_item_ordering_history(cx: &mut TestAppContext) {
3410        init_test(cx);
3411        let fs = FakeFs::new(cx.executor());
3412
3413        let project = Project::test(fs, None, cx).await;
3414        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3415        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3416
3417        add_labeled_item(&pane, "A", false, cx);
3418        add_labeled_item(&pane, "B", false, cx);
3419        add_labeled_item(&pane, "C", false, cx);
3420        add_labeled_item(&pane, "D", false, cx);
3421        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3422
3423        pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
3424        add_labeled_item(&pane, "1", false, cx);
3425        assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
3426
3427        pane.update(cx, |pane, cx| {
3428            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3429        })
3430        .unwrap()
3431        .await
3432        .unwrap();
3433        assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
3434
3435        pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
3436        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3437
3438        pane.update(cx, |pane, cx| {
3439            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3440        })
3441        .unwrap()
3442        .await
3443        .unwrap();
3444        assert_item_labels(&pane, ["A", "B*", "C"], cx);
3445
3446        pane.update(cx, |pane, cx| {
3447            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3448        })
3449        .unwrap()
3450        .await
3451        .unwrap();
3452        assert_item_labels(&pane, ["A", "C*"], cx);
3453
3454        pane.update(cx, |pane, cx| {
3455            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3456        })
3457        .unwrap()
3458        .await
3459        .unwrap();
3460        assert_item_labels(&pane, ["A*"], cx);
3461    }
3462
3463    #[gpui::test]
3464    async fn test_remove_item_ordering_neighbour(cx: &mut TestAppContext) {
3465        init_test(cx);
3466        cx.update_global::<SettingsStore, ()>(|s, cx| {
3467            s.update_user_settings::<ItemSettings>(cx, |s| {
3468                s.activate_on_close = Some(ActivateOnClose::Neighbour);
3469            });
3470        });
3471        let fs = FakeFs::new(cx.executor());
3472
3473        let project = Project::test(fs, None, cx).await;
3474        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3475        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3476
3477        add_labeled_item(&pane, "A", false, cx);
3478        add_labeled_item(&pane, "B", false, cx);
3479        add_labeled_item(&pane, "C", false, cx);
3480        add_labeled_item(&pane, "D", false, cx);
3481        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3482
3483        pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
3484        add_labeled_item(&pane, "1", false, cx);
3485        assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
3486
3487        pane.update(cx, |pane, cx| {
3488            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3489        })
3490        .unwrap()
3491        .await
3492        .unwrap();
3493        assert_item_labels(&pane, ["A", "B", "C*", "D"], cx);
3494
3495        pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
3496        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3497
3498        pane.update(cx, |pane, cx| {
3499            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3500        })
3501        .unwrap()
3502        .await
3503        .unwrap();
3504        assert_item_labels(&pane, ["A", "B", "C*"], cx);
3505
3506        pane.update(cx, |pane, cx| {
3507            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3508        })
3509        .unwrap()
3510        .await
3511        .unwrap();
3512        assert_item_labels(&pane, ["A", "B*"], cx);
3513
3514        pane.update(cx, |pane, cx| {
3515            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3516        })
3517        .unwrap()
3518        .await
3519        .unwrap();
3520        assert_item_labels(&pane, ["A*"], cx);
3521    }
3522
3523    #[gpui::test]
3524    async fn test_close_inactive_items(cx: &mut TestAppContext) {
3525        init_test(cx);
3526        let fs = FakeFs::new(cx.executor());
3527
3528        let project = Project::test(fs, None, cx).await;
3529        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3530        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3531
3532        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
3533
3534        pane.update(cx, |pane, cx| {
3535            pane.close_inactive_items(
3536                &CloseInactiveItems {
3537                    save_intent: None,
3538                    close_pinned: false,
3539                },
3540                cx,
3541            )
3542        })
3543        .unwrap()
3544        .await
3545        .unwrap();
3546        assert_item_labels(&pane, ["C*"], cx);
3547    }
3548
3549    #[gpui::test]
3550    async fn test_close_clean_items(cx: &mut TestAppContext) {
3551        init_test(cx);
3552        let fs = FakeFs::new(cx.executor());
3553
3554        let project = Project::test(fs, None, cx).await;
3555        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3556        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3557
3558        add_labeled_item(&pane, "A", true, cx);
3559        add_labeled_item(&pane, "B", false, cx);
3560        add_labeled_item(&pane, "C", true, cx);
3561        add_labeled_item(&pane, "D", false, cx);
3562        add_labeled_item(&pane, "E", false, cx);
3563        assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
3564
3565        pane.update(cx, |pane, cx| {
3566            pane.close_clean_items(
3567                &CloseCleanItems {
3568                    close_pinned: false,
3569                },
3570                cx,
3571            )
3572        })
3573        .unwrap()
3574        .await
3575        .unwrap();
3576        assert_item_labels(&pane, ["A^", "C*^"], cx);
3577    }
3578
3579    #[gpui::test]
3580    async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
3581        init_test(cx);
3582        let fs = FakeFs::new(cx.executor());
3583
3584        let project = Project::test(fs, None, cx).await;
3585        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3586        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3587
3588        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
3589
3590        pane.update(cx, |pane, cx| {
3591            pane.close_items_to_the_left(
3592                &CloseItemsToTheLeft {
3593                    close_pinned: false,
3594                },
3595                cx,
3596            )
3597        })
3598        .unwrap()
3599        .await
3600        .unwrap();
3601        assert_item_labels(&pane, ["C*", "D", "E"], cx);
3602    }
3603
3604    #[gpui::test]
3605    async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
3606        init_test(cx);
3607        let fs = FakeFs::new(cx.executor());
3608
3609        let project = Project::test(fs, None, cx).await;
3610        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3611        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3612
3613        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
3614
3615        pane.update(cx, |pane, cx| {
3616            pane.close_items_to_the_right(
3617                &CloseItemsToTheRight {
3618                    close_pinned: false,
3619                },
3620                cx,
3621            )
3622        })
3623        .unwrap()
3624        .await
3625        .unwrap();
3626        assert_item_labels(&pane, ["A", "B", "C*"], cx);
3627    }
3628
3629    #[gpui::test]
3630    async fn test_close_all_items(cx: &mut TestAppContext) {
3631        init_test(cx);
3632        let fs = FakeFs::new(cx.executor());
3633
3634        let project = Project::test(fs, None, cx).await;
3635        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3636        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3637
3638        let item_a = add_labeled_item(&pane, "A", false, cx);
3639        add_labeled_item(&pane, "B", false, cx);
3640        add_labeled_item(&pane, "C", false, cx);
3641        assert_item_labels(&pane, ["A", "B", "C*"], cx);
3642
3643        pane.update(cx, |pane, cx| {
3644            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
3645            pane.pin_tab_at(ix, cx);
3646            pane.close_all_items(
3647                &CloseAllItems {
3648                    save_intent: None,
3649                    close_pinned: false,
3650                },
3651                cx,
3652            )
3653        })
3654        .unwrap()
3655        .await
3656        .unwrap();
3657        assert_item_labels(&pane, ["A*"], cx);
3658
3659        pane.update(cx, |pane, cx| {
3660            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
3661            pane.unpin_tab_at(ix, cx);
3662            pane.close_all_items(
3663                &CloseAllItems {
3664                    save_intent: None,
3665                    close_pinned: false,
3666                },
3667                cx,
3668            )
3669        })
3670        .unwrap()
3671        .await
3672        .unwrap();
3673
3674        assert_item_labels(&pane, [], cx);
3675
3676        add_labeled_item(&pane, "A", true, cx);
3677        add_labeled_item(&pane, "B", true, cx);
3678        add_labeled_item(&pane, "C", true, cx);
3679        assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
3680
3681        let save = pane
3682            .update(cx, |pane, cx| {
3683                pane.close_all_items(
3684                    &CloseAllItems {
3685                        save_intent: None,
3686                        close_pinned: false,
3687                    },
3688                    cx,
3689                )
3690            })
3691            .unwrap();
3692
3693        cx.executor().run_until_parked();
3694        cx.simulate_prompt_answer(2);
3695        save.await.unwrap();
3696        assert_item_labels(&pane, [], cx);
3697    }
3698
3699    #[gpui::test]
3700    async fn test_close_all_items_including_pinned(cx: &mut TestAppContext) {
3701        init_test(cx);
3702        let fs = FakeFs::new(cx.executor());
3703
3704        let project = Project::test(fs, None, cx).await;
3705        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3706        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3707
3708        let item_a = add_labeled_item(&pane, "A", false, cx);
3709        add_labeled_item(&pane, "B", false, cx);
3710        add_labeled_item(&pane, "C", false, cx);
3711        assert_item_labels(&pane, ["A", "B", "C*"], cx);
3712
3713        pane.update(cx, |pane, cx| {
3714            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
3715            pane.pin_tab_at(ix, cx);
3716            pane.close_all_items(
3717                &CloseAllItems {
3718                    save_intent: None,
3719                    close_pinned: true,
3720                },
3721                cx,
3722            )
3723        })
3724        .unwrap()
3725        .await
3726        .unwrap();
3727        assert_item_labels(&pane, [], cx);
3728    }
3729
3730    fn init_test(cx: &mut TestAppContext) {
3731        cx.update(|cx| {
3732            let settings_store = SettingsStore::test(cx);
3733            cx.set_global(settings_store);
3734            theme::init(LoadThemes::JustBase, cx);
3735            crate::init_settings(cx);
3736            Project::init_settings(cx);
3737        });
3738    }
3739
3740    fn add_labeled_item(
3741        pane: &View<Pane>,
3742        label: &str,
3743        is_dirty: bool,
3744        cx: &mut VisualTestContext,
3745    ) -> Box<View<TestItem>> {
3746        pane.update(cx, |pane, cx| {
3747            let labeled_item = Box::new(
3748                cx.new_view(|cx| TestItem::new(cx).with_label(label).with_dirty(is_dirty)),
3749            );
3750            pane.add_item(labeled_item.clone(), false, false, None, cx);
3751            labeled_item
3752        })
3753    }
3754
3755    fn set_labeled_items<const COUNT: usize>(
3756        pane: &View<Pane>,
3757        labels: [&str; COUNT],
3758        cx: &mut VisualTestContext,
3759    ) -> [Box<View<TestItem>>; COUNT] {
3760        pane.update(cx, |pane, cx| {
3761            pane.items.clear();
3762            let mut active_item_index = 0;
3763
3764            let mut index = 0;
3765            let items = labels.map(|mut label| {
3766                if label.ends_with('*') {
3767                    label = label.trim_end_matches('*');
3768                    active_item_index = index;
3769                }
3770
3771                let labeled_item = Box::new(cx.new_view(|cx| TestItem::new(cx).with_label(label)));
3772                pane.add_item(labeled_item.clone(), false, false, None, cx);
3773                index += 1;
3774                labeled_item
3775            });
3776
3777            pane.activate_item(active_item_index, false, false, cx);
3778
3779            items
3780        })
3781    }
3782
3783    // Assert the item label, with the active item label suffixed with a '*'
3784    fn assert_item_labels<const COUNT: usize>(
3785        pane: &View<Pane>,
3786        expected_states: [&str; COUNT],
3787        cx: &mut VisualTestContext,
3788    ) {
3789        pane.update(cx, |pane, cx| {
3790            let actual_states = pane
3791                .items
3792                .iter()
3793                .enumerate()
3794                .map(|(ix, item)| {
3795                    let mut state = item
3796                        .to_any()
3797                        .downcast::<TestItem>()
3798                        .unwrap()
3799                        .read(cx)
3800                        .label
3801                        .clone();
3802                    if ix == pane.active_item_index {
3803                        state.push('*');
3804                    }
3805                    if item.is_dirty(cx) {
3806                        state.push('^');
3807                    }
3808                    state
3809                })
3810                .collect::<Vec<_>>();
3811
3812            assert_eq!(
3813                actual_states, expected_states,
3814                "pane items do not match expectation"
3815            );
3816        })
3817    }
3818}