pane.rs

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