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