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    CloseWindow, 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            // Close the window when there's no active items to close, if configured
 952            if WorkspaceSettings::get_global(cx)
 953                .when_closing_with_no_tabs
 954                .should_close()
 955            {
 956                cx.dispatch_action(Box::new(CloseWindow));
 957            }
 958
 959            return None;
 960        }
 961        let active_item_id = self.items[self.active_item_index].item_id();
 962        Some(self.close_item_by_id(
 963            active_item_id,
 964            action.save_intent.unwrap_or(SaveIntent::Close),
 965            cx,
 966        ))
 967    }
 968
 969    pub fn close_item_by_id(
 970        &mut self,
 971        item_id_to_close: EntityId,
 972        save_intent: SaveIntent,
 973        cx: &mut ViewContext<Self>,
 974    ) -> Task<Result<()>> {
 975        self.close_items(cx, save_intent, move |view_id| view_id == item_id_to_close)
 976    }
 977
 978    pub fn close_inactive_items(
 979        &mut self,
 980        action: &CloseInactiveItems,
 981        cx: &mut ViewContext<Self>,
 982    ) -> Option<Task<Result<()>>> {
 983        if self.items.is_empty() {
 984            return None;
 985        }
 986
 987        let active_item_id = self.items[self.active_item_index].item_id();
 988        Some(self.close_items(
 989            cx,
 990            action.save_intent.unwrap_or(SaveIntent::Close),
 991            move |item_id| item_id != active_item_id,
 992        ))
 993    }
 994
 995    pub fn close_clean_items(
 996        &mut self,
 997        _: &CloseCleanItems,
 998        cx: &mut ViewContext<Self>,
 999    ) -> Option<Task<Result<()>>> {
1000        let item_ids: Vec<_> = self
1001            .items()
1002            .filter(|item| !item.is_dirty(cx))
1003            .map(|item| item.item_id())
1004            .collect();
1005        Some(self.close_items(cx, SaveIntent::Close, move |item_id| {
1006            item_ids.contains(&item_id)
1007        }))
1008    }
1009
1010    pub fn close_items_to_the_left(
1011        &mut self,
1012        _: &CloseItemsToTheLeft,
1013        cx: &mut ViewContext<Self>,
1014    ) -> Option<Task<Result<()>>> {
1015        if self.items.is_empty() {
1016            return None;
1017        }
1018        let active_item_id = self.items[self.active_item_index].item_id();
1019        Some(self.close_items_to_the_left_by_id(active_item_id, cx))
1020    }
1021
1022    pub fn close_items_to_the_left_by_id(
1023        &mut self,
1024        item_id: EntityId,
1025        cx: &mut ViewContext<Self>,
1026    ) -> Task<Result<()>> {
1027        let item_ids: Vec<_> = self
1028            .items()
1029            .take_while(|item| item.item_id() != item_id)
1030            .map(|item| item.item_id())
1031            .collect();
1032        self.close_items(cx, SaveIntent::Close, move |item_id| {
1033            item_ids.contains(&item_id)
1034        })
1035    }
1036
1037    pub fn close_items_to_the_right(
1038        &mut self,
1039        _: &CloseItemsToTheRight,
1040        cx: &mut ViewContext<Self>,
1041    ) -> Option<Task<Result<()>>> {
1042        if self.items.is_empty() {
1043            return None;
1044        }
1045        let active_item_id = self.items[self.active_item_index].item_id();
1046        Some(self.close_items_to_the_right_by_id(active_item_id, cx))
1047    }
1048
1049    pub fn close_items_to_the_right_by_id(
1050        &mut self,
1051        item_id: EntityId,
1052        cx: &mut ViewContext<Self>,
1053    ) -> Task<Result<()>> {
1054        let item_ids: Vec<_> = self
1055            .items()
1056            .rev()
1057            .take_while(|item| item.item_id() != item_id)
1058            .map(|item| item.item_id())
1059            .collect();
1060        self.close_items(cx, SaveIntent::Close, move |item_id| {
1061            item_ids.contains(&item_id)
1062        })
1063    }
1064
1065    pub fn close_all_items(
1066        &mut self,
1067        action: &CloseAllItems,
1068        cx: &mut ViewContext<Self>,
1069    ) -> Option<Task<Result<()>>> {
1070        if self.items.is_empty() {
1071            return None;
1072        }
1073
1074        Some(
1075            self.close_items(cx, action.save_intent.unwrap_or(SaveIntent::Close), |_| {
1076                true
1077            }),
1078        )
1079    }
1080
1081    pub(super) fn file_names_for_prompt(
1082        items: &mut dyn Iterator<Item = &Box<dyn ItemHandle>>,
1083        all_dirty_items: usize,
1084        cx: &AppContext,
1085    ) -> (String, String) {
1086        /// Quantity of item paths displayed in prompt prior to cutoff..
1087        const FILE_NAMES_CUTOFF_POINT: usize = 10;
1088        let mut file_names: Vec<_> = items
1089            .filter_map(|item| {
1090                item.project_path(cx).and_then(|project_path| {
1091                    project_path
1092                        .path
1093                        .file_name()
1094                        .and_then(|name| name.to_str().map(ToOwned::to_owned))
1095                })
1096            })
1097            .take(FILE_NAMES_CUTOFF_POINT)
1098            .collect();
1099        let should_display_followup_text =
1100            all_dirty_items > FILE_NAMES_CUTOFF_POINT || file_names.len() != all_dirty_items;
1101        if should_display_followup_text {
1102            let not_shown_files = all_dirty_items - file_names.len();
1103            if not_shown_files == 1 {
1104                file_names.push(".. 1 file not shown".into());
1105            } else {
1106                file_names.push(format!(".. {} files not shown", not_shown_files));
1107            }
1108        }
1109        (
1110            format!(
1111                "Do you want to save changes to the following {} files?",
1112                all_dirty_items
1113            ),
1114            file_names.join("\n"),
1115        )
1116    }
1117
1118    pub fn close_items(
1119        &mut self,
1120        cx: &mut ViewContext<Pane>,
1121        mut save_intent: SaveIntent,
1122        should_close: impl Fn(EntityId) -> bool,
1123    ) -> Task<Result<()>> {
1124        // Find the items to close.
1125        let mut items_to_close = Vec::new();
1126        let mut dirty_items = Vec::new();
1127        for item in &self.items {
1128            if should_close(item.item_id()) {
1129                items_to_close.push(item.boxed_clone());
1130                if item.is_dirty(cx) {
1131                    dirty_items.push(item.boxed_clone());
1132                }
1133            }
1134        }
1135
1136        // If a buffer is open both in a singleton editor and in a multibuffer, make sure
1137        // to focus the singleton buffer when prompting to save that buffer, as opposed
1138        // to focusing the multibuffer, because this gives the user a more clear idea
1139        // of what content they would be saving.
1140        items_to_close.sort_by_key(|item| !item.is_singleton(cx));
1141
1142        let workspace = self.workspace.clone();
1143        cx.spawn(|pane, mut cx| async move {
1144            if save_intent == SaveIntent::Close && dirty_items.len() > 1 {
1145                let answer = pane.update(&mut cx, |_, cx| {
1146                    let (prompt, detail) =
1147                        Self::file_names_for_prompt(&mut dirty_items.iter(), dirty_items.len(), cx);
1148                    cx.prompt(
1149                        PromptLevel::Warning,
1150                        &prompt,
1151                        Some(&detail),
1152                        &["Save all", "Discard all", "Cancel"],
1153                    )
1154                })?;
1155                match answer.await {
1156                    Ok(0) => save_intent = SaveIntent::SaveAll,
1157                    Ok(1) => save_intent = SaveIntent::Skip,
1158                    _ => {}
1159                }
1160            }
1161            let mut saved_project_items_ids = HashSet::default();
1162            for item in items_to_close.clone() {
1163                // Find the item's current index and its set of project item models. Avoid
1164                // storing these in advance, in case they have changed since this task
1165                // was started.
1166                let (item_ix, mut project_item_ids) = pane.update(&mut cx, |pane, cx| {
1167                    (pane.index_for_item(&*item), item.project_item_model_ids(cx))
1168                })?;
1169                let item_ix = if let Some(ix) = item_ix {
1170                    ix
1171                } else {
1172                    continue;
1173                };
1174
1175                // Check if this view has any project items that are not open anywhere else
1176                // in the workspace, AND that the user has not already been prompted to save.
1177                // If there are any such project entries, prompt the user to save this item.
1178                let project = workspace.update(&mut cx, |workspace, cx| {
1179                    for item in workspace.items(cx) {
1180                        if !items_to_close
1181                            .iter()
1182                            .any(|item_to_close| item_to_close.item_id() == item.item_id())
1183                        {
1184                            let other_project_item_ids = item.project_item_model_ids(cx);
1185                            project_item_ids.retain(|id| !other_project_item_ids.contains(id));
1186                        }
1187                    }
1188                    workspace.project().clone()
1189                })?;
1190                let should_save = project_item_ids
1191                    .iter()
1192                    .any(|id| saved_project_items_ids.insert(*id));
1193
1194                if should_save
1195                    && !Self::save_item(
1196                        project.clone(),
1197                        &pane,
1198                        item_ix,
1199                        &*item,
1200                        save_intent,
1201                        &mut cx,
1202                    )
1203                    .await?
1204                {
1205                    break;
1206                }
1207
1208                // Remove the item from the pane.
1209                pane.update(&mut cx, |pane, cx| {
1210                    if let Some(item_ix) = pane
1211                        .items
1212                        .iter()
1213                        .position(|i| i.item_id() == item.item_id())
1214                    {
1215                        pane.remove_item(item_ix, false, true, cx);
1216                    }
1217                })
1218                .ok();
1219            }
1220
1221            pane.update(&mut cx, |_, cx| cx.notify()).ok();
1222            Ok(())
1223        })
1224    }
1225
1226    pub fn remove_item(
1227        &mut self,
1228        item_index: usize,
1229        activate_pane: bool,
1230        close_pane_if_empty: bool,
1231        cx: &mut ViewContext<Self>,
1232    ) {
1233        self.activation_history
1234            .retain(|entry| entry.entity_id != self.items[item_index].item_id());
1235
1236        if item_index == self.active_item_index {
1237            let index_to_activate = self
1238                .activation_history
1239                .pop()
1240                .and_then(|last_activated_item| {
1241                    self.items.iter().enumerate().find_map(|(index, item)| {
1242                        (item.item_id() == last_activated_item.entity_id).then_some(index)
1243                    })
1244                })
1245                // We didn't have a valid activation history entry, so fallback
1246                // to activating the item to the left
1247                .unwrap_or_else(|| item_index.min(self.items.len()).saturating_sub(1));
1248
1249            let should_activate = activate_pane || self.has_focus(cx);
1250            if self.items.len() == 1 && should_activate {
1251                self.focus_handle.focus(cx);
1252            } else {
1253                self.activate_item(index_to_activate, should_activate, should_activate, cx);
1254            }
1255        }
1256
1257        let item = self.items.remove(item_index);
1258
1259        cx.emit(Event::RemoveItem {
1260            item_id: item.item_id(),
1261        });
1262        if self.items.is_empty() {
1263            item.deactivated(cx);
1264            if close_pane_if_empty {
1265                self.update_toolbar(cx);
1266                cx.emit(Event::Remove);
1267            }
1268        }
1269
1270        if item_index < self.active_item_index {
1271            self.active_item_index -= 1;
1272        }
1273
1274        let mode = self.nav_history.mode();
1275        self.nav_history.set_mode(NavigationMode::ClosingItem);
1276        item.deactivated(cx);
1277        self.nav_history.set_mode(mode);
1278
1279        if self.is_active_preview_item(item.item_id()) {
1280            self.set_preview_item_id(None, cx);
1281        }
1282
1283        if let Some(path) = item.project_path(cx) {
1284            let abs_path = self
1285                .nav_history
1286                .0
1287                .lock()
1288                .paths_by_item
1289                .get(&item.item_id())
1290                .and_then(|(_, abs_path)| abs_path.clone());
1291
1292            self.nav_history
1293                .0
1294                .lock()
1295                .paths_by_item
1296                .insert(item.item_id(), (path, abs_path));
1297        } else {
1298            self.nav_history
1299                .0
1300                .lock()
1301                .paths_by_item
1302                .remove(&item.item_id());
1303        }
1304
1305        if self.items.is_empty() && close_pane_if_empty && self.zoomed {
1306            cx.emit(Event::ZoomOut);
1307        }
1308
1309        cx.notify();
1310    }
1311
1312    pub async fn save_item(
1313        project: Model<Project>,
1314        pane: &WeakView<Pane>,
1315        item_ix: usize,
1316        item: &dyn ItemHandle,
1317        save_intent: SaveIntent,
1318        cx: &mut AsyncWindowContext,
1319    ) -> Result<bool> {
1320        const CONFLICT_MESSAGE: &str =
1321                "This file has changed on disk since you started editing it. Do you want to overwrite it?";
1322
1323        if save_intent == SaveIntent::Skip {
1324            return Ok(true);
1325        }
1326
1327        let (mut has_conflict, mut is_dirty, mut can_save, can_save_as) = cx.update(|cx| {
1328            (
1329                item.has_conflict(cx),
1330                item.is_dirty(cx),
1331                item.can_save(cx),
1332                item.is_singleton(cx),
1333            )
1334        })?;
1335
1336        // when saving a single buffer, we ignore whether or not it's dirty.
1337        if save_intent == SaveIntent::Save || save_intent == SaveIntent::SaveWithoutFormat {
1338            is_dirty = true;
1339        }
1340
1341        if save_intent == SaveIntent::SaveAs {
1342            is_dirty = true;
1343            has_conflict = false;
1344            can_save = false;
1345        }
1346
1347        if save_intent == SaveIntent::Overwrite {
1348            has_conflict = false;
1349        }
1350
1351        let should_format = save_intent != SaveIntent::SaveWithoutFormat;
1352
1353        if has_conflict && can_save {
1354            let answer = pane.update(cx, |pane, cx| {
1355                pane.activate_item(item_ix, true, true, cx);
1356                cx.prompt(
1357                    PromptLevel::Warning,
1358                    CONFLICT_MESSAGE,
1359                    None,
1360                    &["Overwrite", "Discard", "Cancel"],
1361                )
1362            })?;
1363            match answer.await {
1364                Ok(0) => {
1365                    pane.update(cx, |_, cx| item.save(should_format, project, cx))?
1366                        .await?
1367                }
1368                Ok(1) => pane.update(cx, |_, cx| item.reload(project, cx))?.await?,
1369                _ => return Ok(false),
1370            }
1371        } else if is_dirty && (can_save || can_save_as) {
1372            if save_intent == SaveIntent::Close {
1373                let will_autosave = cx.update(|cx| {
1374                    matches!(
1375                        WorkspaceSettings::get_global(cx).autosave,
1376                        AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange
1377                    ) && Self::can_autosave_item(item, cx)
1378                })?;
1379                if !will_autosave {
1380                    let item_id = item.item_id();
1381                    let answer_task = pane.update(cx, |pane, cx| {
1382                        if pane.save_modals_spawned.insert(item_id) {
1383                            pane.activate_item(item_ix, true, true, cx);
1384                            let prompt = dirty_message_for(item.project_path(cx));
1385                            Some(cx.prompt(
1386                                PromptLevel::Warning,
1387                                &prompt,
1388                                None,
1389                                &["Save", "Don't Save", "Cancel"],
1390                            ))
1391                        } else {
1392                            None
1393                        }
1394                    })?;
1395                    if let Some(answer_task) = answer_task {
1396                        let answer = answer_task.await;
1397                        pane.update(cx, |pane, _| {
1398                            if !pane.save_modals_spawned.remove(&item_id) {
1399                                debug_panic!(
1400                                    "save modal was not present in spawned modals after awaiting for its answer"
1401                                )
1402                            }
1403                        })?;
1404                        match answer {
1405                            Ok(0) => {}
1406                            Ok(1) => return Ok(true), // Don't save this file
1407                            _ => return Ok(false),    // Cancel
1408                        }
1409                    } else {
1410                        return Ok(false);
1411                    }
1412                }
1413            }
1414
1415            if can_save {
1416                pane.update(cx, |_, cx| item.save(should_format, project, cx))?
1417                    .await?;
1418            } else if can_save_as {
1419                let abs_path = pane.update(cx, |pane, cx| {
1420                    pane.workspace
1421                        .update(cx, |workspace, cx| workspace.prompt_for_new_path(cx))
1422                })??;
1423                if let Some(abs_path) = abs_path.await.ok().flatten() {
1424                    pane.update(cx, |_, cx| item.save_as(project, abs_path, cx))?
1425                        .await?;
1426                } else {
1427                    return Ok(false);
1428                }
1429            }
1430        }
1431        Ok(true)
1432    }
1433
1434    fn can_autosave_item(item: &dyn ItemHandle, cx: &AppContext) -> bool {
1435        let is_deleted = item.project_entry_ids(cx).is_empty();
1436        item.is_dirty(cx) && !item.has_conflict(cx) && item.can_save(cx) && !is_deleted
1437    }
1438
1439    pub fn autosave_item(
1440        item: &dyn ItemHandle,
1441        project: Model<Project>,
1442        cx: &mut WindowContext,
1443    ) -> Task<Result<()>> {
1444        let format = if let AutosaveSetting::AfterDelay { .. } =
1445            WorkspaceSettings::get_global(cx).autosave
1446        {
1447            false
1448        } else {
1449            true
1450        };
1451        if Self::can_autosave_item(item, cx) {
1452            item.save(format, project, cx)
1453        } else {
1454            Task::ready(Ok(()))
1455        }
1456    }
1457
1458    pub fn focus(&mut self, cx: &mut ViewContext<Pane>) {
1459        cx.focus(&self.focus_handle);
1460    }
1461
1462    pub fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
1463        if let Some(active_item) = self.active_item() {
1464            let focus_handle = active_item.focus_handle(cx);
1465            cx.focus(&focus_handle);
1466        }
1467    }
1468
1469    pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext<Self>) {
1470        cx.emit(Event::Split(direction));
1471    }
1472
1473    pub fn toolbar(&self) -> &View<Toolbar> {
1474        &self.toolbar
1475    }
1476
1477    pub fn handle_deleted_project_item(
1478        &mut self,
1479        entry_id: ProjectEntryId,
1480        cx: &mut ViewContext<Pane>,
1481    ) -> Option<()> {
1482        let (item_index_to_delete, item_id) = self.items().enumerate().find_map(|(i, item)| {
1483            if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
1484                Some((i, item.item_id()))
1485            } else {
1486                None
1487            }
1488        })?;
1489
1490        self.remove_item(item_index_to_delete, false, true, cx);
1491        self.nav_history.remove_item(item_id);
1492
1493        Some(())
1494    }
1495
1496    fn update_toolbar(&mut self, cx: &mut ViewContext<Self>) {
1497        let active_item = self
1498            .items
1499            .get(self.active_item_index)
1500            .map(|item| item.as_ref());
1501        self.toolbar.update(cx, |toolbar, cx| {
1502            toolbar.set_active_item(active_item, cx);
1503        });
1504    }
1505
1506    fn update_status_bar(&mut self, cx: &mut ViewContext<Self>) {
1507        let workspace = self.workspace.clone();
1508        let pane = cx.view().clone();
1509
1510        cx.window_context().defer(move |cx| {
1511            let Ok(status_bar) = workspace.update(cx, |workspace, _| workspace.status_bar.clone())
1512            else {
1513                return;
1514            };
1515
1516            status_bar.update(cx, move |status_bar, cx| {
1517                status_bar.set_active_pane(&pane, cx);
1518            });
1519        });
1520    }
1521
1522    fn render_tab(
1523        &self,
1524        ix: usize,
1525        item: &Box<dyn ItemHandle>,
1526        detail: usize,
1527        cx: &mut ViewContext<'_, Pane>,
1528    ) -> impl IntoElement {
1529        let is_active = ix == self.active_item_index;
1530        let is_preview = self
1531            .preview_item_id
1532            .map(|id| id == item.item_id())
1533            .unwrap_or(false);
1534
1535        let label = item.tab_content(
1536            TabContentParams {
1537                detail: Some(detail),
1538                selected: is_active,
1539                preview: is_preview,
1540            },
1541            cx,
1542        );
1543        let close_side = &ItemSettings::get_global(cx).close_position;
1544        let indicator = render_item_indicator(item.boxed_clone(), cx);
1545        let item_id = item.item_id();
1546        let is_first_item = ix == 0;
1547        let is_last_item = ix == self.items.len() - 1;
1548        let position_relative_to_active_item = ix.cmp(&self.active_item_index);
1549
1550        let tab = Tab::new(ix)
1551            .position(if is_first_item {
1552                TabPosition::First
1553            } else if is_last_item {
1554                TabPosition::Last
1555            } else {
1556                TabPosition::Middle(position_relative_to_active_item)
1557            })
1558            .close_side(match close_side {
1559                ClosePosition::Left => ui::TabCloseSide::Start,
1560                ClosePosition::Right => ui::TabCloseSide::End,
1561            })
1562            .selected(is_active)
1563            .on_click(
1564                cx.listener(move |pane: &mut Self, _, cx| pane.activate_item(ix, true, true, cx)),
1565            )
1566            // TODO: This should be a click listener with the middle mouse button instead of a mouse down listener.
1567            .on_mouse_down(
1568                MouseButton::Middle,
1569                cx.listener(move |pane, _event, cx| {
1570                    pane.close_item_by_id(item_id, SaveIntent::Close, cx)
1571                        .detach_and_log_err(cx);
1572                }),
1573            )
1574            .on_mouse_down(
1575                MouseButton::Left,
1576                cx.listener(move |pane, event: &MouseDownEvent, cx| {
1577                    if let Some(id) = pane.preview_item_id {
1578                        if id == item_id && event.click_count > 1 {
1579                            pane.set_preview_item_id(None, cx);
1580                        }
1581                    }
1582                }),
1583            )
1584            .on_drag(
1585                DraggedTab {
1586                    item: item.boxed_clone(),
1587                    pane: cx.view().clone(),
1588                    detail,
1589                    is_active,
1590                    ix,
1591                },
1592                |tab, cx| cx.new_view(|_| tab.clone()),
1593            )
1594            .drag_over::<DraggedTab>(|tab, _, cx| {
1595                tab.bg(cx.theme().colors().drop_target_background)
1596            })
1597            .drag_over::<ProjectEntryId>(|tab, _, cx| {
1598                tab.bg(cx.theme().colors().drop_target_background)
1599            })
1600            .when_some(self.can_drop_predicate.clone(), |this, p| {
1601                this.can_drop(move |a, cx| p(a, cx))
1602            })
1603            .on_drop(cx.listener(move |this, dragged_tab: &DraggedTab, cx| {
1604                this.drag_split_direction = None;
1605                this.handle_tab_drop(dragged_tab, ix, cx)
1606            }))
1607            .on_drop(cx.listener(move |this, entry_id: &ProjectEntryId, cx| {
1608                this.drag_split_direction = None;
1609                this.handle_project_entry_drop(entry_id, cx)
1610            }))
1611            .on_drop(cx.listener(move |this, paths, cx| {
1612                this.drag_split_direction = None;
1613                this.handle_external_paths_drop(paths, cx)
1614            }))
1615            .when_some(item.tab_tooltip_text(cx), |tab, text| {
1616                tab.tooltip(move |cx| Tooltip::text(text.clone(), cx))
1617            })
1618            .start_slot::<Indicator>(indicator)
1619            .end_slot(
1620                IconButton::new("close tab", IconName::Close)
1621                    .shape(IconButtonShape::Square)
1622                    .icon_color(Color::Muted)
1623                    .size(ButtonSize::None)
1624                    .icon_size(IconSize::XSmall)
1625                    .on_click(cx.listener(move |pane, _, cx| {
1626                        pane.close_item_by_id(item_id, SaveIntent::Close, cx)
1627                            .detach_and_log_err(cx);
1628                    })),
1629            )
1630            .child(label);
1631
1632        let single_entry_to_resolve = {
1633            let item_entries = self.items[ix].project_entry_ids(cx);
1634            if item_entries.len() == 1 {
1635                Some(item_entries[0])
1636            } else {
1637                None
1638            }
1639        };
1640
1641        let pane = cx.view().downgrade();
1642        right_click_menu(ix).trigger(tab).menu(move |cx| {
1643            let pane = pane.clone();
1644            ContextMenu::build(cx, move |mut menu, cx| {
1645                if let Some(pane) = pane.upgrade() {
1646                    menu = menu
1647                        .entry(
1648                            "Close",
1649                            Some(Box::new(CloseActiveItem { save_intent: None })),
1650                            cx.handler_for(&pane, move |pane, cx| {
1651                                pane.close_item_by_id(item_id, SaveIntent::Close, cx)
1652                                    .detach_and_log_err(cx);
1653                            }),
1654                        )
1655                        .entry(
1656                            "Close Others",
1657                            Some(Box::new(CloseInactiveItems { save_intent: None })),
1658                            cx.handler_for(&pane, move |pane, cx| {
1659                                pane.close_items(cx, SaveIntent::Close, |id| id != item_id)
1660                                    .detach_and_log_err(cx);
1661                            }),
1662                        )
1663                        .separator()
1664                        .entry(
1665                            "Close Left",
1666                            Some(Box::new(CloseItemsToTheLeft)),
1667                            cx.handler_for(&pane, move |pane, cx| {
1668                                pane.close_items_to_the_left_by_id(item_id, cx)
1669                                    .detach_and_log_err(cx);
1670                            }),
1671                        )
1672                        .entry(
1673                            "Close Right",
1674                            Some(Box::new(CloseItemsToTheRight)),
1675                            cx.handler_for(&pane, move |pane, cx| {
1676                                pane.close_items_to_the_right_by_id(item_id, cx)
1677                                    .detach_and_log_err(cx);
1678                            }),
1679                        )
1680                        .separator()
1681                        .entry(
1682                            "Close Clean",
1683                            Some(Box::new(CloseCleanItems)),
1684                            cx.handler_for(&pane, move |pane, cx| {
1685                                if let Some(task) = pane.close_clean_items(&CloseCleanItems, cx) {
1686                                    task.detach_and_log_err(cx)
1687                                }
1688                            }),
1689                        )
1690                        .entry(
1691                            "Close All",
1692                            Some(Box::new(CloseAllItems { save_intent: None })),
1693                            cx.handler_for(&pane, |pane, cx| {
1694                                if let Some(task) =
1695                                    pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
1696                                {
1697                                    task.detach_and_log_err(cx)
1698                                }
1699                            }),
1700                        );
1701
1702                    if let Some(entry) = single_entry_to_resolve {
1703                        let parent_abs_path = pane
1704                            .update(cx, |pane, cx| {
1705                                pane.workspace.update(cx, |workspace, cx| {
1706                                    let project = workspace.project().read(cx);
1707                                    project.worktree_for_entry(entry, cx).and_then(|worktree| {
1708                                        let worktree = worktree.read(cx);
1709                                        let entry = worktree.entry_for_id(entry)?;
1710                                        let abs_path = worktree.absolutize(&entry.path).ok()?;
1711                                        let parent = if entry.is_symlink {
1712                                            abs_path.canonicalize().ok()?
1713                                        } else {
1714                                            abs_path
1715                                        }
1716                                        .parent()?
1717                                        .to_path_buf();
1718                                        Some(parent)
1719                                    })
1720                                })
1721                            })
1722                            .ok()
1723                            .flatten();
1724
1725                        let entry_id = entry.to_proto();
1726                        menu = menu
1727                            .separator()
1728                            .entry(
1729                                "Reveal In Project Panel",
1730                                Some(Box::new(RevealInProjectPanel {
1731                                    entry_id: Some(entry_id),
1732                                })),
1733                                cx.handler_for(&pane, move |pane, cx| {
1734                                    pane.project.update(cx, |_, cx| {
1735                                        cx.emit(project::Event::RevealInProjectPanel(
1736                                            ProjectEntryId::from_proto(entry_id),
1737                                        ))
1738                                    });
1739                                }),
1740                            )
1741                            .when_some(parent_abs_path, |menu, abs_path| {
1742                                menu.entry(
1743                                    "Open in Terminal",
1744                                    Some(Box::new(OpenInTerminal)),
1745                                    cx.handler_for(&pane, move |_, cx| {
1746                                        cx.dispatch_action(
1747                                            OpenTerminal {
1748                                                working_directory: abs_path.clone(),
1749                                            }
1750                                            .boxed_clone(),
1751                                        );
1752                                    }),
1753                                )
1754                            });
1755                    }
1756                }
1757
1758                menu
1759            })
1760        })
1761    }
1762
1763    fn render_tab_bar(&mut self, cx: &mut ViewContext<'_, Pane>) -> impl IntoElement {
1764        let navigate_backward = IconButton::new("navigate_backward", IconName::ArrowLeft)
1765            .shape(IconButtonShape::Square)
1766            .icon_size(IconSize::Small)
1767            .on_click({
1768                let view = cx.view().clone();
1769                move |_, cx| view.update(cx, Self::navigate_backward)
1770            })
1771            .disabled(!self.can_navigate_backward())
1772            .tooltip(|cx| Tooltip::for_action("Go Back", &GoBack, cx));
1773
1774        let navigate_forward = IconButton::new("navigate_forward", IconName::ArrowRight)
1775            .shape(IconButtonShape::Square)
1776            .icon_size(IconSize::Small)
1777            .on_click({
1778                let view = cx.view().clone();
1779                move |_, cx| view.update(cx, Self::navigate_forward)
1780            })
1781            .disabled(!self.can_navigate_forward())
1782            .tooltip(|cx| Tooltip::for_action("Go Forward", &GoForward, cx));
1783
1784        TabBar::new("tab_bar")
1785            .track_scroll(self.tab_bar_scroll_handle.clone())
1786            .when(
1787                self.display_nav_history_buttons.unwrap_or_default(),
1788                |tab_bar| tab_bar.start_children(vec![navigate_backward, navigate_forward]),
1789            )
1790            .when(self.has_focus(cx), |tab_bar| {
1791                tab_bar.end_child({
1792                    let render_tab_buttons = self.render_tab_bar_buttons.clone();
1793                    render_tab_buttons(self, cx)
1794                })
1795            })
1796            .children(
1797                self.items
1798                    .iter()
1799                    .enumerate()
1800                    .zip(tab_details(&self.items, cx))
1801                    .map(|((ix, item), detail)| self.render_tab(ix, item, detail, cx)),
1802            )
1803            .child(
1804                div()
1805                    .id("tab_bar_drop_target")
1806                    .min_w_6()
1807                    // HACK: This empty child is currently necessary to force the drop target to appear
1808                    // despite us setting a min width above.
1809                    .child("")
1810                    .h_full()
1811                    .flex_grow()
1812                    .drag_over::<DraggedTab>(|bar, _, cx| {
1813                        bar.bg(cx.theme().colors().drop_target_background)
1814                    })
1815                    .drag_over::<ProjectEntryId>(|bar, _, cx| {
1816                        bar.bg(cx.theme().colors().drop_target_background)
1817                    })
1818                    .on_drop(cx.listener(move |this, dragged_tab: &DraggedTab, cx| {
1819                        this.drag_split_direction = None;
1820                        this.handle_tab_drop(dragged_tab, this.items.len(), cx)
1821                    }))
1822                    .on_drop(cx.listener(move |this, entry_id: &ProjectEntryId, cx| {
1823                        this.drag_split_direction = None;
1824                        this.handle_project_entry_drop(entry_id, cx)
1825                    }))
1826                    .on_drop(cx.listener(move |this, paths, cx| {
1827                        this.drag_split_direction = None;
1828                        this.handle_external_paths_drop(paths, cx)
1829                    }))
1830                    .on_click(cx.listener(move |this, event: &ClickEvent, cx| {
1831                        if event.up.click_count == 2 {
1832                            cx.dispatch_action(this.double_click_dispatch_action.boxed_clone())
1833                        }
1834                    })),
1835            )
1836    }
1837
1838    pub fn render_menu_overlay(menu: &View<ContextMenu>) -> Div {
1839        div().absolute().bottom_0().right_0().size_0().child(
1840            deferred(
1841                anchored()
1842                    .anchor(AnchorCorner::TopRight)
1843                    .child(menu.clone()),
1844            )
1845            .with_priority(1),
1846        )
1847    }
1848
1849    pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
1850        self.zoomed = zoomed;
1851        cx.notify();
1852    }
1853
1854    pub fn is_zoomed(&self) -> bool {
1855        self.zoomed
1856    }
1857
1858    fn handle_drag_move<T>(&mut self, event: &DragMoveEvent<T>, cx: &mut ViewContext<Self>) {
1859        if !self.can_split {
1860            return;
1861        }
1862
1863        let rect = event.bounds.size;
1864
1865        let size = event.bounds.size.width.min(event.bounds.size.height)
1866            * WorkspaceSettings::get_global(cx).drop_target_size;
1867
1868        let relative_cursor = Point::new(
1869            event.event.position.x - event.bounds.left(),
1870            event.event.position.y - event.bounds.top(),
1871        );
1872
1873        let direction = if relative_cursor.x < size
1874            || relative_cursor.x > rect.width - size
1875            || relative_cursor.y < size
1876            || relative_cursor.y > rect.height - size
1877        {
1878            [
1879                SplitDirection::Up,
1880                SplitDirection::Right,
1881                SplitDirection::Down,
1882                SplitDirection::Left,
1883            ]
1884            .iter()
1885            .min_by_key(|side| match side {
1886                SplitDirection::Up => relative_cursor.y,
1887                SplitDirection::Right => rect.width - relative_cursor.x,
1888                SplitDirection::Down => rect.height - relative_cursor.y,
1889                SplitDirection::Left => relative_cursor.x,
1890            })
1891            .cloned()
1892        } else {
1893            None
1894        };
1895
1896        if direction != self.drag_split_direction {
1897            self.drag_split_direction = direction;
1898        }
1899    }
1900
1901    fn handle_tab_drop(
1902        &mut self,
1903        dragged_tab: &DraggedTab,
1904        ix: usize,
1905        cx: &mut ViewContext<'_, Self>,
1906    ) {
1907        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
1908            if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_tab, cx) {
1909                return;
1910            }
1911        }
1912        let mut to_pane = cx.view().clone();
1913        let split_direction = self.drag_split_direction;
1914        let item_id = dragged_tab.item.item_id();
1915        if let Some(preview_item_id) = self.preview_item_id {
1916            if item_id == preview_item_id {
1917                self.set_preview_item_id(None, cx);
1918            }
1919        }
1920
1921        let from_pane = dragged_tab.pane.clone();
1922        self.workspace
1923            .update(cx, |_, cx| {
1924                cx.defer(move |workspace, cx| {
1925                    if let Some(split_direction) = split_direction {
1926                        to_pane = workspace.split_pane(to_pane, split_direction, cx);
1927                    }
1928                    workspace.move_item(from_pane, to_pane, item_id, ix, cx);
1929                });
1930            })
1931            .log_err();
1932    }
1933
1934    fn handle_project_entry_drop(
1935        &mut self,
1936        project_entry_id: &ProjectEntryId,
1937        cx: &mut ViewContext<'_, Self>,
1938    ) {
1939        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
1940            if let ControlFlow::Break(()) = custom_drop_handle(self, project_entry_id, cx) {
1941                return;
1942            }
1943        }
1944        let mut to_pane = cx.view().clone();
1945        let split_direction = self.drag_split_direction;
1946        let project_entry_id = *project_entry_id;
1947        self.workspace
1948            .update(cx, |_, cx| {
1949                cx.defer(move |workspace, cx| {
1950                    if let Some(path) = workspace
1951                        .project()
1952                        .read(cx)
1953                        .path_for_entry(project_entry_id, cx)
1954                    {
1955                        if let Some(split_direction) = split_direction {
1956                            to_pane = workspace.split_pane(to_pane, split_direction, cx);
1957                        }
1958                        workspace
1959                            .open_path(path, Some(to_pane.downgrade()), true, cx)
1960                            .detach_and_log_err(cx);
1961                    }
1962                });
1963            })
1964            .log_err();
1965    }
1966
1967    fn handle_external_paths_drop(
1968        &mut self,
1969        paths: &ExternalPaths,
1970        cx: &mut ViewContext<'_, Self>,
1971    ) {
1972        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
1973            if let ControlFlow::Break(()) = custom_drop_handle(self, paths, cx) {
1974                return;
1975            }
1976        }
1977        let mut to_pane = cx.view().clone();
1978        let mut split_direction = self.drag_split_direction;
1979        let paths = paths.paths().to_vec();
1980        let is_remote = self
1981            .workspace
1982            .update(cx, |workspace, cx| {
1983                if workspace.project().read(cx).is_remote() {
1984                    workspace.show_error(
1985                        &anyhow::anyhow!("Cannot drop files on a remote project"),
1986                        cx,
1987                    );
1988                    true
1989                } else {
1990                    false
1991                }
1992            })
1993            .unwrap_or(true);
1994        if is_remote {
1995            return;
1996        }
1997
1998        self.workspace
1999            .update(cx, |workspace, cx| {
2000                let fs = Arc::clone(workspace.project().read(cx).fs());
2001                cx.spawn(|workspace, mut cx| async move {
2002                    let mut is_file_checks = FuturesUnordered::new();
2003                    for path in &paths {
2004                        is_file_checks.push(fs.is_file(path))
2005                    }
2006                    let mut has_files_to_open = false;
2007                    while let Some(is_file) = is_file_checks.next().await {
2008                        if is_file {
2009                            has_files_to_open = true;
2010                            break;
2011                        }
2012                    }
2013                    drop(is_file_checks);
2014                    if !has_files_to_open {
2015                        split_direction = None;
2016                    }
2017
2018                    if let Some(open_task) = workspace
2019                        .update(&mut cx, |workspace, cx| {
2020                            if let Some(split_direction) = split_direction {
2021                                to_pane = workspace.split_pane(to_pane, split_direction, cx);
2022                            }
2023                            workspace.open_paths(
2024                                paths,
2025                                OpenVisible::OnlyDirectories,
2026                                Some(to_pane.downgrade()),
2027                                cx,
2028                            )
2029                        })
2030                        .ok()
2031                    {
2032                        let _opened_items: Vec<_> = open_task.await;
2033                    }
2034                })
2035                .detach();
2036            })
2037            .log_err();
2038    }
2039
2040    pub fn display_nav_history_buttons(&mut self, display: Option<bool>) {
2041        self.display_nav_history_buttons = display;
2042    }
2043}
2044
2045impl FocusableView for Pane {
2046    fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
2047        self.focus_handle.clone()
2048    }
2049}
2050
2051impl Render for Pane {
2052    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2053        let mut key_context = KeyContext::new_with_defaults();
2054        key_context.add("Pane");
2055        if self.active_item().is_none() {
2056            key_context.add("EmptyPane");
2057        }
2058
2059        let should_display_tab_bar = self.should_display_tab_bar.clone();
2060        let display_tab_bar = should_display_tab_bar(cx);
2061
2062        v_flex()
2063            .key_context(key_context)
2064            .track_focus(&self.focus_handle)
2065            .size_full()
2066            .flex_none()
2067            .overflow_hidden()
2068            .on_action(cx.listener(|pane, _: &AlternateFile, cx| {
2069                pane.alternate_file(cx);
2070            }))
2071            .on_action(cx.listener(|pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx)))
2072            .on_action(cx.listener(|pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx)))
2073            .on_action(
2074                cx.listener(|pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx)),
2075            )
2076            .on_action(cx.listener(|pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx)))
2077            .on_action(cx.listener(|pane, _: &GoBack, cx| pane.navigate_backward(cx)))
2078            .on_action(cx.listener(|pane, _: &GoForward, cx| pane.navigate_forward(cx)))
2079            .on_action(cx.listener(Pane::toggle_zoom))
2080            .on_action(cx.listener(|pane: &mut Pane, action: &ActivateItem, cx| {
2081                pane.activate_item(action.0, true, true, cx);
2082            }))
2083            .on_action(cx.listener(|pane: &mut Pane, _: &ActivateLastItem, cx| {
2084                pane.activate_item(pane.items.len() - 1, true, true, cx);
2085            }))
2086            .on_action(cx.listener(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
2087                pane.activate_prev_item(true, cx);
2088            }))
2089            .on_action(cx.listener(|pane: &mut Pane, _: &ActivateNextItem, cx| {
2090                pane.activate_next_item(true, cx);
2091            }))
2092            .when(PreviewTabsSettings::get_global(cx).enabled, |this| {
2093                this.on_action(cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, cx| {
2094                    if let Some(active_item_id) = pane.active_item().map(|i| i.item_id()) {
2095                        if pane.is_active_preview_item(active_item_id) {
2096                            pane.set_preview_item_id(None, cx);
2097                        } else {
2098                            pane.set_preview_item_id(Some(active_item_id), cx);
2099                        }
2100                    }
2101                }))
2102            })
2103            .on_action(
2104                cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
2105                    if let Some(task) = pane.close_active_item(action, cx) {
2106                        task.detach_and_log_err(cx)
2107                    }
2108                }),
2109            )
2110            .on_action(
2111                cx.listener(|pane: &mut Self, action: &CloseInactiveItems, cx| {
2112                    if let Some(task) = pane.close_inactive_items(action, cx) {
2113                        task.detach_and_log_err(cx)
2114                    }
2115                }),
2116            )
2117            .on_action(
2118                cx.listener(|pane: &mut Self, action: &CloseCleanItems, cx| {
2119                    if let Some(task) = pane.close_clean_items(action, cx) {
2120                        task.detach_and_log_err(cx)
2121                    }
2122                }),
2123            )
2124            .on_action(
2125                cx.listener(|pane: &mut Self, action: &CloseItemsToTheLeft, cx| {
2126                    if let Some(task) = pane.close_items_to_the_left(action, cx) {
2127                        task.detach_and_log_err(cx)
2128                    }
2129                }),
2130            )
2131            .on_action(
2132                cx.listener(|pane: &mut Self, action: &CloseItemsToTheRight, cx| {
2133                    if let Some(task) = pane.close_items_to_the_right(action, cx) {
2134                        task.detach_and_log_err(cx)
2135                    }
2136                }),
2137            )
2138            .on_action(cx.listener(|pane: &mut Self, action: &CloseAllItems, cx| {
2139                if let Some(task) = pane.close_all_items(action, cx) {
2140                    task.detach_and_log_err(cx)
2141                }
2142            }))
2143            .on_action(
2144                cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
2145                    if let Some(task) = pane.close_active_item(action, cx) {
2146                        task.detach_and_log_err(cx)
2147                    }
2148                }),
2149            )
2150            .on_action(
2151                cx.listener(|pane: &mut Self, action: &RevealInProjectPanel, cx| {
2152                    let entry_id = action
2153                        .entry_id
2154                        .map(ProjectEntryId::from_proto)
2155                        .or_else(|| pane.active_item()?.project_entry_ids(cx).first().copied());
2156                    if let Some(entry_id) = entry_id {
2157                        pane.project.update(cx, |_, cx| {
2158                            cx.emit(project::Event::RevealInProjectPanel(entry_id))
2159                        });
2160                    }
2161                }),
2162            )
2163            .when(self.active_item().is_some() && display_tab_bar, |pane| {
2164                pane.child(self.render_tab_bar(cx))
2165            })
2166            .child({
2167                let has_worktrees = self.project.read(cx).worktrees().next().is_some();
2168                // main content
2169                div()
2170                    .flex_1()
2171                    .relative()
2172                    .group("")
2173                    .on_drag_move::<DraggedTab>(cx.listener(Self::handle_drag_move))
2174                    .on_drag_move::<ProjectEntryId>(cx.listener(Self::handle_drag_move))
2175                    .on_drag_move::<ExternalPaths>(cx.listener(Self::handle_drag_move))
2176                    .map(|div| {
2177                        if let Some(item) = self.active_item() {
2178                            div.v_flex()
2179                                .child(self.toolbar.clone())
2180                                .child(item.to_any())
2181                        } else {
2182                            let placeholder = div.h_flex().size_full().justify_center();
2183                            if has_worktrees {
2184                                placeholder
2185                            } else {
2186                                placeholder.child(
2187                                    Label::new("Open a file or project to get started.")
2188                                        .color(Color::Muted),
2189                                )
2190                            }
2191                        }
2192                    })
2193                    .child(
2194                        // drag target
2195                        div()
2196                            .invisible()
2197                            .absolute()
2198                            .bg(cx.theme().colors().drop_target_background)
2199                            .group_drag_over::<DraggedTab>("", |style| style.visible())
2200                            .group_drag_over::<ProjectEntryId>("", |style| style.visible())
2201                            .group_drag_over::<ExternalPaths>("", |style| style.visible())
2202                            .when_some(self.can_drop_predicate.clone(), |this, p| {
2203                                this.can_drop(move |a, cx| p(a, cx))
2204                            })
2205                            .on_drop(cx.listener(move |this, dragged_tab, cx| {
2206                                this.handle_tab_drop(dragged_tab, this.active_item_index(), cx)
2207                            }))
2208                            .on_drop(cx.listener(move |this, entry_id, cx| {
2209                                this.handle_project_entry_drop(entry_id, cx)
2210                            }))
2211                            .on_drop(cx.listener(move |this, paths, cx| {
2212                                this.handle_external_paths_drop(paths, cx)
2213                            }))
2214                            .map(|div| {
2215                                let size = DefiniteLength::Fraction(0.5);
2216                                match self.drag_split_direction {
2217                                    None => div.top_0().right_0().bottom_0().left_0(),
2218                                    Some(SplitDirection::Up) => {
2219                                        div.top_0().left_0().right_0().h(size)
2220                                    }
2221                                    Some(SplitDirection::Down) => {
2222                                        div.left_0().bottom_0().right_0().h(size)
2223                                    }
2224                                    Some(SplitDirection::Left) => {
2225                                        div.top_0().left_0().bottom_0().w(size)
2226                                    }
2227                                    Some(SplitDirection::Right) => {
2228                                        div.top_0().bottom_0().right_0().w(size)
2229                                    }
2230                                }
2231                            }),
2232                    )
2233            })
2234            .on_mouse_down(
2235                MouseButton::Navigate(NavigationDirection::Back),
2236                cx.listener(|pane, _, cx| {
2237                    if let Some(workspace) = pane.workspace.upgrade() {
2238                        let pane = cx.view().downgrade();
2239                        cx.window_context().defer(move |cx| {
2240                            workspace.update(cx, |workspace, cx| {
2241                                workspace.go_back(pane, cx).detach_and_log_err(cx)
2242                            })
2243                        })
2244                    }
2245                }),
2246            )
2247            .on_mouse_down(
2248                MouseButton::Navigate(NavigationDirection::Forward),
2249                cx.listener(|pane, _, cx| {
2250                    if let Some(workspace) = pane.workspace.upgrade() {
2251                        let pane = cx.view().downgrade();
2252                        cx.window_context().defer(move |cx| {
2253                            workspace.update(cx, |workspace, cx| {
2254                                workspace.go_forward(pane, cx).detach_and_log_err(cx)
2255                            })
2256                        })
2257                    }
2258                }),
2259            )
2260    }
2261}
2262
2263impl ItemNavHistory {
2264    pub fn push<D: 'static + Send + Any>(&mut self, data: Option<D>, cx: &mut WindowContext) {
2265        self.history
2266            .push(data, self.item.clone(), self.is_preview, cx);
2267    }
2268
2269    pub fn pop_backward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
2270        self.history.pop(NavigationMode::GoingBack, cx)
2271    }
2272
2273    pub fn pop_forward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
2274        self.history.pop(NavigationMode::GoingForward, cx)
2275    }
2276}
2277
2278impl NavHistory {
2279    pub fn for_each_entry(
2280        &self,
2281        cx: &AppContext,
2282        mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
2283    ) {
2284        let borrowed_history = self.0.lock();
2285        borrowed_history
2286            .forward_stack
2287            .iter()
2288            .chain(borrowed_history.backward_stack.iter())
2289            .chain(borrowed_history.closed_stack.iter())
2290            .for_each(|entry| {
2291                if let Some(project_and_abs_path) =
2292                    borrowed_history.paths_by_item.get(&entry.item.id())
2293                {
2294                    f(entry, project_and_abs_path.clone());
2295                } else if let Some(item) = entry.item.upgrade() {
2296                    if let Some(path) = item.project_path(cx) {
2297                        f(entry, (path, None));
2298                    }
2299                }
2300            })
2301    }
2302
2303    pub fn set_mode(&mut self, mode: NavigationMode) {
2304        self.0.lock().mode = mode;
2305    }
2306
2307    pub fn mode(&self) -> NavigationMode {
2308        self.0.lock().mode
2309    }
2310
2311    pub fn disable(&mut self) {
2312        self.0.lock().mode = NavigationMode::Disabled;
2313    }
2314
2315    pub fn enable(&mut self) {
2316        self.0.lock().mode = NavigationMode::Normal;
2317    }
2318
2319    pub fn pop(&mut self, mode: NavigationMode, cx: &mut WindowContext) -> Option<NavigationEntry> {
2320        let mut state = self.0.lock();
2321        let entry = match mode {
2322            NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
2323                return None
2324            }
2325            NavigationMode::GoingBack => &mut state.backward_stack,
2326            NavigationMode::GoingForward => &mut state.forward_stack,
2327            NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
2328        }
2329        .pop_back();
2330        if entry.is_some() {
2331            state.did_update(cx);
2332        }
2333        entry
2334    }
2335
2336    pub fn push<D: 'static + Send + Any>(
2337        &mut self,
2338        data: Option<D>,
2339        item: Arc<dyn WeakItemHandle>,
2340        is_preview: bool,
2341        cx: &mut WindowContext,
2342    ) {
2343        let state = &mut *self.0.lock();
2344        match state.mode {
2345            NavigationMode::Disabled => {}
2346            NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
2347                if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2348                    state.backward_stack.pop_front();
2349                }
2350                state.backward_stack.push_back(NavigationEntry {
2351                    item,
2352                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2353                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2354                    is_preview,
2355                });
2356                state.forward_stack.clear();
2357            }
2358            NavigationMode::GoingBack => {
2359                if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2360                    state.forward_stack.pop_front();
2361                }
2362                state.forward_stack.push_back(NavigationEntry {
2363                    item,
2364                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2365                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2366                    is_preview,
2367                });
2368            }
2369            NavigationMode::GoingForward => {
2370                if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2371                    state.backward_stack.pop_front();
2372                }
2373                state.backward_stack.push_back(NavigationEntry {
2374                    item,
2375                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2376                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2377                    is_preview,
2378                });
2379            }
2380            NavigationMode::ClosingItem => {
2381                if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2382                    state.closed_stack.pop_front();
2383                }
2384                state.closed_stack.push_back(NavigationEntry {
2385                    item,
2386                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2387                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2388                    is_preview,
2389                });
2390            }
2391        }
2392        state.did_update(cx);
2393    }
2394
2395    pub fn remove_item(&mut self, item_id: EntityId) {
2396        let mut state = self.0.lock();
2397        state.paths_by_item.remove(&item_id);
2398        state
2399            .backward_stack
2400            .retain(|entry| entry.item.id() != item_id);
2401        state
2402            .forward_stack
2403            .retain(|entry| entry.item.id() != item_id);
2404        state
2405            .closed_stack
2406            .retain(|entry| entry.item.id() != item_id);
2407    }
2408
2409    pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option<PathBuf>)> {
2410        self.0.lock().paths_by_item.get(&item_id).cloned()
2411    }
2412}
2413
2414impl NavHistoryState {
2415    pub fn did_update(&self, cx: &mut WindowContext) {
2416        if let Some(pane) = self.pane.upgrade() {
2417            cx.defer(move |cx| {
2418                pane.update(cx, |pane, cx| pane.history_updated(cx));
2419            });
2420        }
2421    }
2422}
2423
2424fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
2425    let path = buffer_path
2426        .as_ref()
2427        .and_then(|p| {
2428            p.path
2429                .to_str()
2430                .and_then(|s| if s == "" { None } else { Some(s) })
2431        })
2432        .unwrap_or("This buffer");
2433    let path = truncate_and_remove_front(path, 80);
2434    format!("{path} contains unsaved edits. Do you want to save it?")
2435}
2436
2437pub fn tab_details(items: &Vec<Box<dyn ItemHandle>>, cx: &AppContext) -> Vec<usize> {
2438    let mut tab_details = items.iter().map(|_| 0).collect::<Vec<_>>();
2439    let mut tab_descriptions = HashMap::default();
2440    let mut done = false;
2441    while !done {
2442        done = true;
2443
2444        // Store item indices by their tab description.
2445        for (ix, (item, detail)) in items.iter().zip(&tab_details).enumerate() {
2446            if let Some(description) = item.tab_description(*detail, cx) {
2447                if *detail == 0
2448                    || Some(&description) != item.tab_description(detail - 1, cx).as_ref()
2449                {
2450                    tab_descriptions
2451                        .entry(description)
2452                        .or_insert(Vec::new())
2453                        .push(ix);
2454                }
2455            }
2456        }
2457
2458        // If two or more items have the same tab description, increase their level
2459        // of detail and try again.
2460        for (_, item_ixs) in tab_descriptions.drain() {
2461            if item_ixs.len() > 1 {
2462                done = false;
2463                for ix in item_ixs {
2464                    tab_details[ix] += 1;
2465                }
2466            }
2467        }
2468    }
2469
2470    tab_details
2471}
2472
2473pub fn render_item_indicator(item: Box<dyn ItemHandle>, cx: &WindowContext) -> Option<Indicator> {
2474    maybe!({
2475        let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
2476            (true, _) => Color::Warning,
2477            (_, true) => Color::Accent,
2478            (false, false) => return None,
2479        };
2480
2481        Some(Indicator::dot().color(indicator_color))
2482    })
2483}
2484
2485#[cfg(test)]
2486mod tests {
2487    use super::*;
2488    use crate::item::test::{TestItem, TestProjectItem};
2489    use gpui::{TestAppContext, VisualTestContext};
2490    use project::FakeFs;
2491    use settings::SettingsStore;
2492    use theme::LoadThemes;
2493
2494    #[gpui::test]
2495    async fn test_remove_active_empty(cx: &mut TestAppContext) {
2496        init_test(cx);
2497        let fs = FakeFs::new(cx.executor());
2498
2499        let project = Project::test(fs, None, cx).await;
2500        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2501        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2502
2503        pane.update(cx, |pane, cx| {
2504            assert!(pane
2505                .close_active_item(&CloseActiveItem { save_intent: None }, cx)
2506                .is_none())
2507        });
2508    }
2509
2510    #[gpui::test]
2511    async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
2512        init_test(cx);
2513        let fs = FakeFs::new(cx.executor());
2514
2515        let project = Project::test(fs, None, cx).await;
2516        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2517        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2518
2519        // 1. Add with a destination index
2520        //   a. Add before the active item
2521        set_labeled_items(&pane, ["A", "B*", "C"], cx);
2522        pane.update(cx, |pane, cx| {
2523            pane.add_item(
2524                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2525                false,
2526                false,
2527                Some(0),
2528                cx,
2529            );
2530        });
2531        assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
2532
2533        //   b. Add after the active item
2534        set_labeled_items(&pane, ["A", "B*", "C"], cx);
2535        pane.update(cx, |pane, cx| {
2536            pane.add_item(
2537                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2538                false,
2539                false,
2540                Some(2),
2541                cx,
2542            );
2543        });
2544        assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
2545
2546        //   c. Add at the end of the item list (including off the length)
2547        set_labeled_items(&pane, ["A", "B*", "C"], cx);
2548        pane.update(cx, |pane, cx| {
2549            pane.add_item(
2550                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2551                false,
2552                false,
2553                Some(5),
2554                cx,
2555            );
2556        });
2557        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2558
2559        // 2. Add without a destination index
2560        //   a. Add with active item at the start of the item list
2561        set_labeled_items(&pane, ["A*", "B", "C"], cx);
2562        pane.update(cx, |pane, cx| {
2563            pane.add_item(
2564                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2565                false,
2566                false,
2567                None,
2568                cx,
2569            );
2570        });
2571        set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
2572
2573        //   b. Add with active item at the end of the item list
2574        set_labeled_items(&pane, ["A", "B", "C*"], cx);
2575        pane.update(cx, |pane, cx| {
2576            pane.add_item(
2577                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2578                false,
2579                false,
2580                None,
2581                cx,
2582            );
2583        });
2584        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2585    }
2586
2587    #[gpui::test]
2588    async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
2589        init_test(cx);
2590        let fs = FakeFs::new(cx.executor());
2591
2592        let project = Project::test(fs, None, cx).await;
2593        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2594        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2595
2596        // 1. Add with a destination index
2597        //   1a. Add before the active item
2598        let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
2599        pane.update(cx, |pane, cx| {
2600            pane.add_item(d, false, false, Some(0), cx);
2601        });
2602        assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
2603
2604        //   1b. Add after the active item
2605        let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
2606        pane.update(cx, |pane, cx| {
2607            pane.add_item(d, false, false, Some(2), cx);
2608        });
2609        assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
2610
2611        //   1c. Add at the end of the item list (including off the length)
2612        let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
2613        pane.update(cx, |pane, cx| {
2614            pane.add_item(a, false, false, Some(5), cx);
2615        });
2616        assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
2617
2618        //   1d. Add same item to active index
2619        let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
2620        pane.update(cx, |pane, cx| {
2621            pane.add_item(b, false, false, Some(1), cx);
2622        });
2623        assert_item_labels(&pane, ["A", "B*", "C"], cx);
2624
2625        //   1e. Add item to index after same item in last position
2626        let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
2627        pane.update(cx, |pane, cx| {
2628            pane.add_item(c, false, false, Some(2), cx);
2629        });
2630        assert_item_labels(&pane, ["A", "B", "C*"], cx);
2631
2632        // 2. Add without a destination index
2633        //   2a. Add with active item at the start of the item list
2634        let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
2635        pane.update(cx, |pane, cx| {
2636            pane.add_item(d, false, false, None, cx);
2637        });
2638        assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
2639
2640        //   2b. Add with active item at the end of the item list
2641        let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
2642        pane.update(cx, |pane, cx| {
2643            pane.add_item(a, false, false, None, cx);
2644        });
2645        assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
2646
2647        //   2c. Add active item to active item at end of list
2648        let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
2649        pane.update(cx, |pane, cx| {
2650            pane.add_item(c, false, false, None, cx);
2651        });
2652        assert_item_labels(&pane, ["A", "B", "C*"], cx);
2653
2654        //   2d. Add active item to active item at start of list
2655        let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
2656        pane.update(cx, |pane, cx| {
2657            pane.add_item(a, false, false, None, cx);
2658        });
2659        assert_item_labels(&pane, ["A*", "B", "C"], cx);
2660    }
2661
2662    #[gpui::test]
2663    async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
2664        init_test(cx);
2665        let fs = FakeFs::new(cx.executor());
2666
2667        let project = Project::test(fs, None, cx).await;
2668        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2669        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2670
2671        // singleton view
2672        pane.update(cx, |pane, cx| {
2673            pane.add_item(
2674                Box::new(cx.new_view(|cx| {
2675                    TestItem::new(cx)
2676                        .with_singleton(true)
2677                        .with_label("buffer 1")
2678                        .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
2679                })),
2680                false,
2681                false,
2682                None,
2683                cx,
2684            );
2685        });
2686        assert_item_labels(&pane, ["buffer 1*"], cx);
2687
2688        // new singleton view with the same project entry
2689        pane.update(cx, |pane, cx| {
2690            pane.add_item(
2691                Box::new(cx.new_view(|cx| {
2692                    TestItem::new(cx)
2693                        .with_singleton(true)
2694                        .with_label("buffer 1")
2695                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
2696                })),
2697                false,
2698                false,
2699                None,
2700                cx,
2701            );
2702        });
2703        assert_item_labels(&pane, ["buffer 1*"], cx);
2704
2705        // new singleton view with different project entry
2706        pane.update(cx, |pane, cx| {
2707            pane.add_item(
2708                Box::new(cx.new_view(|cx| {
2709                    TestItem::new(cx)
2710                        .with_singleton(true)
2711                        .with_label("buffer 2")
2712                        .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
2713                })),
2714                false,
2715                false,
2716                None,
2717                cx,
2718            );
2719        });
2720        assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
2721
2722        // new multibuffer view with the same project entry
2723        pane.update(cx, |pane, cx| {
2724            pane.add_item(
2725                Box::new(cx.new_view(|cx| {
2726                    TestItem::new(cx)
2727                        .with_singleton(false)
2728                        .with_label("multibuffer 1")
2729                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
2730                })),
2731                false,
2732                false,
2733                None,
2734                cx,
2735            );
2736        });
2737        assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
2738
2739        // another multibuffer view with the same project entry
2740        pane.update(cx, |pane, cx| {
2741            pane.add_item(
2742                Box::new(cx.new_view(|cx| {
2743                    TestItem::new(cx)
2744                        .with_singleton(false)
2745                        .with_label("multibuffer 1b")
2746                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
2747                })),
2748                false,
2749                false,
2750                None,
2751                cx,
2752            );
2753        });
2754        assert_item_labels(
2755            &pane,
2756            ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
2757            cx,
2758        );
2759    }
2760
2761    #[gpui::test]
2762    async fn test_remove_item_ordering(cx: &mut TestAppContext) {
2763        init_test(cx);
2764        let fs = FakeFs::new(cx.executor());
2765
2766        let project = Project::test(fs, None, cx).await;
2767        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2768        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2769
2770        add_labeled_item(&pane, "A", false, cx);
2771        add_labeled_item(&pane, "B", false, cx);
2772        add_labeled_item(&pane, "C", false, cx);
2773        add_labeled_item(&pane, "D", false, cx);
2774        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2775
2776        pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
2777        add_labeled_item(&pane, "1", false, cx);
2778        assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
2779
2780        pane.update(cx, |pane, cx| {
2781            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
2782        })
2783        .unwrap()
2784        .await
2785        .unwrap();
2786        assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
2787
2788        pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
2789        assert_item_labels(&pane, ["A", "B", "C", "D*"], 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", "B*", "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", "C*"], cx);
2806
2807        pane.update(cx, |pane, cx| {
2808            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
2809        })
2810        .unwrap()
2811        .await
2812        .unwrap();
2813        assert_item_labels(&pane, ["A*"], cx);
2814    }
2815
2816    #[gpui::test]
2817    async fn test_close_inactive_items(cx: &mut TestAppContext) {
2818        init_test(cx);
2819        let fs = FakeFs::new(cx.executor());
2820
2821        let project = Project::test(fs, None, cx).await;
2822        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2823        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2824
2825        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
2826
2827        pane.update(cx, |pane, cx| {
2828            pane.close_inactive_items(&CloseInactiveItems { save_intent: None }, cx)
2829        })
2830        .unwrap()
2831        .await
2832        .unwrap();
2833        assert_item_labels(&pane, ["C*"], cx);
2834    }
2835
2836    #[gpui::test]
2837    async fn test_close_clean_items(cx: &mut TestAppContext) {
2838        init_test(cx);
2839        let fs = FakeFs::new(cx.executor());
2840
2841        let project = Project::test(fs, None, cx).await;
2842        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2843        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2844
2845        add_labeled_item(&pane, "A", true, cx);
2846        add_labeled_item(&pane, "B", false, cx);
2847        add_labeled_item(&pane, "C", true, cx);
2848        add_labeled_item(&pane, "D", false, cx);
2849        add_labeled_item(&pane, "E", false, cx);
2850        assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
2851
2852        pane.update(cx, |pane, cx| pane.close_clean_items(&CloseCleanItems, cx))
2853            .unwrap()
2854            .await
2855            .unwrap();
2856        assert_item_labels(&pane, ["A^", "C*^"], cx);
2857    }
2858
2859    #[gpui::test]
2860    async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
2861        init_test(cx);
2862        let fs = FakeFs::new(cx.executor());
2863
2864        let project = Project::test(fs, None, cx).await;
2865        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2866        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2867
2868        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
2869
2870        pane.update(cx, |pane, cx| {
2871            pane.close_items_to_the_left(&CloseItemsToTheLeft, cx)
2872        })
2873        .unwrap()
2874        .await
2875        .unwrap();
2876        assert_item_labels(&pane, ["C*", "D", "E"], cx);
2877    }
2878
2879    #[gpui::test]
2880    async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
2881        init_test(cx);
2882        let fs = FakeFs::new(cx.executor());
2883
2884        let project = Project::test(fs, None, cx).await;
2885        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2886        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2887
2888        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
2889
2890        pane.update(cx, |pane, cx| {
2891            pane.close_items_to_the_right(&CloseItemsToTheRight, cx)
2892        })
2893        .unwrap()
2894        .await
2895        .unwrap();
2896        assert_item_labels(&pane, ["A", "B", "C*"], cx);
2897    }
2898
2899    #[gpui::test]
2900    async fn test_close_all_items(cx: &mut TestAppContext) {
2901        init_test(cx);
2902        let fs = FakeFs::new(cx.executor());
2903
2904        let project = Project::test(fs, None, cx).await;
2905        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2906        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2907
2908        add_labeled_item(&pane, "A", false, cx);
2909        add_labeled_item(&pane, "B", false, cx);
2910        add_labeled_item(&pane, "C", false, cx);
2911        assert_item_labels(&pane, ["A", "B", "C*"], cx);
2912
2913        pane.update(cx, |pane, cx| {
2914            pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
2915        })
2916        .unwrap()
2917        .await
2918        .unwrap();
2919        assert_item_labels(&pane, [], cx);
2920
2921        add_labeled_item(&pane, "A", true, cx);
2922        add_labeled_item(&pane, "B", true, cx);
2923        add_labeled_item(&pane, "C", true, cx);
2924        assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
2925
2926        let save = pane
2927            .update(cx, |pane, cx| {
2928                pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
2929            })
2930            .unwrap();
2931
2932        cx.executor().run_until_parked();
2933        cx.simulate_prompt_answer(2);
2934        save.await.unwrap();
2935        assert_item_labels(&pane, [], cx);
2936    }
2937
2938    fn init_test(cx: &mut TestAppContext) {
2939        cx.update(|cx| {
2940            let settings_store = SettingsStore::test(cx);
2941            cx.set_global(settings_store);
2942            theme::init(LoadThemes::JustBase, cx);
2943            crate::init_settings(cx);
2944            Project::init_settings(cx);
2945        });
2946    }
2947
2948    fn add_labeled_item(
2949        pane: &View<Pane>,
2950        label: &str,
2951        is_dirty: bool,
2952        cx: &mut VisualTestContext,
2953    ) -> Box<View<TestItem>> {
2954        pane.update(cx, |pane, cx| {
2955            let labeled_item = Box::new(
2956                cx.new_view(|cx| TestItem::new(cx).with_label(label).with_dirty(is_dirty)),
2957            );
2958            pane.add_item(labeled_item.clone(), false, false, None, cx);
2959            labeled_item
2960        })
2961    }
2962
2963    fn set_labeled_items<const COUNT: usize>(
2964        pane: &View<Pane>,
2965        labels: [&str; COUNT],
2966        cx: &mut VisualTestContext,
2967    ) -> [Box<View<TestItem>>; COUNT] {
2968        pane.update(cx, |pane, cx| {
2969            pane.items.clear();
2970            let mut active_item_index = 0;
2971
2972            let mut index = 0;
2973            let items = labels.map(|mut label| {
2974                if label.ends_with('*') {
2975                    label = label.trim_end_matches('*');
2976                    active_item_index = index;
2977                }
2978
2979                let labeled_item = Box::new(cx.new_view(|cx| TestItem::new(cx).with_label(label)));
2980                pane.add_item(labeled_item.clone(), false, false, None, cx);
2981                index += 1;
2982                labeled_item
2983            });
2984
2985            pane.activate_item(active_item_index, false, false, cx);
2986
2987            items
2988        })
2989    }
2990
2991    // Assert the item label, with the active item label suffixed with a '*'
2992    fn assert_item_labels<const COUNT: usize>(
2993        pane: &View<Pane>,
2994        expected_states: [&str; COUNT],
2995        cx: &mut VisualTestContext,
2996    ) {
2997        pane.update(cx, |pane, cx| {
2998            let actual_states = pane
2999                .items
3000                .iter()
3001                .enumerate()
3002                .map(|(ix, item)| {
3003                    let mut state = item
3004                        .to_any()
3005                        .downcast::<TestItem>()
3006                        .unwrap()
3007                        .read(cx)
3008                        .label
3009                        .clone();
3010                    if ix == pane.active_item_index {
3011                        state.push('*');
3012                    }
3013                    if item.is_dirty(cx) {
3014                        state.push('^');
3015                    }
3016                    state
3017                })
3018                .collect::<Vec<_>>();
3019
3020            assert_eq!(
3021                actual_states, expected_states,
3022                "pane items do not match expectation"
3023            );
3024        })
3025    }
3026}
3027
3028impl Render for DraggedTab {
3029    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3030        let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
3031        let label = self.item.tab_content(
3032            TabContentParams {
3033                detail: Some(self.detail),
3034                selected: false,
3035                preview: false,
3036            },
3037            cx,
3038        );
3039        Tab::new("")
3040            .selected(self.is_active)
3041            .child(label)
3042            .render(cx)
3043            .font(ui_font)
3044    }
3045}