pane.rs

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