pane.rs

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