pane.rs

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