pane.rs

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