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