pane.rs

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