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