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