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