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