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