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