pane.rs

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