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        cx: &mut ViewContext<'_, Pane>,
1793    ) -> impl IntoElement {
1794        let project_path = item.project_path(cx);
1795
1796        let is_active = ix == self.active_item_index;
1797        let is_preview = self
1798            .preview_item_id
1799            .map(|id| id == item.item_id())
1800            .unwrap_or(false);
1801
1802        let label = item.tab_content(
1803            TabContentParams {
1804                detail: Some(detail),
1805                selected: is_active,
1806                preview: is_preview,
1807            },
1808            cx,
1809        );
1810
1811        let icon_color = if ItemSettings::get_global(cx).git_status {
1812            project_path
1813                .as_ref()
1814                .and_then(|path| self.project.read(cx).entry_for_path(path, cx))
1815                .map(|entry| {
1816                    Self::git_aware_icon_color(entry.git_status, entry.is_ignored, is_active)
1817                })
1818                .unwrap_or_else(|| Self::icon_color(is_active))
1819        } else {
1820            Self::icon_color(is_active)
1821        };
1822
1823        let icon = item.tab_icon(cx);
1824        let close_side = &ItemSettings::get_global(cx).close_position;
1825        let indicator = render_item_indicator(item.boxed_clone(), cx);
1826        let item_id = item.item_id();
1827        let is_first_item = ix == 0;
1828        let is_last_item = ix == self.items.len() - 1;
1829        let is_pinned = self.is_tab_pinned(ix);
1830        let position_relative_to_active_item = ix.cmp(&self.active_item_index);
1831
1832        let tab = Tab::new(ix)
1833            .position(if is_first_item {
1834                TabPosition::First
1835            } else if is_last_item {
1836                TabPosition::Last
1837            } else {
1838                TabPosition::Middle(position_relative_to_active_item)
1839            })
1840            .close_side(match close_side {
1841                ClosePosition::Left => ui::TabCloseSide::Start,
1842                ClosePosition::Right => ui::TabCloseSide::End,
1843            })
1844            .selected(is_active)
1845            .on_click(
1846                cx.listener(move |pane: &mut Self, _, cx| pane.activate_item(ix, true, true, cx)),
1847            )
1848            // TODO: This should be a click listener with the middle mouse button instead of a mouse down listener.
1849            .on_mouse_down(
1850                MouseButton::Middle,
1851                cx.listener(move |pane, _event, cx| {
1852                    pane.close_item_by_id(item_id, SaveIntent::Close, cx)
1853                        .detach_and_log_err(cx);
1854                }),
1855            )
1856            .on_mouse_down(
1857                MouseButton::Left,
1858                cx.listener(move |pane, event: &MouseDownEvent, cx| {
1859                    if let Some(id) = pane.preview_item_id {
1860                        if id == item_id && event.click_count > 1 {
1861                            pane.set_preview_item_id(None, cx);
1862                        }
1863                    }
1864                }),
1865            )
1866            .on_drag(
1867                DraggedTab {
1868                    item: item.boxed_clone(),
1869                    pane: cx.view().clone(),
1870                    detail,
1871                    is_active,
1872                    ix,
1873                },
1874                |tab, cx| cx.new_view(|_| tab.clone()),
1875            )
1876            .drag_over::<DraggedTab>(|tab, _, cx| {
1877                tab.bg(cx.theme().colors().drop_target_background)
1878            })
1879            .drag_over::<DraggedSelection>(|tab, _, cx| {
1880                tab.bg(cx.theme().colors().drop_target_background)
1881            })
1882            .when_some(self.can_drop_predicate.clone(), |this, p| {
1883                this.can_drop(move |a, cx| p(a, cx))
1884            })
1885            .on_drop(cx.listener(move |this, dragged_tab: &DraggedTab, cx| {
1886                this.drag_split_direction = None;
1887                this.handle_tab_drop(dragged_tab, ix, cx)
1888            }))
1889            .on_drop(cx.listener(move |this, selection: &DraggedSelection, cx| {
1890                this.drag_split_direction = None;
1891                this.handle_dragged_selection_drop(selection, cx)
1892            }))
1893            .on_drop(cx.listener(move |this, paths, cx| {
1894                this.drag_split_direction = None;
1895                this.handle_external_paths_drop(paths, cx)
1896            }))
1897            .when_some(item.tab_tooltip_text(cx), |tab, text| {
1898                tab.tooltip(move |cx| Tooltip::text(text.clone(), cx))
1899            })
1900            .start_slot::<Indicator>(indicator)
1901            .map(|this| {
1902                let end_slot = if is_pinned {
1903                    IconButton::new("unpin tab", IconName::Pin)
1904                        .shape(IconButtonShape::Square)
1905                        .icon_color(Color::Muted)
1906                        .size(ButtonSize::None)
1907                        .icon_size(IconSize::XSmall)
1908                        .on_click(cx.listener(move |pane, _, cx| {
1909                            pane.unpin_tab_at(ix, cx);
1910                        }))
1911                        .tooltip(|cx| Tooltip::text("Unpin Tab", cx))
1912                } else {
1913                    IconButton::new("close tab", IconName::Close)
1914                        .visible_on_hover("")
1915                        .shape(IconButtonShape::Square)
1916                        .icon_color(Color::Muted)
1917                        .size(ButtonSize::None)
1918                        .icon_size(IconSize::XSmall)
1919                        .on_click(cx.listener(move |pane, _, cx| {
1920                            pane.close_item_by_id(item_id, SaveIntent::Close, cx)
1921                                .detach_and_log_err(cx);
1922                        }))
1923                };
1924                this.end_slot(end_slot)
1925            })
1926            .child(
1927                h_flex()
1928                    .gap_1()
1929                    .children(icon.map(|icon| icon.size(IconSize::Small).color(icon_color)))
1930                    .child(label),
1931            );
1932
1933        let single_entry_to_resolve = {
1934            let item_entries = self.items[ix].project_entry_ids(cx);
1935            if item_entries.len() == 1 {
1936                Some(item_entries[0])
1937            } else {
1938                None
1939            }
1940        };
1941
1942        let is_pinned = self.is_tab_pinned(ix);
1943        let pane = cx.view().downgrade();
1944        right_click_menu(ix).trigger(tab).menu(move |cx| {
1945            let pane = pane.clone();
1946            ContextMenu::build(cx, move |mut menu, cx| {
1947                if let Some(pane) = pane.upgrade() {
1948                    menu = menu
1949                        .entry(
1950                            "Close",
1951                            Some(Box::new(CloseActiveItem { save_intent: None })),
1952                            cx.handler_for(&pane, move |pane, cx| {
1953                                pane.close_item_by_id(item_id, SaveIntent::Close, cx)
1954                                    .detach_and_log_err(cx);
1955                            }),
1956                        )
1957                        .entry(
1958                            "Close Others",
1959                            Some(Box::new(CloseInactiveItems { save_intent: None })),
1960                            cx.handler_for(&pane, move |pane, cx| {
1961                                pane.close_items(cx, SaveIntent::Close, |id| id != item_id)
1962                                    .detach_and_log_err(cx);
1963                            }),
1964                        )
1965                        .separator()
1966                        .entry(
1967                            "Close Left",
1968                            Some(Box::new(CloseItemsToTheLeft)),
1969                            cx.handler_for(&pane, move |pane, cx| {
1970                                pane.close_items_to_the_left_by_id(item_id, cx)
1971                                    .detach_and_log_err(cx);
1972                            }),
1973                        )
1974                        .entry(
1975                            "Close Right",
1976                            Some(Box::new(CloseItemsToTheRight)),
1977                            cx.handler_for(&pane, move |pane, cx| {
1978                                pane.close_items_to_the_right_by_id(item_id, cx)
1979                                    .detach_and_log_err(cx);
1980                            }),
1981                        )
1982                        .separator()
1983                        .entry(
1984                            "Close Clean",
1985                            Some(Box::new(CloseCleanItems)),
1986                            cx.handler_for(&pane, move |pane, cx| {
1987                                if let Some(task) = pane.close_clean_items(&CloseCleanItems, cx) {
1988                                    task.detach_and_log_err(cx)
1989                                }
1990                            }),
1991                        )
1992                        .entry(
1993                            "Close All",
1994                            Some(Box::new(CloseAllItems { save_intent: None })),
1995                            cx.handler_for(&pane, |pane, cx| {
1996                                if let Some(task) =
1997                                    pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
1998                                {
1999                                    task.detach_and_log_err(cx)
2000                                }
2001                            }),
2002                        );
2003
2004                    let pin_tab_entries = |menu: ContextMenu| {
2005                        menu.separator().map(|this| {
2006                            if is_pinned {
2007                                this.entry(
2008                                    "Unpin Tab",
2009                                    Some(TogglePinTab.boxed_clone()),
2010                                    cx.handler_for(&pane, move |pane, cx| {
2011                                        pane.unpin_tab_at(ix, cx);
2012                                    }),
2013                                )
2014                            } else {
2015                                this.entry(
2016                                    "Pin Tab",
2017                                    Some(TogglePinTab.boxed_clone()),
2018                                    cx.handler_for(&pane, move |pane, cx| {
2019                                        pane.pin_tab_at(ix, cx);
2020                                    }),
2021                                )
2022                            }
2023                        })
2024                    };
2025                    if let Some(entry) = single_entry_to_resolve {
2026                        let entry_abs_path = pane.read(cx).entry_abs_path(entry, cx);
2027                        let parent_abs_path = entry_abs_path
2028                            .as_deref()
2029                            .and_then(|abs_path| Some(abs_path.parent()?.to_path_buf()));
2030
2031                        let entry_id = entry.to_proto();
2032                        menu = menu
2033                            .separator()
2034                            .when_some(entry_abs_path, |menu, abs_path| {
2035                                menu.entry(
2036                                    "Copy Path",
2037                                    Some(Box::new(CopyPath)),
2038                                    cx.handler_for(&pane, move |_, cx| {
2039                                        cx.write_to_clipboard(ClipboardItem::new_string(
2040                                            abs_path.to_string_lossy().to_string(),
2041                                        ));
2042                                    }),
2043                                )
2044                            })
2045                            .entry(
2046                                "Copy Relative Path",
2047                                Some(Box::new(CopyRelativePath)),
2048                                cx.handler_for(&pane, move |pane, cx| {
2049                                    pane.copy_relative_path(&CopyRelativePath, cx);
2050                                }),
2051                            )
2052                            .map(pin_tab_entries)
2053                            .separator()
2054                            .entry(
2055                                "Reveal In Project Panel",
2056                                Some(Box::new(RevealInProjectPanel {
2057                                    entry_id: Some(entry_id),
2058                                })),
2059                                cx.handler_for(&pane, move |pane, cx| {
2060                                    pane.project.update(cx, |_, cx| {
2061                                        cx.emit(project::Event::RevealInProjectPanel(
2062                                            ProjectEntryId::from_proto(entry_id),
2063                                        ))
2064                                    });
2065                                }),
2066                            )
2067                            .when_some(parent_abs_path, |menu, parent_abs_path| {
2068                                menu.entry(
2069                                    "Open in Terminal",
2070                                    Some(Box::new(OpenInTerminal)),
2071                                    cx.handler_for(&pane, move |_, cx| {
2072                                        cx.dispatch_action(
2073                                            OpenTerminal {
2074                                                working_directory: parent_abs_path.clone(),
2075                                            }
2076                                            .boxed_clone(),
2077                                        );
2078                                    }),
2079                                )
2080                            });
2081                    } else {
2082                        menu = menu.map(pin_tab_entries);
2083                    }
2084                }
2085
2086                menu
2087            })
2088        })
2089    }
2090
2091    fn render_tab_bar(&mut self, cx: &mut ViewContext<'_, Pane>) -> impl IntoElement {
2092        let focus_handle = self.focus_handle.clone();
2093        let navigate_backward = IconButton::new("navigate_backward", IconName::ArrowLeft)
2094            .shape(IconButtonShape::Square)
2095            .icon_size(IconSize::Small)
2096            .on_click({
2097                let view = cx.view().clone();
2098                move |_, cx| view.update(cx, Self::navigate_backward)
2099            })
2100            .disabled(!self.can_navigate_backward())
2101            .tooltip({
2102                let focus_handle = focus_handle.clone();
2103                move |cx| Tooltip::for_action_in("Go Back", &GoBack, &focus_handle, cx)
2104            });
2105
2106        let navigate_forward = IconButton::new("navigate_forward", IconName::ArrowRight)
2107            .shape(IconButtonShape::Square)
2108            .icon_size(IconSize::Small)
2109            .on_click({
2110                let view = cx.view().clone();
2111                move |_, cx| view.update(cx, Self::navigate_forward)
2112            })
2113            .disabled(!self.can_navigate_forward())
2114            .tooltip({
2115                let focus_handle = focus_handle.clone();
2116                move |cx| Tooltip::for_action_in("Go Forward", &GoForward, &focus_handle, cx)
2117            });
2118
2119        let mut tab_items = self
2120            .items
2121            .iter()
2122            .enumerate()
2123            .zip(tab_details(&self.items, cx))
2124            .map(|((ix, item), detail)| self.render_tab(ix, &**item, detail, cx))
2125            .collect::<Vec<_>>();
2126
2127        let unpinned_tabs = tab_items.split_off(self.pinned_tab_count);
2128        let pinned_tabs = tab_items;
2129        TabBar::new("tab_bar")
2130            .when(
2131                self.display_nav_history_buttons.unwrap_or_default(),
2132                |tab_bar| {
2133                    tab_bar
2134                        .start_child(navigate_backward)
2135                        .start_child(navigate_forward)
2136                },
2137            )
2138            .map(|tab_bar| {
2139                let render_tab_buttons = self.render_tab_bar_buttons.clone();
2140                let (left_children, right_children) = render_tab_buttons(self, cx);
2141
2142                tab_bar
2143                    .start_children(left_children)
2144                    .end_children(right_children)
2145            })
2146            .children(pinned_tabs.len().ne(&0).then(|| {
2147                h_flex()
2148                    .children(pinned_tabs)
2149                    .border_r_2()
2150                    .border_color(cx.theme().colors().border)
2151            }))
2152            .child(
2153                h_flex()
2154                    .id("unpinned tabs")
2155                    .overflow_x_scroll()
2156                    .w_full()
2157                    .track_scroll(&self.tab_bar_scroll_handle)
2158                    .children(unpinned_tabs)
2159                    .child(
2160                        div()
2161                            .id("tab_bar_drop_target")
2162                            .min_w_6()
2163                            // HACK: This empty child is currently necessary to force the drop target to appear
2164                            // despite us setting a min width above.
2165                            .child("")
2166                            .h_full()
2167                            .flex_grow()
2168                            .drag_over::<DraggedTab>(|bar, _, cx| {
2169                                bar.bg(cx.theme().colors().drop_target_background)
2170                            })
2171                            .drag_over::<DraggedSelection>(|bar, _, cx| {
2172                                bar.bg(cx.theme().colors().drop_target_background)
2173                            })
2174                            .on_drop(cx.listener(move |this, dragged_tab: &DraggedTab, cx| {
2175                                this.drag_split_direction = None;
2176                                this.handle_tab_drop(dragged_tab, this.items.len(), cx)
2177                            }))
2178                            .on_drop(cx.listener(move |this, selection: &DraggedSelection, cx| {
2179                                this.drag_split_direction = None;
2180                                this.handle_project_entry_drop(
2181                                    &selection.active_selection.entry_id,
2182                                    cx,
2183                                )
2184                            }))
2185                            .on_drop(cx.listener(move |this, paths, cx| {
2186                                this.drag_split_direction = None;
2187                                this.handle_external_paths_drop(paths, cx)
2188                            }))
2189                            .on_click(cx.listener(move |this, event: &ClickEvent, cx| {
2190                                if event.up.click_count == 2 {
2191                                    cx.dispatch_action(
2192                                        this.double_click_dispatch_action.boxed_clone(),
2193                                    )
2194                                }
2195                            })),
2196                    ),
2197            )
2198    }
2199
2200    pub fn render_menu_overlay(menu: &View<ContextMenu>) -> Div {
2201        div().absolute().bottom_0().right_0().size_0().child(
2202            deferred(
2203                anchored()
2204                    .anchor(AnchorCorner::TopRight)
2205                    .child(menu.clone()),
2206            )
2207            .with_priority(1),
2208        )
2209    }
2210
2211    pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
2212        self.zoomed = zoomed;
2213        cx.notify();
2214    }
2215
2216    pub fn is_zoomed(&self) -> bool {
2217        self.zoomed
2218    }
2219
2220    fn handle_drag_move<T>(&mut self, event: &DragMoveEvent<T>, cx: &mut ViewContext<Self>) {
2221        if !self.can_split {
2222            return;
2223        }
2224
2225        let rect = event.bounds.size;
2226
2227        let size = event.bounds.size.width.min(event.bounds.size.height)
2228            * WorkspaceSettings::get_global(cx).drop_target_size;
2229
2230        let relative_cursor = Point::new(
2231            event.event.position.x - event.bounds.left(),
2232            event.event.position.y - event.bounds.top(),
2233        );
2234
2235        let direction = if relative_cursor.x < size
2236            || relative_cursor.x > rect.width - size
2237            || relative_cursor.y < size
2238            || relative_cursor.y > rect.height - size
2239        {
2240            [
2241                SplitDirection::Up,
2242                SplitDirection::Right,
2243                SplitDirection::Down,
2244                SplitDirection::Left,
2245            ]
2246            .iter()
2247            .min_by_key(|side| match side {
2248                SplitDirection::Up => relative_cursor.y,
2249                SplitDirection::Right => rect.width - relative_cursor.x,
2250                SplitDirection::Down => rect.height - relative_cursor.y,
2251                SplitDirection::Left => relative_cursor.x,
2252            })
2253            .cloned()
2254        } else {
2255            None
2256        };
2257
2258        if direction != self.drag_split_direction {
2259            self.drag_split_direction = direction;
2260        }
2261    }
2262
2263    fn handle_tab_drop(
2264        &mut self,
2265        dragged_tab: &DraggedTab,
2266        ix: usize,
2267        cx: &mut ViewContext<'_, Self>,
2268    ) {
2269        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2270            if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_tab, cx) {
2271                return;
2272            }
2273        }
2274        let mut to_pane = cx.view().clone();
2275        let split_direction = self.drag_split_direction;
2276        let item_id = dragged_tab.item.item_id();
2277        if let Some(preview_item_id) = self.preview_item_id {
2278            if item_id == preview_item_id {
2279                self.set_preview_item_id(None, cx);
2280            }
2281        }
2282
2283        let from_pane = dragged_tab.pane.clone();
2284        self.workspace
2285            .update(cx, |_, cx| {
2286                cx.defer(move |workspace, cx| {
2287                    if let Some(split_direction) = split_direction {
2288                        to_pane = workspace.split_pane(to_pane, split_direction, cx);
2289                    }
2290                    let old_ix = from_pane.read(cx).index_for_item_id(item_id);
2291                    if to_pane == from_pane {
2292                        if let Some(old_index) = old_ix {
2293                            to_pane.update(cx, |this, _| {
2294                                if old_index < this.pinned_tab_count
2295                                    && (ix == this.items.len() || ix > this.pinned_tab_count)
2296                                {
2297                                    this.pinned_tab_count -= 1;
2298                                } else if this.has_pinned_tabs()
2299                                    && old_index >= this.pinned_tab_count
2300                                    && ix < this.pinned_tab_count
2301                                {
2302                                    this.pinned_tab_count += 1;
2303                                }
2304                            });
2305                        }
2306                    } else {
2307                        to_pane.update(cx, |this, _| {
2308                            if this.has_pinned_tabs() && ix < this.pinned_tab_count {
2309                                this.pinned_tab_count += 1;
2310                            }
2311                        });
2312                        from_pane.update(cx, |this, _| {
2313                            if let Some(index) = old_ix {
2314                                if this.pinned_tab_count > index {
2315                                    this.pinned_tab_count -= 1;
2316                                }
2317                            }
2318                        })
2319                    }
2320                    workspace.move_item(from_pane.clone(), to_pane.clone(), item_id, ix, cx);
2321                });
2322            })
2323            .log_err();
2324    }
2325
2326    fn handle_dragged_selection_drop(
2327        &mut self,
2328        dragged_selection: &DraggedSelection,
2329        cx: &mut ViewContext<'_, Self>,
2330    ) {
2331        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2332            if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_selection, cx) {
2333                return;
2334            }
2335        }
2336        self.handle_project_entry_drop(&dragged_selection.active_selection.entry_id, cx);
2337    }
2338
2339    fn handle_project_entry_drop(
2340        &mut self,
2341        project_entry_id: &ProjectEntryId,
2342        cx: &mut ViewContext<'_, Self>,
2343    ) {
2344        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2345            if let ControlFlow::Break(()) = custom_drop_handle(self, project_entry_id, cx) {
2346                return;
2347            }
2348        }
2349        let mut to_pane = cx.view().clone();
2350        let split_direction = self.drag_split_direction;
2351        let project_entry_id = *project_entry_id;
2352        self.workspace
2353            .update(cx, |_, cx| {
2354                cx.defer(move |workspace, cx| {
2355                    if let Some(path) = workspace
2356                        .project()
2357                        .read(cx)
2358                        .path_for_entry(project_entry_id, cx)
2359                    {
2360                        let load_path_task = workspace.load_path(path, cx);
2361                        cx.spawn(|workspace, mut cx| async move {
2362                            if let Some((project_entry_id, build_item)) =
2363                                load_path_task.await.notify_async_err(&mut cx)
2364                            {
2365                                let (to_pane, new_item_handle) = workspace
2366                                    .update(&mut cx, |workspace, cx| {
2367                                        if let Some(split_direction) = split_direction {
2368                                            to_pane =
2369                                                workspace.split_pane(to_pane, split_direction, cx);
2370                                        }
2371                                        let new_item_handle = to_pane.update(cx, |pane, cx| {
2372                                            pane.open_item(
2373                                                project_entry_id,
2374                                                true,
2375                                                false,
2376                                                cx,
2377                                                build_item,
2378                                            )
2379                                        });
2380                                        (to_pane, new_item_handle)
2381                                    })
2382                                    .log_err()?;
2383                                to_pane
2384                                    .update(&mut cx, |this, cx| {
2385                                        let Some(index) = this.index_for_item(&*new_item_handle)
2386                                        else {
2387                                            return;
2388                                        };
2389                                        if !this.is_tab_pinned(index) {
2390                                            this.pin_tab_at(index, cx);
2391                                        }
2392                                    })
2393                                    .ok()?
2394                            }
2395                            Some(())
2396                        })
2397                        .detach();
2398                    };
2399                });
2400            })
2401            .log_err();
2402    }
2403
2404    fn handle_external_paths_drop(
2405        &mut self,
2406        paths: &ExternalPaths,
2407        cx: &mut ViewContext<'_, Self>,
2408    ) {
2409        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2410            if let ControlFlow::Break(()) = custom_drop_handle(self, paths, cx) {
2411                return;
2412            }
2413        }
2414        let mut to_pane = cx.view().clone();
2415        let mut split_direction = self.drag_split_direction;
2416        let paths = paths.paths().to_vec();
2417        let is_remote = self
2418            .workspace
2419            .update(cx, |workspace, cx| {
2420                if workspace.project().read(cx).is_via_collab() {
2421                    workspace.show_error(
2422                        &anyhow::anyhow!("Cannot drop files on a remote project"),
2423                        cx,
2424                    );
2425                    true
2426                } else {
2427                    false
2428                }
2429            })
2430            .unwrap_or(true);
2431        if is_remote {
2432            return;
2433        }
2434
2435        self.workspace
2436            .update(cx, |workspace, cx| {
2437                let fs = Arc::clone(workspace.project().read(cx).fs());
2438                cx.spawn(|workspace, mut cx| async move {
2439                    let mut is_file_checks = FuturesUnordered::new();
2440                    for path in &paths {
2441                        is_file_checks.push(fs.is_file(path))
2442                    }
2443                    let mut has_files_to_open = false;
2444                    while let Some(is_file) = is_file_checks.next().await {
2445                        if is_file {
2446                            has_files_to_open = true;
2447                            break;
2448                        }
2449                    }
2450                    drop(is_file_checks);
2451                    if !has_files_to_open {
2452                        split_direction = None;
2453                    }
2454
2455                    if let Ok(open_task) = workspace.update(&mut cx, |workspace, cx| {
2456                        if let Some(split_direction) = split_direction {
2457                            to_pane = workspace.split_pane(to_pane, split_direction, cx);
2458                        }
2459                        workspace.open_paths(
2460                            paths,
2461                            OpenVisible::OnlyDirectories,
2462                            Some(to_pane.downgrade()),
2463                            cx,
2464                        )
2465                    }) {
2466                        let opened_items: Vec<_> = open_task.await;
2467                        _ = workspace.update(&mut cx, |workspace, cx| {
2468                            for item in opened_items.into_iter().flatten() {
2469                                if let Err(e) = item {
2470                                    workspace.show_error(&e, cx);
2471                                }
2472                            }
2473                        });
2474                    }
2475                })
2476                .detach();
2477            })
2478            .log_err();
2479    }
2480
2481    pub fn display_nav_history_buttons(&mut self, display: Option<bool>) {
2482        self.display_nav_history_buttons = display;
2483    }
2484}
2485
2486impl FocusableView for Pane {
2487    fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
2488        self.focus_handle.clone()
2489    }
2490}
2491
2492impl Render for Pane {
2493    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2494        let mut key_context = KeyContext::new_with_defaults();
2495        key_context.add("Pane");
2496        if self.active_item().is_none() {
2497            key_context.add("EmptyPane");
2498        }
2499
2500        let should_display_tab_bar = self.should_display_tab_bar.clone();
2501        let display_tab_bar = should_display_tab_bar(cx);
2502
2503        v_flex()
2504            .key_context(key_context)
2505            .track_focus(&self.focus_handle)
2506            .size_full()
2507            .flex_none()
2508            .overflow_hidden()
2509            .on_action(cx.listener(|pane, _: &AlternateFile, cx| {
2510                pane.alternate_file(cx);
2511            }))
2512            .on_action(cx.listener(|pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx)))
2513            .on_action(cx.listener(|pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx)))
2514            .on_action(cx.listener(|pane, _: &SplitHorizontal, cx| {
2515                pane.split(SplitDirection::horizontal(cx), cx)
2516            }))
2517            .on_action(cx.listener(|pane, _: &SplitVertical, cx| {
2518                pane.split(SplitDirection::vertical(cx), cx)
2519            }))
2520            .on_action(
2521                cx.listener(|pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx)),
2522            )
2523            .on_action(cx.listener(|pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx)))
2524            .on_action(cx.listener(|pane, _: &GoBack, cx| pane.navigate_backward(cx)))
2525            .on_action(cx.listener(|pane, _: &GoForward, cx| pane.navigate_forward(cx)))
2526            .on_action(cx.listener(|pane, _: &JoinIntoNext, cx| pane.join_into_next(cx)))
2527            .on_action(cx.listener(Pane::toggle_zoom))
2528            .on_action(cx.listener(|pane: &mut Pane, action: &ActivateItem, cx| {
2529                pane.activate_item(action.0, true, true, cx);
2530            }))
2531            .on_action(cx.listener(|pane: &mut Pane, _: &ActivateLastItem, cx| {
2532                pane.activate_item(pane.items.len() - 1, true, true, cx);
2533            }))
2534            .on_action(cx.listener(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
2535                pane.activate_prev_item(true, cx);
2536            }))
2537            .on_action(cx.listener(|pane: &mut Pane, _: &ActivateNextItem, cx| {
2538                pane.activate_next_item(true, cx);
2539            }))
2540            .on_action(cx.listener(|pane, action, cx| {
2541                pane.toggle_pin_tab(action, cx);
2542            }))
2543            .when(PreviewTabsSettings::get_global(cx).enabled, |this| {
2544                this.on_action(cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, cx| {
2545                    if let Some(active_item_id) = pane.active_item().map(|i| i.item_id()) {
2546                        if pane.is_active_preview_item(active_item_id) {
2547                            pane.set_preview_item_id(None, cx);
2548                        } else {
2549                            pane.set_preview_item_id(Some(active_item_id), cx);
2550                        }
2551                    }
2552                }))
2553            })
2554            .on_action(
2555                cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
2556                    if let Some(task) = pane.close_active_item(action, cx) {
2557                        task.detach_and_log_err(cx)
2558                    }
2559                }),
2560            )
2561            .on_action(
2562                cx.listener(|pane: &mut Self, action: &CloseInactiveItems, cx| {
2563                    if let Some(task) = pane.close_inactive_items(action, cx) {
2564                        task.detach_and_log_err(cx)
2565                    }
2566                }),
2567            )
2568            .on_action(
2569                cx.listener(|pane: &mut Self, action: &CloseCleanItems, cx| {
2570                    if let Some(task) = pane.close_clean_items(action, cx) {
2571                        task.detach_and_log_err(cx)
2572                    }
2573                }),
2574            )
2575            .on_action(
2576                cx.listener(|pane: &mut Self, action: &CloseItemsToTheLeft, cx| {
2577                    if let Some(task) = pane.close_items_to_the_left(action, cx) {
2578                        task.detach_and_log_err(cx)
2579                    }
2580                }),
2581            )
2582            .on_action(
2583                cx.listener(|pane: &mut Self, action: &CloseItemsToTheRight, cx| {
2584                    if let Some(task) = pane.close_items_to_the_right(action, cx) {
2585                        task.detach_and_log_err(cx)
2586                    }
2587                }),
2588            )
2589            .on_action(cx.listener(|pane: &mut Self, action: &CloseAllItems, cx| {
2590                if let Some(task) = pane.close_all_items(action, cx) {
2591                    task.detach_and_log_err(cx)
2592                }
2593            }))
2594            .on_action(
2595                cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
2596                    if let Some(task) = pane.close_active_item(action, cx) {
2597                        task.detach_and_log_err(cx)
2598                    }
2599                }),
2600            )
2601            .on_action(
2602                cx.listener(|pane: &mut Self, action: &RevealInProjectPanel, cx| {
2603                    let entry_id = action
2604                        .entry_id
2605                        .map(ProjectEntryId::from_proto)
2606                        .or_else(|| pane.active_item()?.project_entry_ids(cx).first().copied());
2607                    if let Some(entry_id) = entry_id {
2608                        pane.project.update(cx, |_, cx| {
2609                            cx.emit(project::Event::RevealInProjectPanel(entry_id))
2610                        });
2611                    }
2612                }),
2613            )
2614            .when(self.active_item().is_some() && display_tab_bar, |pane| {
2615                pane.child(self.render_tab_bar(cx))
2616            })
2617            .child({
2618                let has_worktrees = self.project.read(cx).worktrees(cx).next().is_some();
2619                // main content
2620                div()
2621                    .flex_1()
2622                    .relative()
2623                    .group("")
2624                    .on_drag_move::<DraggedTab>(cx.listener(Self::handle_drag_move))
2625                    .on_drag_move::<DraggedSelection>(cx.listener(Self::handle_drag_move))
2626                    .on_drag_move::<ExternalPaths>(cx.listener(Self::handle_drag_move))
2627                    .map(|div| {
2628                        if let Some(item) = self.active_item() {
2629                            div.v_flex()
2630                                .child(self.toolbar.clone())
2631                                .child(item.to_any())
2632                        } else {
2633                            let placeholder = div.h_flex().size_full().justify_center();
2634                            if has_worktrees {
2635                                placeholder
2636                            } else {
2637                                placeholder.child(
2638                                    Label::new("Open a file or project to get started.")
2639                                        .color(Color::Muted),
2640                                )
2641                            }
2642                        }
2643                    })
2644                    .child(
2645                        // drag target
2646                        div()
2647                            .invisible()
2648                            .absolute()
2649                            .bg(cx.theme().colors().drop_target_background)
2650                            .group_drag_over::<DraggedTab>("", |style| style.visible())
2651                            .group_drag_over::<DraggedSelection>("", |style| style.visible())
2652                            .group_drag_over::<ExternalPaths>("", |style| style.visible())
2653                            .when_some(self.can_drop_predicate.clone(), |this, p| {
2654                                this.can_drop(move |a, cx| p(a, cx))
2655                            })
2656                            .on_drop(cx.listener(move |this, dragged_tab, cx| {
2657                                this.handle_tab_drop(dragged_tab, this.active_item_index(), cx)
2658                            }))
2659                            .on_drop(cx.listener(move |this, selection: &DraggedSelection, cx| {
2660                                this.handle_dragged_selection_drop(selection, cx)
2661                            }))
2662                            .on_drop(cx.listener(move |this, paths, cx| {
2663                                this.handle_external_paths_drop(paths, cx)
2664                            }))
2665                            .map(|div| {
2666                                let size = DefiniteLength::Fraction(0.5);
2667                                match self.drag_split_direction {
2668                                    None => div.top_0().right_0().bottom_0().left_0(),
2669                                    Some(SplitDirection::Up) => {
2670                                        div.top_0().left_0().right_0().h(size)
2671                                    }
2672                                    Some(SplitDirection::Down) => {
2673                                        div.left_0().bottom_0().right_0().h(size)
2674                                    }
2675                                    Some(SplitDirection::Left) => {
2676                                        div.top_0().left_0().bottom_0().w(size)
2677                                    }
2678                                    Some(SplitDirection::Right) => {
2679                                        div.top_0().bottom_0().right_0().w(size)
2680                                    }
2681                                }
2682                            }),
2683                    )
2684            })
2685            .on_mouse_down(
2686                MouseButton::Navigate(NavigationDirection::Back),
2687                cx.listener(|pane, _, cx| {
2688                    if let Some(workspace) = pane.workspace.upgrade() {
2689                        let pane = cx.view().downgrade();
2690                        cx.window_context().defer(move |cx| {
2691                            workspace.update(cx, |workspace, cx| {
2692                                workspace.go_back(pane, cx).detach_and_log_err(cx)
2693                            })
2694                        })
2695                    }
2696                }),
2697            )
2698            .on_mouse_down(
2699                MouseButton::Navigate(NavigationDirection::Forward),
2700                cx.listener(|pane, _, cx| {
2701                    if let Some(workspace) = pane.workspace.upgrade() {
2702                        let pane = cx.view().downgrade();
2703                        cx.window_context().defer(move |cx| {
2704                            workspace.update(cx, |workspace, cx| {
2705                                workspace.go_forward(pane, cx).detach_and_log_err(cx)
2706                            })
2707                        })
2708                    }
2709                }),
2710            )
2711    }
2712}
2713
2714impl ItemNavHistory {
2715    pub fn push<D: 'static + Send + Any>(&mut self, data: Option<D>, cx: &mut WindowContext) {
2716        self.history
2717            .push(data, self.item.clone(), self.is_preview, cx);
2718    }
2719
2720    pub fn pop_backward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
2721        self.history.pop(NavigationMode::GoingBack, cx)
2722    }
2723
2724    pub fn pop_forward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
2725        self.history.pop(NavigationMode::GoingForward, cx)
2726    }
2727}
2728
2729impl NavHistory {
2730    pub fn for_each_entry(
2731        &self,
2732        cx: &AppContext,
2733        mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
2734    ) {
2735        let borrowed_history = self.0.lock();
2736        borrowed_history
2737            .forward_stack
2738            .iter()
2739            .chain(borrowed_history.backward_stack.iter())
2740            .chain(borrowed_history.closed_stack.iter())
2741            .for_each(|entry| {
2742                if let Some(project_and_abs_path) =
2743                    borrowed_history.paths_by_item.get(&entry.item.id())
2744                {
2745                    f(entry, project_and_abs_path.clone());
2746                } else if let Some(item) = entry.item.upgrade() {
2747                    if let Some(path) = item.project_path(cx) {
2748                        f(entry, (path, None));
2749                    }
2750                }
2751            })
2752    }
2753
2754    pub fn set_mode(&mut self, mode: NavigationMode) {
2755        self.0.lock().mode = mode;
2756    }
2757
2758    pub fn mode(&self) -> NavigationMode {
2759        self.0.lock().mode
2760    }
2761
2762    pub fn disable(&mut self) {
2763        self.0.lock().mode = NavigationMode::Disabled;
2764    }
2765
2766    pub fn enable(&mut self) {
2767        self.0.lock().mode = NavigationMode::Normal;
2768    }
2769
2770    pub fn pop(&mut self, mode: NavigationMode, cx: &mut WindowContext) -> Option<NavigationEntry> {
2771        let mut state = self.0.lock();
2772        let entry = match mode {
2773            NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
2774                return None
2775            }
2776            NavigationMode::GoingBack => &mut state.backward_stack,
2777            NavigationMode::GoingForward => &mut state.forward_stack,
2778            NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
2779        }
2780        .pop_back();
2781        if entry.is_some() {
2782            state.did_update(cx);
2783        }
2784        entry
2785    }
2786
2787    pub fn push<D: 'static + Send + Any>(
2788        &mut self,
2789        data: Option<D>,
2790        item: Arc<dyn WeakItemHandle>,
2791        is_preview: bool,
2792        cx: &mut WindowContext,
2793    ) {
2794        let state = &mut *self.0.lock();
2795        match state.mode {
2796            NavigationMode::Disabled => {}
2797            NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
2798                if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2799                    state.backward_stack.pop_front();
2800                }
2801                state.backward_stack.push_back(NavigationEntry {
2802                    item,
2803                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2804                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2805                    is_preview,
2806                });
2807                state.forward_stack.clear();
2808            }
2809            NavigationMode::GoingBack => {
2810                if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2811                    state.forward_stack.pop_front();
2812                }
2813                state.forward_stack.push_back(NavigationEntry {
2814                    item,
2815                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2816                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2817                    is_preview,
2818                });
2819            }
2820            NavigationMode::GoingForward => {
2821                if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2822                    state.backward_stack.pop_front();
2823                }
2824                state.backward_stack.push_back(NavigationEntry {
2825                    item,
2826                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2827                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2828                    is_preview,
2829                });
2830            }
2831            NavigationMode::ClosingItem => {
2832                if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2833                    state.closed_stack.pop_front();
2834                }
2835                state.closed_stack.push_back(NavigationEntry {
2836                    item,
2837                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2838                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2839                    is_preview,
2840                });
2841            }
2842        }
2843        state.did_update(cx);
2844    }
2845
2846    pub fn remove_item(&mut self, item_id: EntityId) {
2847        let mut state = self.0.lock();
2848        state.paths_by_item.remove(&item_id);
2849        state
2850            .backward_stack
2851            .retain(|entry| entry.item.id() != item_id);
2852        state
2853            .forward_stack
2854            .retain(|entry| entry.item.id() != item_id);
2855        state
2856            .closed_stack
2857            .retain(|entry| entry.item.id() != item_id);
2858    }
2859
2860    pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option<PathBuf>)> {
2861        self.0.lock().paths_by_item.get(&item_id).cloned()
2862    }
2863}
2864
2865impl NavHistoryState {
2866    pub fn did_update(&self, cx: &mut WindowContext) {
2867        if let Some(pane) = self.pane.upgrade() {
2868            cx.defer(move |cx| {
2869                pane.update(cx, |pane, cx| pane.history_updated(cx));
2870            });
2871        }
2872    }
2873}
2874
2875fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
2876    let path = buffer_path
2877        .as_ref()
2878        .and_then(|p| {
2879            p.path
2880                .to_str()
2881                .and_then(|s| if s.is_empty() { None } else { Some(s) })
2882        })
2883        .unwrap_or("This buffer");
2884    let path = truncate_and_remove_front(path, 80);
2885    format!("{path} contains unsaved edits. Do you want to save it?")
2886}
2887
2888pub fn tab_details(items: &[Box<dyn ItemHandle>], cx: &AppContext) -> Vec<usize> {
2889    let mut tab_details = items.iter().map(|_| 0).collect::<Vec<_>>();
2890    let mut tab_descriptions = HashMap::default();
2891    let mut done = false;
2892    while !done {
2893        done = true;
2894
2895        // Store item indices by their tab description.
2896        for (ix, (item, detail)) in items.iter().zip(&tab_details).enumerate() {
2897            if let Some(description) = item.tab_description(*detail, cx) {
2898                if *detail == 0
2899                    || Some(&description) != item.tab_description(detail - 1, cx).as_ref()
2900                {
2901                    tab_descriptions
2902                        .entry(description)
2903                        .or_insert(Vec::new())
2904                        .push(ix);
2905                }
2906            }
2907        }
2908
2909        // If two or more items have the same tab description, increase their level
2910        // of detail and try again.
2911        for (_, item_ixs) in tab_descriptions.drain() {
2912            if item_ixs.len() > 1 {
2913                done = false;
2914                for ix in item_ixs {
2915                    tab_details[ix] += 1;
2916                }
2917            }
2918        }
2919    }
2920
2921    tab_details
2922}
2923
2924pub fn render_item_indicator(item: Box<dyn ItemHandle>, cx: &WindowContext) -> Option<Indicator> {
2925    maybe!({
2926        let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
2927            (true, _) => Color::Warning,
2928            (_, true) => Color::Accent,
2929            (false, false) => return None,
2930        };
2931
2932        Some(Indicator::dot().color(indicator_color))
2933    })
2934}
2935
2936impl Render for DraggedTab {
2937    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2938        let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
2939        let label = self.item.tab_content(
2940            TabContentParams {
2941                detail: Some(self.detail),
2942                selected: false,
2943                preview: false,
2944            },
2945            cx,
2946        );
2947        Tab::new("")
2948            .selected(self.is_active)
2949            .child(label)
2950            .render(cx)
2951            .font(ui_font)
2952    }
2953}
2954
2955#[cfg(test)]
2956mod tests {
2957    use super::*;
2958    use crate::item::test::{TestItem, TestProjectItem};
2959    use gpui::{TestAppContext, VisualTestContext};
2960    use project::FakeFs;
2961    use settings::SettingsStore;
2962    use theme::LoadThemes;
2963
2964    #[gpui::test]
2965    async fn test_remove_active_empty(cx: &mut TestAppContext) {
2966        init_test(cx);
2967        let fs = FakeFs::new(cx.executor());
2968
2969        let project = Project::test(fs, None, cx).await;
2970        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2971        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2972
2973        pane.update(cx, |pane, cx| {
2974            assert!(pane
2975                .close_active_item(&CloseActiveItem { save_intent: None }, cx)
2976                .is_none())
2977        });
2978    }
2979
2980    #[gpui::test]
2981    async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
2982        init_test(cx);
2983        let fs = FakeFs::new(cx.executor());
2984
2985        let project = Project::test(fs, None, cx).await;
2986        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2987        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2988
2989        // 1. Add with a destination index
2990        //   a. Add before the active item
2991        set_labeled_items(&pane, ["A", "B*", "C"], cx);
2992        pane.update(cx, |pane, cx| {
2993            pane.add_item(
2994                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2995                false,
2996                false,
2997                Some(0),
2998                cx,
2999            );
3000        });
3001        assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
3002
3003        //   b. Add after the active item
3004        set_labeled_items(&pane, ["A", "B*", "C"], cx);
3005        pane.update(cx, |pane, cx| {
3006            pane.add_item(
3007                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3008                false,
3009                false,
3010                Some(2),
3011                cx,
3012            );
3013        });
3014        assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3015
3016        //   c. Add at the end of the item list (including off the length)
3017        set_labeled_items(&pane, ["A", "B*", "C"], cx);
3018        pane.update(cx, |pane, cx| {
3019            pane.add_item(
3020                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3021                false,
3022                false,
3023                Some(5),
3024                cx,
3025            );
3026        });
3027        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3028
3029        // 2. Add without a destination index
3030        //   a. Add with active item at the start of the item list
3031        set_labeled_items(&pane, ["A*", "B", "C"], cx);
3032        pane.update(cx, |pane, cx| {
3033            pane.add_item(
3034                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3035                false,
3036                false,
3037                None,
3038                cx,
3039            );
3040        });
3041        set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
3042
3043        //   b. Add with active item at the end of the item list
3044        set_labeled_items(&pane, ["A", "B", "C*"], cx);
3045        pane.update(cx, |pane, cx| {
3046            pane.add_item(
3047                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3048                false,
3049                false,
3050                None,
3051                cx,
3052            );
3053        });
3054        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3055    }
3056
3057    #[gpui::test]
3058    async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
3059        init_test(cx);
3060        let fs = FakeFs::new(cx.executor());
3061
3062        let project = Project::test(fs, None, cx).await;
3063        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3064        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3065
3066        // 1. Add with a destination index
3067        //   1a. Add before the active item
3068        let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3069        pane.update(cx, |pane, cx| {
3070            pane.add_item(d, false, false, Some(0), cx);
3071        });
3072        assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
3073
3074        //   1b. Add after the active item
3075        let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3076        pane.update(cx, |pane, cx| {
3077            pane.add_item(d, false, false, Some(2), cx);
3078        });
3079        assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3080
3081        //   1c. Add at the end of the item list (including off the length)
3082        let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3083        pane.update(cx, |pane, cx| {
3084            pane.add_item(a, false, false, Some(5), cx);
3085        });
3086        assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3087
3088        //   1d. Add same item to active index
3089        let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3090        pane.update(cx, |pane, cx| {
3091            pane.add_item(b, false, false, Some(1), cx);
3092        });
3093        assert_item_labels(&pane, ["A", "B*", "C"], cx);
3094
3095        //   1e. Add item to index after same item in last position
3096        let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3097        pane.update(cx, |pane, cx| {
3098            pane.add_item(c, false, false, Some(2), cx);
3099        });
3100        assert_item_labels(&pane, ["A", "B", "C*"], cx);
3101
3102        // 2. Add without a destination index
3103        //   2a. Add with active item at the start of the item list
3104        let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
3105        pane.update(cx, |pane, cx| {
3106            pane.add_item(d, false, false, None, cx);
3107        });
3108        assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
3109
3110        //   2b. Add with active item at the end of the item list
3111        let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
3112        pane.update(cx, |pane, cx| {
3113            pane.add_item(a, false, false, None, cx);
3114        });
3115        assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3116
3117        //   2c. Add active item to active item at end of list
3118        let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
3119        pane.update(cx, |pane, cx| {
3120            pane.add_item(c, false, false, None, cx);
3121        });
3122        assert_item_labels(&pane, ["A", "B", "C*"], cx);
3123
3124        //   2d. Add active item to active item at start of list
3125        let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
3126        pane.update(cx, |pane, cx| {
3127            pane.add_item(a, false, false, None, cx);
3128        });
3129        assert_item_labels(&pane, ["A*", "B", "C"], cx);
3130    }
3131
3132    #[gpui::test]
3133    async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
3134        init_test(cx);
3135        let fs = FakeFs::new(cx.executor());
3136
3137        let project = Project::test(fs, None, cx).await;
3138        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3139        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3140
3141        // singleton view
3142        pane.update(cx, |pane, cx| {
3143            pane.add_item(
3144                Box::new(cx.new_view(|cx| {
3145                    TestItem::new(cx)
3146                        .with_singleton(true)
3147                        .with_label("buffer 1")
3148                        .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
3149                })),
3150                false,
3151                false,
3152                None,
3153                cx,
3154            );
3155        });
3156        assert_item_labels(&pane, ["buffer 1*"], cx);
3157
3158        // new singleton view with the same project entry
3159        pane.update(cx, |pane, cx| {
3160            pane.add_item(
3161                Box::new(cx.new_view(|cx| {
3162                    TestItem::new(cx)
3163                        .with_singleton(true)
3164                        .with_label("buffer 1")
3165                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3166                })),
3167                false,
3168                false,
3169                None,
3170                cx,
3171            );
3172        });
3173        assert_item_labels(&pane, ["buffer 1*"], cx);
3174
3175        // new singleton view with different project entry
3176        pane.update(cx, |pane, cx| {
3177            pane.add_item(
3178                Box::new(cx.new_view(|cx| {
3179                    TestItem::new(cx)
3180                        .with_singleton(true)
3181                        .with_label("buffer 2")
3182                        .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
3183                })),
3184                false,
3185                false,
3186                None,
3187                cx,
3188            );
3189        });
3190        assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
3191
3192        // new multibuffer view with the same project entry
3193        pane.update(cx, |pane, cx| {
3194            pane.add_item(
3195                Box::new(cx.new_view(|cx| {
3196                    TestItem::new(cx)
3197                        .with_singleton(false)
3198                        .with_label("multibuffer 1")
3199                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3200                })),
3201                false,
3202                false,
3203                None,
3204                cx,
3205            );
3206        });
3207        assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
3208
3209        // another multibuffer view with the same project entry
3210        pane.update(cx, |pane, cx| {
3211            pane.add_item(
3212                Box::new(cx.new_view(|cx| {
3213                    TestItem::new(cx)
3214                        .with_singleton(false)
3215                        .with_label("multibuffer 1b")
3216                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3217                })),
3218                false,
3219                false,
3220                None,
3221                cx,
3222            );
3223        });
3224        assert_item_labels(
3225            &pane,
3226            ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
3227            cx,
3228        );
3229    }
3230
3231    #[gpui::test]
3232    async fn test_remove_item_ordering(cx: &mut TestAppContext) {
3233        init_test(cx);
3234        let fs = FakeFs::new(cx.executor());
3235
3236        let project = Project::test(fs, None, cx).await;
3237        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3238        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3239
3240        add_labeled_item(&pane, "A", false, cx);
3241        add_labeled_item(&pane, "B", false, cx);
3242        add_labeled_item(&pane, "C", false, cx);
3243        add_labeled_item(&pane, "D", false, cx);
3244        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3245
3246        pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
3247        add_labeled_item(&pane, "1", false, cx);
3248        assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
3249
3250        pane.update(cx, |pane, cx| {
3251            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3252        })
3253        .unwrap()
3254        .await
3255        .unwrap();
3256        assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
3257
3258        pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
3259        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3260
3261        pane.update(cx, |pane, cx| {
3262            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3263        })
3264        .unwrap()
3265        .await
3266        .unwrap();
3267        assert_item_labels(&pane, ["A", "B*", "C"], cx);
3268
3269        pane.update(cx, |pane, cx| {
3270            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3271        })
3272        .unwrap()
3273        .await
3274        .unwrap();
3275        assert_item_labels(&pane, ["A", "C*"], cx);
3276
3277        pane.update(cx, |pane, cx| {
3278            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3279        })
3280        .unwrap()
3281        .await
3282        .unwrap();
3283        assert_item_labels(&pane, ["A*"], cx);
3284    }
3285
3286    #[gpui::test]
3287    async fn test_close_inactive_items(cx: &mut TestAppContext) {
3288        init_test(cx);
3289        let fs = FakeFs::new(cx.executor());
3290
3291        let project = Project::test(fs, None, cx).await;
3292        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3293        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3294
3295        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
3296
3297        pane.update(cx, |pane, cx| {
3298            pane.close_inactive_items(&CloseInactiveItems { save_intent: None }, cx)
3299        })
3300        .unwrap()
3301        .await
3302        .unwrap();
3303        assert_item_labels(&pane, ["C*"], cx);
3304    }
3305
3306    #[gpui::test]
3307    async fn test_close_clean_items(cx: &mut TestAppContext) {
3308        init_test(cx);
3309        let fs = FakeFs::new(cx.executor());
3310
3311        let project = Project::test(fs, None, cx).await;
3312        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3313        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3314
3315        add_labeled_item(&pane, "A", true, cx);
3316        add_labeled_item(&pane, "B", false, cx);
3317        add_labeled_item(&pane, "C", true, cx);
3318        add_labeled_item(&pane, "D", false, cx);
3319        add_labeled_item(&pane, "E", false, cx);
3320        assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
3321
3322        pane.update(cx, |pane, cx| pane.close_clean_items(&CloseCleanItems, cx))
3323            .unwrap()
3324            .await
3325            .unwrap();
3326        assert_item_labels(&pane, ["A^", "C*^"], cx);
3327    }
3328
3329    #[gpui::test]
3330    async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
3331        init_test(cx);
3332        let fs = FakeFs::new(cx.executor());
3333
3334        let project = Project::test(fs, None, cx).await;
3335        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3336        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3337
3338        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
3339
3340        pane.update(cx, |pane, cx| {
3341            pane.close_items_to_the_left(&CloseItemsToTheLeft, cx)
3342        })
3343        .unwrap()
3344        .await
3345        .unwrap();
3346        assert_item_labels(&pane, ["C*", "D", "E"], cx);
3347    }
3348
3349    #[gpui::test]
3350    async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
3351        init_test(cx);
3352        let fs = FakeFs::new(cx.executor());
3353
3354        let project = Project::test(fs, None, cx).await;
3355        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3356        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3357
3358        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
3359
3360        pane.update(cx, |pane, cx| {
3361            pane.close_items_to_the_right(&CloseItemsToTheRight, cx)
3362        })
3363        .unwrap()
3364        .await
3365        .unwrap();
3366        assert_item_labels(&pane, ["A", "B", "C*"], cx);
3367    }
3368
3369    #[gpui::test]
3370    async fn test_close_all_items(cx: &mut TestAppContext) {
3371        init_test(cx);
3372        let fs = FakeFs::new(cx.executor());
3373
3374        let project = Project::test(fs, None, cx).await;
3375        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3376        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3377
3378        add_labeled_item(&pane, "A", false, cx);
3379        add_labeled_item(&pane, "B", false, cx);
3380        add_labeled_item(&pane, "C", false, cx);
3381        assert_item_labels(&pane, ["A", "B", "C*"], cx);
3382
3383        pane.update(cx, |pane, cx| {
3384            pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
3385        })
3386        .unwrap()
3387        .await
3388        .unwrap();
3389        assert_item_labels(&pane, [], cx);
3390
3391        add_labeled_item(&pane, "A", true, cx);
3392        add_labeled_item(&pane, "B", true, cx);
3393        add_labeled_item(&pane, "C", true, cx);
3394        assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
3395
3396        let save = pane
3397            .update(cx, |pane, cx| {
3398                pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
3399            })
3400            .unwrap();
3401
3402        cx.executor().run_until_parked();
3403        cx.simulate_prompt_answer(2);
3404        save.await.unwrap();
3405        assert_item_labels(&pane, [], cx);
3406    }
3407
3408    fn init_test(cx: &mut TestAppContext) {
3409        cx.update(|cx| {
3410            let settings_store = SettingsStore::test(cx);
3411            cx.set_global(settings_store);
3412            theme::init(LoadThemes::JustBase, cx);
3413            crate::init_settings(cx);
3414            Project::init_settings(cx);
3415        });
3416    }
3417
3418    fn add_labeled_item(
3419        pane: &View<Pane>,
3420        label: &str,
3421        is_dirty: bool,
3422        cx: &mut VisualTestContext,
3423    ) -> Box<View<TestItem>> {
3424        pane.update(cx, |pane, cx| {
3425            let labeled_item = Box::new(
3426                cx.new_view(|cx| TestItem::new(cx).with_label(label).with_dirty(is_dirty)),
3427            );
3428            pane.add_item(labeled_item.clone(), false, false, None, cx);
3429            labeled_item
3430        })
3431    }
3432
3433    fn set_labeled_items<const COUNT: usize>(
3434        pane: &View<Pane>,
3435        labels: [&str; COUNT],
3436        cx: &mut VisualTestContext,
3437    ) -> [Box<View<TestItem>>; COUNT] {
3438        pane.update(cx, |pane, cx| {
3439            pane.items.clear();
3440            let mut active_item_index = 0;
3441
3442            let mut index = 0;
3443            let items = labels.map(|mut label| {
3444                if label.ends_with('*') {
3445                    label = label.trim_end_matches('*');
3446                    active_item_index = index;
3447                }
3448
3449                let labeled_item = Box::new(cx.new_view(|cx| TestItem::new(cx).with_label(label)));
3450                pane.add_item(labeled_item.clone(), false, false, None, cx);
3451                index += 1;
3452                labeled_item
3453            });
3454
3455            pane.activate_item(active_item_index, false, false, cx);
3456
3457            items
3458        })
3459    }
3460
3461    // Assert the item label, with the active item label suffixed with a '*'
3462    fn assert_item_labels<const COUNT: usize>(
3463        pane: &View<Pane>,
3464        expected_states: [&str; COUNT],
3465        cx: &mut VisualTestContext,
3466    ) {
3467        pane.update(cx, |pane, cx| {
3468            let actual_states = pane
3469                .items
3470                .iter()
3471                .enumerate()
3472                .map(|(ix, item)| {
3473                    let mut state = item
3474                        .to_any()
3475                        .downcast::<TestItem>()
3476                        .unwrap()
3477                        .read(cx)
3478                        .label
3479                        .clone();
3480                    if ix == pane.active_item_index {
3481                        state.push('*');
3482                    }
3483                    if item.is_dirty(cx) {
3484                        state.push('^');
3485                    }
3486                    state
3487                })
3488                .collect::<Vec<_>>();
3489
3490            assert_eq!(
3491                actual_states, expected_states,
3492                "pane items do not match expectation"
3493            );
3494        })
3495    }
3496}