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