pane.rs

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