pane.rs

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