pane.rs

   1mod dragged_item_receiver;
   2
   3use super::{ItemHandle, SplitDirection};
   4pub use crate::toolbar::Toolbar;
   5use crate::{
   6    item::{ItemSettings, WeakItemHandle},
   7    notify_of_new_dock, AutosaveSetting, Item, NewCenterTerminal, NewFile, NewSearch, ToggleZoom,
   8    Workspace, WorkspaceSettings,
   9};
  10use anyhow::Result;
  11use collections::{HashMap, HashSet, VecDeque};
  12use context_menu::{ContextMenu, ContextMenuItem};
  13use drag_and_drop::{DragAndDrop, Draggable};
  14use dragged_item_receiver::dragged_item_receiver;
  15use fs::repository::GitFileStatus;
  16use futures::StreamExt;
  17use gpui::{
  18    actions,
  19    elements::*,
  20    geometry::{
  21        rect::RectF,
  22        vector::{vec2f, Vector2F},
  23    },
  24    impl_actions,
  25    keymap_matcher::KeymapContext,
  26    platform::{CursorStyle, MouseButton, NavigationDirection, PromptLevel},
  27    Action, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, EventContext,
  28    ModelHandle, MouseRegion, Quad, Task, View, ViewContext, ViewHandle, WeakViewHandle,
  29    WindowContext,
  30};
  31use project::{Project, ProjectEntryId, ProjectPath};
  32use serde::Deserialize;
  33use std::{
  34    any::Any,
  35    cell::RefCell,
  36    cmp, mem,
  37    path::{Path, PathBuf},
  38    rc::Rc,
  39    sync::{
  40        atomic::{AtomicUsize, Ordering},
  41        Arc,
  42    },
  43};
  44use theme::{Theme, ThemeSettings};
  45use util::truncate_and_remove_front;
  46
  47#[derive(PartialEq, Clone, Copy, Deserialize, Debug)]
  48#[serde(rename_all = "camelCase")]
  49pub enum SaveBehavior {
  50    /// ask before overwriting conflicting files (used by default with %s)
  51    PromptOnConflict,
  52    /// ask before writing any file that wouldn't be auto-saved (used by default with %w)
  53    PromptOnWrite,
  54    /// never prompt, write on conflict (used with vim's :w!)
  55    SilentlyOverwrite,
  56    /// skip all save-related behaviour (used with vim's :cq)
  57    DontSave,
  58}
  59
  60#[derive(Clone, Deserialize, PartialEq)]
  61pub struct ActivateItem(pub usize);
  62
  63#[derive(Clone, PartialEq)]
  64pub struct CloseItemById {
  65    pub item_id: usize,
  66    pub pane: WeakViewHandle<Pane>,
  67}
  68
  69#[derive(Clone, PartialEq)]
  70pub struct CloseItemsToTheLeftById {
  71    pub item_id: usize,
  72    pub pane: WeakViewHandle<Pane>,
  73}
  74
  75#[derive(Clone, PartialEq)]
  76pub struct CloseItemsToTheRightById {
  77    pub item_id: usize,
  78    pub pane: WeakViewHandle<Pane>,
  79}
  80
  81#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
  82pub struct CloseActiveItem {
  83    pub save_behavior: Option<SaveBehavior>,
  84}
  85
  86actions!(
  87    pane,
  88    [
  89        ActivatePrevItem,
  90        ActivateNextItem,
  91        ActivateLastItem,
  92        CloseInactiveItems,
  93        CloseCleanItems,
  94        CloseItemsToTheLeft,
  95        CloseItemsToTheRight,
  96        CloseAllItems,
  97        GoBack,
  98        GoForward,
  99        ReopenClosedItem,
 100        SplitLeft,
 101        SplitUp,
 102        SplitRight,
 103        SplitDown,
 104    ]
 105);
 106
 107impl_actions!(pane, [ActivateItem, CloseActiveItem]);
 108
 109const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
 110
 111pub type BackgroundActions = fn() -> &'static [(&'static str, &'static dyn Action)];
 112
 113pub fn init(cx: &mut AppContext) {
 114    cx.add_action(Pane::toggle_zoom);
 115    cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| {
 116        pane.activate_item(action.0, true, true, cx);
 117    });
 118    cx.add_action(|pane: &mut Pane, _: &ActivateLastItem, cx| {
 119        pane.activate_item(pane.items.len() - 1, true, true, cx);
 120    });
 121    cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
 122        pane.activate_prev_item(true, cx);
 123    });
 124    cx.add_action(|pane: &mut Pane, _: &ActivateNextItem, cx| {
 125        pane.activate_next_item(true, cx);
 126    });
 127    cx.add_async_action(Pane::close_active_item);
 128    cx.add_async_action(Pane::close_inactive_items);
 129    cx.add_async_action(Pane::close_clean_items);
 130    cx.add_async_action(Pane::close_items_to_the_left);
 131    cx.add_async_action(Pane::close_items_to_the_right);
 132    cx.add_async_action(Pane::close_all_items);
 133    cx.add_action(|pane: &mut Pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx));
 134    cx.add_action(|pane: &mut Pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx));
 135    cx.add_action(|pane: &mut Pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx));
 136    cx.add_action(|pane: &mut Pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx));
 137}
 138
 139#[derive(Debug)]
 140pub enum Event {
 141    AddItem { item: Box<dyn ItemHandle> },
 142    ActivateItem { local: bool },
 143    Remove,
 144    RemoveItem { item_id: usize },
 145    Split(SplitDirection),
 146    ChangeItemTitle,
 147    Focus,
 148    ZoomIn,
 149    ZoomOut,
 150}
 151
 152pub struct Pane {
 153    items: Vec<Box<dyn ItemHandle>>,
 154    activation_history: Vec<usize>,
 155    zoomed: bool,
 156    active_item_index: usize,
 157    last_focused_view_by_item: HashMap<usize, AnyWeakViewHandle>,
 158    autoscroll: bool,
 159    nav_history: NavHistory,
 160    toolbar: ViewHandle<Toolbar>,
 161    tab_bar_context_menu: TabBarContextMenu,
 162    tab_context_menu: ViewHandle<ContextMenu>,
 163    _background_actions: BackgroundActions,
 164    workspace: WeakViewHandle<Workspace>,
 165    project: ModelHandle<Project>,
 166    has_focus: bool,
 167    can_drop: Rc<dyn Fn(&DragAndDrop<Workspace>, &WindowContext) -> bool>,
 168    can_split: bool,
 169    render_tab_bar_buttons: Rc<dyn Fn(&mut Pane, &mut ViewContext<Pane>) -> AnyElement<Pane>>,
 170}
 171
 172pub struct ItemNavHistory {
 173    history: NavHistory,
 174    item: Rc<dyn WeakItemHandle>,
 175}
 176
 177#[derive(Clone)]
 178pub struct NavHistory(Rc<RefCell<NavHistoryState>>);
 179
 180struct NavHistoryState {
 181    mode: NavigationMode,
 182    backward_stack: VecDeque<NavigationEntry>,
 183    forward_stack: VecDeque<NavigationEntry>,
 184    closed_stack: VecDeque<NavigationEntry>,
 185    paths_by_item: HashMap<usize, (ProjectPath, Option<PathBuf>)>,
 186    pane: WeakViewHandle<Pane>,
 187    next_timestamp: Arc<AtomicUsize>,
 188}
 189
 190#[derive(Copy, Clone)]
 191pub enum NavigationMode {
 192    Normal,
 193    GoingBack,
 194    GoingForward,
 195    ClosingItem,
 196    ReopeningClosedItem,
 197    Disabled,
 198}
 199
 200impl Default for NavigationMode {
 201    fn default() -> Self {
 202        Self::Normal
 203    }
 204}
 205
 206pub struct NavigationEntry {
 207    pub item: Rc<dyn WeakItemHandle>,
 208    pub data: Option<Box<dyn Any>>,
 209    pub timestamp: usize,
 210}
 211
 212pub struct DraggedItem {
 213    pub handle: Box<dyn ItemHandle>,
 214    pub pane: WeakViewHandle<Pane>,
 215}
 216
 217pub enum ReorderBehavior {
 218    None,
 219    MoveAfterActive,
 220    MoveToIndex(usize),
 221}
 222
 223#[derive(Debug, Clone, Copy, PartialEq, Eq)]
 224enum TabBarContextMenuKind {
 225    New,
 226    Split,
 227}
 228
 229struct TabBarContextMenu {
 230    kind: TabBarContextMenuKind,
 231    handle: ViewHandle<ContextMenu>,
 232}
 233
 234impl TabBarContextMenu {
 235    fn handle_if_kind(&self, kind: TabBarContextMenuKind) -> Option<ViewHandle<ContextMenu>> {
 236        if self.kind == kind {
 237            return Some(self.handle.clone());
 238        }
 239        None
 240    }
 241}
 242
 243#[allow(clippy::too_many_arguments)]
 244fn nav_button<A: Action, F: 'static + Fn(&mut Pane, &mut ViewContext<Pane>)>(
 245    svg_path: &'static str,
 246    style: theme::Interactive<theme::IconButton>,
 247    nav_button_height: f32,
 248    tooltip_style: TooltipStyle,
 249    enabled: bool,
 250    on_click: F,
 251    tooltip_action: A,
 252    action_name: &str,
 253    cx: &mut ViewContext<Pane>,
 254) -> AnyElement<Pane> {
 255    MouseEventHandler::new::<A, _>(0, cx, |state, _| {
 256        let style = if enabled {
 257            style.style_for(state)
 258        } else {
 259            style.disabled_style()
 260        };
 261        Svg::new(svg_path)
 262            .with_color(style.color)
 263            .constrained()
 264            .with_width(style.icon_width)
 265            .aligned()
 266            .contained()
 267            .with_style(style.container)
 268            .constrained()
 269            .with_width(style.button_width)
 270            .with_height(nav_button_height)
 271            .aligned()
 272            .top()
 273    })
 274    .with_cursor_style(if enabled {
 275        CursorStyle::PointingHand
 276    } else {
 277        CursorStyle::default()
 278    })
 279    .on_click(MouseButton::Left, move |_, toolbar, cx| {
 280        on_click(toolbar, cx)
 281    })
 282    .with_tooltip::<A>(
 283        0,
 284        action_name.to_string(),
 285        Some(Box::new(tooltip_action)),
 286        tooltip_style,
 287        cx,
 288    )
 289    .contained()
 290    .into_any_named("nav button")
 291}
 292
 293impl Pane {
 294    pub fn new(
 295        workspace: WeakViewHandle<Workspace>,
 296        project: ModelHandle<Project>,
 297        background_actions: BackgroundActions,
 298        next_timestamp: Arc<AtomicUsize>,
 299        cx: &mut ViewContext<Self>,
 300    ) -> Self {
 301        let pane_view_id = cx.view_id();
 302        let handle = cx.weak_handle();
 303        let context_menu = cx.add_view(|cx| ContextMenu::new(pane_view_id, cx));
 304        context_menu.update(cx, |menu, _| {
 305            menu.set_position_mode(OverlayPositionMode::Local)
 306        });
 307
 308        Self {
 309            items: Vec::new(),
 310            activation_history: Vec::new(),
 311            zoomed: false,
 312            active_item_index: 0,
 313            last_focused_view_by_item: Default::default(),
 314            autoscroll: false,
 315            nav_history: NavHistory(Rc::new(RefCell::new(NavHistoryState {
 316                mode: NavigationMode::Normal,
 317                backward_stack: Default::default(),
 318                forward_stack: Default::default(),
 319                closed_stack: Default::default(),
 320                paths_by_item: Default::default(),
 321                pane: handle.clone(),
 322                next_timestamp,
 323            }))),
 324            toolbar: cx.add_view(|_| Toolbar::new()),
 325            tab_bar_context_menu: TabBarContextMenu {
 326                kind: TabBarContextMenuKind::New,
 327                handle: context_menu,
 328            },
 329            tab_context_menu: cx.add_view(|cx| ContextMenu::new(pane_view_id, cx)),
 330            _background_actions: background_actions,
 331            workspace,
 332            project,
 333            has_focus: false,
 334            can_drop: Rc::new(|_, _| true),
 335            can_split: true,
 336            render_tab_bar_buttons: Rc::new(move |pane, cx| {
 337                Flex::row()
 338                    // New menu
 339                    .with_child(Self::render_tab_bar_button(
 340                        0,
 341                        "icons/plus.svg",
 342                        false,
 343                        Some(("New...".into(), None)),
 344                        cx,
 345                        |pane, cx| pane.deploy_new_menu(cx),
 346                        |pane, cx| {
 347                            pane.tab_bar_context_menu
 348                                .handle
 349                                .update(cx, |menu, _| menu.delay_cancel())
 350                        },
 351                        pane.tab_bar_context_menu
 352                            .handle_if_kind(TabBarContextMenuKind::New),
 353                    ))
 354                    .with_child(Self::render_tab_bar_button(
 355                        1,
 356                        "icons/split.svg",
 357                        false,
 358                        Some(("Split Pane".into(), None)),
 359                        cx,
 360                        |pane, cx| pane.deploy_split_menu(cx),
 361                        |pane, cx| {
 362                            pane.tab_bar_context_menu
 363                                .handle
 364                                .update(cx, |menu, _| menu.delay_cancel())
 365                        },
 366                        pane.tab_bar_context_menu
 367                            .handle_if_kind(TabBarContextMenuKind::Split),
 368                    ))
 369                    .with_child({
 370                        let icon_path;
 371                        let tooltip_label;
 372                        if pane.is_zoomed() {
 373                            icon_path = "icons/minimize.svg";
 374                            tooltip_label = "Zoom In";
 375                        } else {
 376                            icon_path = "icons/maximize.svg";
 377                            tooltip_label = "Zoom In";
 378                        }
 379
 380                        Pane::render_tab_bar_button(
 381                            2,
 382                            icon_path,
 383                            pane.is_zoomed(),
 384                            Some((tooltip_label, Some(Box::new(ToggleZoom)))),
 385                            cx,
 386                            move |pane, cx| pane.toggle_zoom(&Default::default(), cx),
 387                            move |_, _| {},
 388                            None,
 389                        )
 390                    })
 391                    .into_any()
 392            }),
 393        }
 394    }
 395
 396    pub(crate) fn workspace(&self) -> &WeakViewHandle<Workspace> {
 397        &self.workspace
 398    }
 399
 400    pub fn has_focus(&self) -> bool {
 401        self.has_focus
 402    }
 403
 404    pub fn active_item_index(&self) -> usize {
 405        self.active_item_index
 406    }
 407
 408    pub fn on_can_drop<F>(&mut self, can_drop: F)
 409    where
 410        F: 'static + Fn(&DragAndDrop<Workspace>, &WindowContext) -> bool,
 411    {
 412        self.can_drop = Rc::new(can_drop);
 413    }
 414
 415    pub fn set_can_split(&mut self, can_split: bool, cx: &mut ViewContext<Self>) {
 416        self.can_split = can_split;
 417        cx.notify();
 418    }
 419
 420    pub fn set_can_navigate(&mut self, can_navigate: bool, cx: &mut ViewContext<Self>) {
 421        self.toolbar.update(cx, |toolbar, cx| {
 422            toolbar.set_can_navigate(can_navigate, cx);
 423        });
 424        cx.notify();
 425    }
 426
 427    pub fn set_render_tab_bar_buttons<F>(&mut self, cx: &mut ViewContext<Self>, render: F)
 428    where
 429        F: 'static + Fn(&mut Pane, &mut ViewContext<Pane>) -> AnyElement<Pane>,
 430    {
 431        self.render_tab_bar_buttons = Rc::new(render);
 432        cx.notify();
 433    }
 434
 435    pub fn nav_history_for_item<T: Item>(&self, item: &ViewHandle<T>) -> ItemNavHistory {
 436        ItemNavHistory {
 437            history: self.nav_history.clone(),
 438            item: Rc::new(item.downgrade()),
 439        }
 440    }
 441
 442    pub fn nav_history(&self) -> &NavHistory {
 443        &self.nav_history
 444    }
 445
 446    pub fn nav_history_mut(&mut self) -> &mut NavHistory {
 447        &mut self.nav_history
 448    }
 449
 450    pub fn disable_history(&mut self) {
 451        self.nav_history.disable();
 452    }
 453
 454    pub fn enable_history(&mut self) {
 455        self.nav_history.enable();
 456    }
 457
 458    pub fn can_navigate_backward(&self) -> bool {
 459        !self.nav_history.0.borrow().backward_stack.is_empty()
 460    }
 461
 462    pub fn can_navigate_forward(&self) -> bool {
 463        !self.nav_history.0.borrow().forward_stack.is_empty()
 464    }
 465
 466    fn history_updated(&mut self, cx: &mut ViewContext<Self>) {
 467        self.toolbar.update(cx, |_, cx| cx.notify());
 468    }
 469
 470    pub(crate) fn open_item(
 471        &mut self,
 472        project_entry_id: ProjectEntryId,
 473        focus_item: bool,
 474        cx: &mut ViewContext<Self>,
 475        build_item: impl FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
 476    ) -> Box<dyn ItemHandle> {
 477        let mut existing_item = None;
 478        for (index, item) in self.items.iter().enumerate() {
 479            if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [project_entry_id]
 480            {
 481                let item = item.boxed_clone();
 482                existing_item = Some((index, item));
 483                break;
 484            }
 485        }
 486
 487        if let Some((index, existing_item)) = existing_item {
 488            self.activate_item(index, focus_item, focus_item, cx);
 489            existing_item
 490        } else {
 491            let new_item = build_item(cx);
 492            self.add_item(new_item.clone(), true, focus_item, None, cx);
 493            new_item
 494        }
 495    }
 496
 497    pub fn add_item(
 498        &mut self,
 499        item: Box<dyn ItemHandle>,
 500        activate_pane: bool,
 501        focus_item: bool,
 502        destination_index: Option<usize>,
 503        cx: &mut ViewContext<Self>,
 504    ) {
 505        if item.is_singleton(cx) {
 506            if let Some(&entry_id) = item.project_entry_ids(cx).get(0) {
 507                let project = self.project.read(cx);
 508                if let Some(project_path) = project.path_for_entry(entry_id, cx) {
 509                    let abs_path = project.absolute_path(&project_path, cx);
 510                    self.nav_history
 511                        .0
 512                        .borrow_mut()
 513                        .paths_by_item
 514                        .insert(item.id(), (project_path, abs_path));
 515                }
 516            }
 517        }
 518        // If no destination index is specified, add or move the item after the active item.
 519        let mut insertion_index = {
 520            cmp::min(
 521                if let Some(destination_index) = destination_index {
 522                    destination_index
 523                } else {
 524                    self.active_item_index + 1
 525                },
 526                self.items.len(),
 527            )
 528        };
 529
 530        // Does the item already exist?
 531        let project_entry_id = if item.is_singleton(cx) {
 532            item.project_entry_ids(cx).get(0).copied()
 533        } else {
 534            None
 535        };
 536
 537        let existing_item_index = self.items.iter().position(|existing_item| {
 538            if existing_item.id() == item.id() {
 539                true
 540            } else if existing_item.is_singleton(cx) {
 541                existing_item
 542                    .project_entry_ids(cx)
 543                    .get(0)
 544                    .map_or(false, |existing_entry_id| {
 545                        Some(existing_entry_id) == project_entry_id.as_ref()
 546                    })
 547            } else {
 548                false
 549            }
 550        });
 551
 552        if let Some(existing_item_index) = existing_item_index {
 553            // If the item already exists, move it to the desired destination and activate it
 554
 555            if existing_item_index != insertion_index {
 556                let existing_item_is_active = existing_item_index == self.active_item_index;
 557
 558                // If the caller didn't specify a destination and the added item is already
 559                // the active one, don't move it
 560                if existing_item_is_active && destination_index.is_none() {
 561                    insertion_index = existing_item_index;
 562                } else {
 563                    self.items.remove(existing_item_index);
 564                    if existing_item_index < self.active_item_index {
 565                        self.active_item_index -= 1;
 566                    }
 567                    insertion_index = insertion_index.min(self.items.len());
 568
 569                    self.items.insert(insertion_index, item.clone());
 570
 571                    if existing_item_is_active {
 572                        self.active_item_index = insertion_index;
 573                    } else if insertion_index <= self.active_item_index {
 574                        self.active_item_index += 1;
 575                    }
 576                }
 577
 578                cx.notify();
 579            }
 580
 581            self.activate_item(insertion_index, activate_pane, focus_item, cx);
 582        } else {
 583            self.items.insert(insertion_index, item.clone());
 584            if insertion_index <= self.active_item_index {
 585                self.active_item_index += 1;
 586            }
 587
 588            self.activate_item(insertion_index, activate_pane, focus_item, cx);
 589            cx.notify();
 590        }
 591
 592        cx.emit(Event::AddItem { item });
 593    }
 594
 595    pub fn items_len(&self) -> usize {
 596        self.items.len()
 597    }
 598
 599    pub fn items(&self) -> impl Iterator<Item = &Box<dyn ItemHandle>> + DoubleEndedIterator {
 600        self.items.iter()
 601    }
 602
 603    pub fn items_of_type<T: View>(&self) -> impl '_ + Iterator<Item = ViewHandle<T>> {
 604        self.items
 605            .iter()
 606            .filter_map(|item| item.as_any().clone().downcast())
 607    }
 608
 609    pub fn active_item(&self) -> Option<Box<dyn ItemHandle>> {
 610        self.items.get(self.active_item_index).cloned()
 611    }
 612
 613    pub fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Vector2F> {
 614        self.items
 615            .get(self.active_item_index)?
 616            .pixel_position_of_cursor(cx)
 617    }
 618
 619    pub fn item_for_entry(
 620        &self,
 621        entry_id: ProjectEntryId,
 622        cx: &AppContext,
 623    ) -> Option<Box<dyn ItemHandle>> {
 624        self.items.iter().find_map(|item| {
 625            if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
 626                Some(item.boxed_clone())
 627            } else {
 628                None
 629            }
 630        })
 631    }
 632
 633    pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option<usize> {
 634        self.items.iter().position(|i| i.id() == item.id())
 635    }
 636
 637    pub fn toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext<Self>) {
 638        // Potentially warn the user of the new keybinding
 639        let workspace_handle = self.workspace().clone();
 640        cx.spawn(|_, mut cx| async move { notify_of_new_dock(&workspace_handle, &mut cx) })
 641            .detach();
 642
 643        if self.zoomed {
 644            cx.emit(Event::ZoomOut);
 645        } else if !self.items.is_empty() {
 646            if !self.has_focus {
 647                cx.focus_self();
 648            }
 649            cx.emit(Event::ZoomIn);
 650        }
 651    }
 652
 653    pub fn activate_item(
 654        &mut self,
 655        index: usize,
 656        activate_pane: bool,
 657        focus_item: bool,
 658        cx: &mut ViewContext<Self>,
 659    ) {
 660        use NavigationMode::{GoingBack, GoingForward};
 661
 662        if index < self.items.len() {
 663            let prev_active_item_ix = mem::replace(&mut self.active_item_index, index);
 664            if prev_active_item_ix != self.active_item_index
 665                || matches!(self.nav_history.mode(), GoingBack | GoingForward)
 666            {
 667                if let Some(prev_item) = self.items.get(prev_active_item_ix) {
 668                    prev_item.deactivated(cx);
 669                }
 670
 671                cx.emit(Event::ActivateItem {
 672                    local: activate_pane,
 673                });
 674            }
 675
 676            if let Some(newly_active_item) = self.items.get(index) {
 677                self.activation_history
 678                    .retain(|&previously_active_item_id| {
 679                        previously_active_item_id != newly_active_item.id()
 680                    });
 681                self.activation_history.push(newly_active_item.id());
 682            }
 683
 684            self.update_toolbar(cx);
 685
 686            if focus_item {
 687                self.focus_active_item(cx);
 688            }
 689
 690            self.autoscroll = true;
 691            cx.notify();
 692        }
 693    }
 694
 695    pub fn activate_prev_item(&mut self, activate_pane: bool, cx: &mut ViewContext<Self>) {
 696        let mut index = self.active_item_index;
 697        if index > 0 {
 698            index -= 1;
 699        } else if !self.items.is_empty() {
 700            index = self.items.len() - 1;
 701        }
 702        self.activate_item(index, activate_pane, activate_pane, cx);
 703    }
 704
 705    pub fn activate_next_item(&mut self, activate_pane: bool, cx: &mut ViewContext<Self>) {
 706        let mut index = self.active_item_index;
 707        if index + 1 < self.items.len() {
 708            index += 1;
 709        } else {
 710            index = 0;
 711        }
 712        self.activate_item(index, activate_pane, activate_pane, cx);
 713    }
 714
 715    pub fn close_active_item(
 716        &mut self,
 717        action: &CloseActiveItem,
 718        cx: &mut ViewContext<Self>,
 719    ) -> Option<Task<Result<()>>> {
 720        if self.items.is_empty() {
 721            return None;
 722        }
 723        let active_item_id = self.items[self.active_item_index].id();
 724        Some(self.close_item_by_id(
 725            active_item_id,
 726            action.save_behavior.unwrap_or(SaveBehavior::PromptOnWrite),
 727            cx,
 728        ))
 729    }
 730
 731    pub fn close_item_by_id(
 732        &mut self,
 733        item_id_to_close: usize,
 734        save_behavior: SaveBehavior,
 735        cx: &mut ViewContext<Self>,
 736    ) -> Task<Result<()>> {
 737        self.close_items(cx, save_behavior, move |view_id| {
 738            view_id == item_id_to_close
 739        })
 740    }
 741
 742    pub fn close_inactive_items(
 743        &mut self,
 744        _: &CloseInactiveItems,
 745        cx: &mut ViewContext<Self>,
 746    ) -> Option<Task<Result<()>>> {
 747        if self.items.is_empty() {
 748            return None;
 749        }
 750
 751        let active_item_id = self.items[self.active_item_index].id();
 752        Some(
 753            self.close_items(cx, SaveBehavior::PromptOnWrite, move |item_id| {
 754                item_id != active_item_id
 755            }),
 756        )
 757    }
 758
 759    pub fn close_clean_items(
 760        &mut self,
 761        _: &CloseCleanItems,
 762        cx: &mut ViewContext<Self>,
 763    ) -> Option<Task<Result<()>>> {
 764        let item_ids: Vec<_> = self
 765            .items()
 766            .filter(|item| !item.is_dirty(cx))
 767            .map(|item| item.id())
 768            .collect();
 769        Some(
 770            self.close_items(cx, SaveBehavior::PromptOnWrite, move |item_id| {
 771                item_ids.contains(&item_id)
 772            }),
 773        )
 774    }
 775
 776    pub fn close_items_to_the_left(
 777        &mut self,
 778        _: &CloseItemsToTheLeft,
 779        cx: &mut ViewContext<Self>,
 780    ) -> Option<Task<Result<()>>> {
 781        if self.items.is_empty() {
 782            return None;
 783        }
 784        let active_item_id = self.items[self.active_item_index].id();
 785        Some(self.close_items_to_the_left_by_id(active_item_id, cx))
 786    }
 787
 788    pub fn close_items_to_the_left_by_id(
 789        &mut self,
 790        item_id: usize,
 791        cx: &mut ViewContext<Self>,
 792    ) -> Task<Result<()>> {
 793        let item_ids: Vec<_> = self
 794            .items()
 795            .take_while(|item| item.id() != item_id)
 796            .map(|item| item.id())
 797            .collect();
 798        self.close_items(cx, SaveBehavior::PromptOnWrite, move |item_id| {
 799            item_ids.contains(&item_id)
 800        })
 801    }
 802
 803    pub fn close_items_to_the_right(
 804        &mut self,
 805        _: &CloseItemsToTheRight,
 806        cx: &mut ViewContext<Self>,
 807    ) -> Option<Task<Result<()>>> {
 808        if self.items.is_empty() {
 809            return None;
 810        }
 811        let active_item_id = self.items[self.active_item_index].id();
 812        Some(self.close_items_to_the_right_by_id(active_item_id, cx))
 813    }
 814
 815    pub fn close_items_to_the_right_by_id(
 816        &mut self,
 817        item_id: usize,
 818        cx: &mut ViewContext<Self>,
 819    ) -> Task<Result<()>> {
 820        let item_ids: Vec<_> = self
 821            .items()
 822            .rev()
 823            .take_while(|item| item.id() != item_id)
 824            .map(|item| item.id())
 825            .collect();
 826        self.close_items(cx, SaveBehavior::PromptOnWrite, move |item_id| {
 827            item_ids.contains(&item_id)
 828        })
 829    }
 830
 831    pub fn close_all_items(
 832        &mut self,
 833        _: &CloseAllItems,
 834        cx: &mut ViewContext<Self>,
 835    ) -> Option<Task<Result<()>>> {
 836        if self.items.is_empty() {
 837            return None;
 838        }
 839
 840        Some(self.close_items(cx, SaveBehavior::PromptOnWrite, |_| true))
 841    }
 842
 843    pub(super) fn file_names_for_prompt(
 844        items: &mut dyn Iterator<Item = &Box<dyn ItemHandle>>,
 845        all_dirty_items: usize,
 846        cx: &AppContext,
 847    ) -> String {
 848        /// Quantity of item paths displayed in prompt prior to cutoff..
 849        const FILE_NAMES_CUTOFF_POINT: usize = 10;
 850        let mut file_names: Vec<_> = items
 851            .filter_map(|item| {
 852                item.project_path(cx).and_then(|project_path| {
 853                    project_path
 854                        .path
 855                        .file_name()
 856                        .and_then(|name| name.to_str().map(ToOwned::to_owned))
 857                })
 858            })
 859            .take(FILE_NAMES_CUTOFF_POINT)
 860            .collect();
 861        let should_display_followup_text =
 862            all_dirty_items > FILE_NAMES_CUTOFF_POINT || file_names.len() != all_dirty_items;
 863        if should_display_followup_text {
 864            let not_shown_files = all_dirty_items - file_names.len();
 865            if not_shown_files == 1 {
 866                file_names.push(".. 1 file not shown".into());
 867            } else {
 868                file_names.push(format!(".. {} files not shown", not_shown_files).into());
 869            }
 870        }
 871        let file_names = file_names.join("\n");
 872        format!(
 873            "Do you want to save changes to the following {} files?\n{file_names}",
 874            all_dirty_items
 875        )
 876    }
 877
 878    pub fn close_items(
 879        &mut self,
 880        cx: &mut ViewContext<Pane>,
 881        mut save_behavior: SaveBehavior,
 882        should_close: impl 'static + Fn(usize) -> bool,
 883    ) -> Task<Result<()>> {
 884        // Find the items to close.
 885        let mut items_to_close = Vec::new();
 886        for item in &self.items {
 887            if should_close(item.id()) {
 888                items_to_close.push(item.boxed_clone());
 889            }
 890        }
 891
 892        // If a buffer is open both in a singleton editor and in a multibuffer, make sure
 893        // to focus the singleton buffer when prompting to save that buffer, as opposed
 894        // to focusing the multibuffer, because this gives the user a more clear idea
 895        // of what content they would be saving.
 896        items_to_close.sort_by_key(|item| !item.is_singleton(cx));
 897
 898        let workspace = self.workspace.clone();
 899        cx.spawn(|pane, mut cx| async move {
 900            if save_behavior == SaveBehavior::PromptOnWrite && items_to_close.len() > 1 {
 901                let mut answer = pane.update(&mut cx, |_, cx| {
 902                    let prompt = Self::file_names_for_prompt(
 903                        &mut items_to_close.iter(),
 904                        items_to_close.len(),
 905                        cx,
 906                    );
 907                    cx.prompt(
 908                        PromptLevel::Warning,
 909                        &prompt,
 910                        &["Save all", "Discard all", "Cancel"],
 911                    )
 912                })?;
 913                match answer.next().await {
 914                    Some(0) => save_behavior = SaveBehavior::PromptOnConflict,
 915                    Some(1) => save_behavior = SaveBehavior::DontSave,
 916                    _ => {}
 917                }
 918            }
 919            let mut saved_project_items_ids = HashSet::default();
 920            for item in items_to_close.clone() {
 921                // Find the item's current index and its set of project item models. Avoid
 922                // storing these in advance, in case they have changed since this task
 923                // was started.
 924                let (item_ix, mut project_item_ids) = pane.read_with(&cx, |pane, cx| {
 925                    (pane.index_for_item(&*item), item.project_item_model_ids(cx))
 926                })?;
 927                let item_ix = if let Some(ix) = item_ix {
 928                    ix
 929                } else {
 930                    continue;
 931                };
 932
 933                // Check if this view has any project items that are not open anywhere else
 934                // in the workspace, AND that the user has not already been prompted to save.
 935                // If there are any such project entries, prompt the user to save this item.
 936                let project = workspace.read_with(&cx, |workspace, cx| {
 937                    for item in workspace.items(cx) {
 938                        if !items_to_close
 939                            .iter()
 940                            .any(|item_to_close| item_to_close.id() == item.id())
 941                        {
 942                            let other_project_item_ids = item.project_item_model_ids(cx);
 943                            project_item_ids.retain(|id| !other_project_item_ids.contains(id));
 944                        }
 945                    }
 946                    workspace.project().clone()
 947                })?;
 948                let should_save = project_item_ids
 949                    .iter()
 950                    .any(|id| saved_project_items_ids.insert(*id));
 951
 952                if should_save
 953                    && !Self::save_item(
 954                        project.clone(),
 955                        &pane,
 956                        item_ix,
 957                        &*item,
 958                        save_behavior,
 959                        &mut cx,
 960                    )
 961                    .await?
 962                {
 963                    break;
 964                }
 965
 966                // Remove the item from the pane.
 967                pane.update(&mut cx, |pane, cx| {
 968                    if let Some(item_ix) = pane.items.iter().position(|i| i.id() == item.id()) {
 969                        pane.remove_item(item_ix, false, cx);
 970                    }
 971                })?;
 972            }
 973
 974            pane.update(&mut cx, |_, cx| cx.notify())?;
 975            Ok(())
 976        })
 977    }
 978
 979    pub fn remove_item(
 980        &mut self,
 981        item_index: usize,
 982        activate_pane: bool,
 983        cx: &mut ViewContext<Self>,
 984    ) {
 985        self.activation_history
 986            .retain(|&history_entry| history_entry != self.items[item_index].id());
 987
 988        if item_index == self.active_item_index {
 989            let index_to_activate = self
 990                .activation_history
 991                .pop()
 992                .and_then(|last_activated_item| {
 993                    self.items.iter().enumerate().find_map(|(index, item)| {
 994                        (item.id() == last_activated_item).then_some(index)
 995                    })
 996                })
 997                // We didn't have a valid activation history entry, so fallback
 998                // to activating the item to the left
 999                .unwrap_or_else(|| item_index.min(self.items.len()).saturating_sub(1));
1000
1001            let should_activate = activate_pane || self.has_focus;
1002            self.activate_item(index_to_activate, should_activate, should_activate, cx);
1003        }
1004
1005        let item = self.items.remove(item_index);
1006
1007        cx.emit(Event::RemoveItem { item_id: item.id() });
1008        if self.items.is_empty() {
1009            item.deactivated(cx);
1010            self.update_toolbar(cx);
1011            cx.emit(Event::Remove);
1012        }
1013
1014        if item_index < self.active_item_index {
1015            self.active_item_index -= 1;
1016        }
1017
1018        self.nav_history.set_mode(NavigationMode::ClosingItem);
1019        item.deactivated(cx);
1020        self.nav_history.set_mode(NavigationMode::Normal);
1021
1022        if let Some(path) = item.project_path(cx) {
1023            let abs_path = self
1024                .nav_history
1025                .0
1026                .borrow()
1027                .paths_by_item
1028                .get(&item.id())
1029                .and_then(|(_, abs_path)| abs_path.clone());
1030
1031            self.nav_history
1032                .0
1033                .borrow_mut()
1034                .paths_by_item
1035                .insert(item.id(), (path, abs_path));
1036        } else {
1037            self.nav_history
1038                .0
1039                .borrow_mut()
1040                .paths_by_item
1041                .remove(&item.id());
1042        }
1043
1044        if self.items.is_empty() && self.zoomed {
1045            cx.emit(Event::ZoomOut);
1046        }
1047
1048        cx.notify();
1049    }
1050
1051    pub async fn save_item(
1052        project: ModelHandle<Project>,
1053        pane: &WeakViewHandle<Pane>,
1054        item_ix: usize,
1055        item: &dyn ItemHandle,
1056        save_behavior: SaveBehavior,
1057        cx: &mut AsyncAppContext,
1058    ) -> Result<bool> {
1059        const CONFLICT_MESSAGE: &str =
1060            "This file has changed on disk since you started editing it. Do you want to overwrite it?";
1061
1062        if save_behavior == SaveBehavior::DontSave {
1063            return Ok(true);
1064        }
1065
1066        let (has_conflict, is_dirty, can_save, is_singleton) = cx.read(|cx| {
1067            (
1068                item.has_conflict(cx),
1069                item.is_dirty(cx),
1070                item.can_save(cx),
1071                item.is_singleton(cx),
1072            )
1073        });
1074
1075        if has_conflict && can_save {
1076            if save_behavior == SaveBehavior::SilentlyOverwrite {
1077                pane.update(cx, |_, cx| item.save(project, cx))?.await?;
1078            } else {
1079                let mut answer = pane.update(cx, |pane, cx| {
1080                    pane.activate_item(item_ix, true, true, cx);
1081                    cx.prompt(
1082                        PromptLevel::Warning,
1083                        CONFLICT_MESSAGE,
1084                        &["Overwrite", "Discard", "Cancel"],
1085                    )
1086                })?;
1087                match answer.next().await {
1088                    Some(0) => pane.update(cx, |_, cx| item.save(project, cx))?.await?,
1089                    Some(1) => pane.update(cx, |_, cx| item.reload(project, cx))?.await?,
1090                    _ => return Ok(false),
1091                }
1092            }
1093        } else if is_dirty && (can_save || is_singleton) {
1094            let will_autosave = cx.read(|cx| {
1095                matches!(
1096                    settings::get::<WorkspaceSettings>(cx).autosave,
1097                    AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange
1098                ) && Self::can_autosave_item(&*item, cx)
1099            });
1100            let should_save = if save_behavior == SaveBehavior::PromptOnWrite && !will_autosave {
1101                let mut answer = pane.update(cx, |pane, cx| {
1102                    pane.activate_item(item_ix, true, true, cx);
1103                    let prompt = dirty_message_for(item.project_path(cx));
1104                    cx.prompt(
1105                        PromptLevel::Warning,
1106                        &prompt,
1107                        &["Save", "Don't Save", "Cancel"],
1108                    )
1109                })?;
1110                match answer.next().await {
1111                    Some(0) => true,
1112                    Some(1) => false,
1113                    _ => return Ok(false),
1114                }
1115            } else {
1116                true
1117            };
1118
1119            if should_save {
1120                if can_save {
1121                    pane.update(cx, |_, cx| item.save(project, cx))?.await?;
1122                } else if is_singleton {
1123                    let start_abs_path = project
1124                        .read_with(cx, |project, cx| {
1125                            let worktree = project.visible_worktrees(cx).next()?;
1126                            Some(worktree.read(cx).as_local()?.abs_path().to_path_buf())
1127                        })
1128                        .unwrap_or_else(|| Path::new("").into());
1129
1130                    let mut abs_path = cx.update(|cx| cx.prompt_for_new_path(&start_abs_path));
1131                    if let Some(abs_path) = abs_path.next().await.flatten() {
1132                        pane.update(cx, |_, cx| item.save_as(project, abs_path, cx))?
1133                            .await?;
1134                    } else {
1135                        return Ok(false);
1136                    }
1137                }
1138            }
1139        }
1140        Ok(true)
1141    }
1142
1143    fn can_autosave_item(item: &dyn ItemHandle, cx: &AppContext) -> bool {
1144        let is_deleted = item.project_entry_ids(cx).is_empty();
1145        item.is_dirty(cx) && !item.has_conflict(cx) && item.can_save(cx) && !is_deleted
1146    }
1147
1148    pub fn autosave_item(
1149        item: &dyn ItemHandle,
1150        project: ModelHandle<Project>,
1151        cx: &mut WindowContext,
1152    ) -> Task<Result<()>> {
1153        if Self::can_autosave_item(item, cx) {
1154            item.save(project, cx)
1155        } else {
1156            Task::ready(Ok(()))
1157        }
1158    }
1159
1160    pub fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
1161        if let Some(active_item) = self.active_item() {
1162            cx.focus(active_item.as_any());
1163        }
1164    }
1165
1166    pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext<Self>) {
1167        cx.emit(Event::Split(direction));
1168    }
1169
1170    fn deploy_split_menu(&mut self, cx: &mut ViewContext<Self>) {
1171        self.tab_bar_context_menu.handle.update(cx, |menu, cx| {
1172            menu.toggle(
1173                Default::default(),
1174                AnchorCorner::TopRight,
1175                vec![
1176                    ContextMenuItem::action("Split Right", SplitRight),
1177                    ContextMenuItem::action("Split Left", SplitLeft),
1178                    ContextMenuItem::action("Split Up", SplitUp),
1179                    ContextMenuItem::action("Split Down", SplitDown),
1180                ],
1181                cx,
1182            );
1183        });
1184
1185        self.tab_bar_context_menu.kind = TabBarContextMenuKind::Split;
1186    }
1187
1188    fn deploy_new_menu(&mut self, cx: &mut ViewContext<Self>) {
1189        self.tab_bar_context_menu.handle.update(cx, |menu, cx| {
1190            menu.toggle(
1191                Default::default(),
1192                AnchorCorner::TopRight,
1193                vec![
1194                    ContextMenuItem::action("New File", NewFile),
1195                    ContextMenuItem::action("New Terminal", NewCenterTerminal),
1196                    ContextMenuItem::action("New Search", NewSearch),
1197                ],
1198                cx,
1199            );
1200        });
1201
1202        self.tab_bar_context_menu.kind = TabBarContextMenuKind::New;
1203    }
1204
1205    fn deploy_tab_context_menu(
1206        &mut self,
1207        position: Vector2F,
1208        target_item_id: usize,
1209        cx: &mut ViewContext<Self>,
1210    ) {
1211        let active_item_id = self.items[self.active_item_index].id();
1212        let is_active_item = target_item_id == active_item_id;
1213        let target_pane = cx.weak_handle();
1214
1215        // The `CloseInactiveItems` action should really be called "CloseOthers" and the behaviour should be dynamically based on the tab the action is ran on.  Currently, this is a weird action because you can run it on a non-active tab and it will close everything by the actual active tab
1216
1217        self.tab_context_menu.update(cx, |menu, cx| {
1218            menu.show(
1219                position,
1220                AnchorCorner::TopLeft,
1221                if is_active_item {
1222                    vec![
1223                        ContextMenuItem::action(
1224                            "Close Active Item",
1225                            CloseActiveItem {
1226                                save_behavior: None,
1227                            },
1228                        ),
1229                        ContextMenuItem::action("Close Inactive Items", CloseInactiveItems),
1230                        ContextMenuItem::action("Close Clean Items", CloseCleanItems),
1231                        ContextMenuItem::action("Close Items To The Left", CloseItemsToTheLeft),
1232                        ContextMenuItem::action("Close Items To The Right", CloseItemsToTheRight),
1233                        ContextMenuItem::action("Close All Items", CloseAllItems),
1234                    ]
1235                } else {
1236                    // In the case of the user right clicking on a non-active tab, for some item-closing commands, we need to provide the id of the tab, for the others, we can reuse the existing command.
1237                    vec![
1238                        ContextMenuItem::handler("Close Inactive Item", {
1239                            let pane = target_pane.clone();
1240                            move |cx| {
1241                                if let Some(pane) = pane.upgrade(cx) {
1242                                    pane.update(cx, |pane, cx| {
1243                                        pane.close_item_by_id(
1244                                            target_item_id,
1245                                            SaveBehavior::PromptOnWrite,
1246                                            cx,
1247                                        )
1248                                        .detach_and_log_err(cx);
1249                                    })
1250                                }
1251                            }
1252                        }),
1253                        ContextMenuItem::action("Close Inactive Items", CloseInactiveItems),
1254                        ContextMenuItem::action("Close Clean Items", CloseCleanItems),
1255                        ContextMenuItem::handler("Close Items To The Left", {
1256                            let pane = target_pane.clone();
1257                            move |cx| {
1258                                if let Some(pane) = pane.upgrade(cx) {
1259                                    pane.update(cx, |pane, cx| {
1260                                        pane.close_items_to_the_left_by_id(target_item_id, cx)
1261                                            .detach_and_log_err(cx);
1262                                    })
1263                                }
1264                            }
1265                        }),
1266                        ContextMenuItem::handler("Close Items To The Right", {
1267                            let pane = target_pane.clone();
1268                            move |cx| {
1269                                if let Some(pane) = pane.upgrade(cx) {
1270                                    pane.update(cx, |pane, cx| {
1271                                        pane.close_items_to_the_right_by_id(target_item_id, cx)
1272                                            .detach_and_log_err(cx);
1273                                    })
1274                                }
1275                            }
1276                        }),
1277                        ContextMenuItem::action("Close All Items", CloseAllItems),
1278                    ]
1279                },
1280                cx,
1281            );
1282        });
1283    }
1284
1285    pub fn toolbar(&self) -> &ViewHandle<Toolbar> {
1286        &self.toolbar
1287    }
1288
1289    pub fn handle_deleted_project_item(
1290        &mut self,
1291        entry_id: ProjectEntryId,
1292        cx: &mut ViewContext<Pane>,
1293    ) -> Option<()> {
1294        let (item_index_to_delete, item_id) = self.items().enumerate().find_map(|(i, item)| {
1295            if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
1296                Some((i, item.id()))
1297            } else {
1298                None
1299            }
1300        })?;
1301
1302        self.remove_item(item_index_to_delete, false, cx);
1303        self.nav_history.remove_item(item_id);
1304
1305        Some(())
1306    }
1307
1308    fn update_toolbar(&mut self, cx: &mut ViewContext<Self>) {
1309        let active_item = self
1310            .items
1311            .get(self.active_item_index)
1312            .map(|item| item.as_ref());
1313        self.toolbar.update(cx, |toolbar, cx| {
1314            toolbar.set_active_item(active_item, cx);
1315        });
1316    }
1317
1318    fn render_tabs(&mut self, cx: &mut ViewContext<Self>) -> impl Element<Self> {
1319        let theme = theme::current(cx).clone();
1320
1321        let pane = cx.handle().downgrade();
1322        let autoscroll = if mem::take(&mut self.autoscroll) {
1323            Some(self.active_item_index)
1324        } else {
1325            None
1326        };
1327
1328        let pane_active = self.has_focus;
1329
1330        enum Tabs {}
1331        let mut row = Flex::row().scrollable::<Tabs>(1, autoscroll, cx);
1332        for (ix, (item, detail)) in self
1333            .items
1334            .iter()
1335            .cloned()
1336            .zip(self.tab_details(cx))
1337            .enumerate()
1338        {
1339            let git_status = item
1340                .project_path(cx)
1341                .and_then(|path| self.project.read(cx).entry_for_path(&path, cx))
1342                .and_then(|entry| entry.git_status());
1343
1344            let detail = if detail == 0 { None } else { Some(detail) };
1345            let tab_active = ix == self.active_item_index;
1346
1347            row.add_child({
1348                enum TabDragReceiver {}
1349                let mut receiver =
1350                    dragged_item_receiver::<TabDragReceiver, _, _>(self, ix, ix, true, None, cx, {
1351                        let item = item.clone();
1352                        let pane = pane.clone();
1353                        let detail = detail.clone();
1354
1355                        let theme = theme::current(cx).clone();
1356                        let mut tooltip_theme = theme.tooltip.clone();
1357                        tooltip_theme.max_text_width = None;
1358                        let tab_tooltip_text =
1359                            item.tab_tooltip_text(cx).map(|text| text.into_owned());
1360
1361                        let mut tab_style = theme
1362                            .workspace
1363                            .tab_bar
1364                            .tab_style(pane_active, tab_active)
1365                            .clone();
1366                        let should_show_status = settings::get::<ItemSettings>(cx).git_status;
1367                        if should_show_status && git_status != None {
1368                            tab_style.label.text.color = match git_status.unwrap() {
1369                                GitFileStatus::Added => tab_style.git.inserted,
1370                                GitFileStatus::Modified => tab_style.git.modified,
1371                                GitFileStatus::Conflict => tab_style.git.conflict,
1372                            };
1373                        }
1374
1375                        move |mouse_state, cx| {
1376                            let hovered = mouse_state.hovered();
1377
1378                            enum Tab {}
1379                            let mouse_event_handler =
1380                                MouseEventHandler::new::<Tab, _>(ix, cx, |_, cx| {
1381                                    Self::render_tab(
1382                                        &item,
1383                                        pane.clone(),
1384                                        ix == 0,
1385                                        detail,
1386                                        hovered,
1387                                        &tab_style,
1388                                        cx,
1389                                    )
1390                                })
1391                                .on_down(MouseButton::Left, move |_, this, cx| {
1392                                    this.activate_item(ix, true, true, cx);
1393                                })
1394                                .on_click(MouseButton::Middle, {
1395                                    let item_id = item.id();
1396                                    move |_, pane, cx| {
1397                                        pane.close_item_by_id(
1398                                            item_id,
1399                                            SaveBehavior::PromptOnWrite,
1400                                            cx,
1401                                        )
1402                                        .detach_and_log_err(cx);
1403                                    }
1404                                })
1405                                .on_down(
1406                                    MouseButton::Right,
1407                                    move |event, pane, cx| {
1408                                        pane.deploy_tab_context_menu(event.position, item.id(), cx);
1409                                    },
1410                                );
1411
1412                            if let Some(tab_tooltip_text) = tab_tooltip_text {
1413                                mouse_event_handler
1414                                    .with_tooltip::<Self>(
1415                                        ix,
1416                                        tab_tooltip_text,
1417                                        None,
1418                                        tooltip_theme,
1419                                        cx,
1420                                    )
1421                                    .into_any()
1422                            } else {
1423                                mouse_event_handler.into_any()
1424                            }
1425                        }
1426                    });
1427
1428                if !pane_active || !tab_active {
1429                    receiver = receiver.with_cursor_style(CursorStyle::PointingHand);
1430                }
1431
1432                receiver.as_draggable(
1433                    DraggedItem {
1434                        handle: item,
1435                        pane: pane.clone(),
1436                    },
1437                    {
1438                        let theme = theme::current(cx).clone();
1439
1440                        let detail = detail.clone();
1441                        move |_, dragged_item: &DraggedItem, cx: &mut ViewContext<Workspace>| {
1442                            let tab_style = &theme.workspace.tab_bar.dragged_tab;
1443                            Self::render_dragged_tab(
1444                                &dragged_item.handle,
1445                                dragged_item.pane.clone(),
1446                                false,
1447                                detail,
1448                                false,
1449                                &tab_style,
1450                                cx,
1451                            )
1452                        }
1453                    },
1454                )
1455            })
1456        }
1457
1458        // Use the inactive tab style along with the current pane's active status to decide how to render
1459        // the filler
1460        let filler_index = self.items.len();
1461        let filler_style = theme.workspace.tab_bar.tab_style(pane_active, false);
1462        enum Filler {}
1463        row.add_child(
1464            dragged_item_receiver::<Filler, _, _>(self, 0, filler_index, true, None, cx, |_, _| {
1465                Empty::new()
1466                    .contained()
1467                    .with_style(filler_style.container)
1468                    .with_border(filler_style.container.border)
1469            })
1470            .flex(1., true)
1471            .into_any_named("filler"),
1472        );
1473
1474        row
1475    }
1476
1477    fn tab_details(&self, cx: &AppContext) -> Vec<usize> {
1478        let mut tab_details = (0..self.items.len()).map(|_| 0).collect::<Vec<_>>();
1479
1480        let mut tab_descriptions = HashMap::default();
1481        let mut done = false;
1482        while !done {
1483            done = true;
1484
1485            // Store item indices by their tab description.
1486            for (ix, (item, detail)) in self.items.iter().zip(&tab_details).enumerate() {
1487                if let Some(description) = item.tab_description(*detail, cx) {
1488                    if *detail == 0
1489                        || Some(&description) != item.tab_description(detail - 1, cx).as_ref()
1490                    {
1491                        tab_descriptions
1492                            .entry(description)
1493                            .or_insert(Vec::new())
1494                            .push(ix);
1495                    }
1496                }
1497            }
1498
1499            // If two or more items have the same tab description, increase their level
1500            // of detail and try again.
1501            for (_, item_ixs) in tab_descriptions.drain() {
1502                if item_ixs.len() > 1 {
1503                    done = false;
1504                    for ix in item_ixs {
1505                        tab_details[ix] += 1;
1506                    }
1507                }
1508            }
1509        }
1510
1511        tab_details
1512    }
1513
1514    fn render_tab(
1515        item: &Box<dyn ItemHandle>,
1516        pane: WeakViewHandle<Pane>,
1517        first: bool,
1518        detail: Option<usize>,
1519        hovered: bool,
1520        tab_style: &theme::Tab,
1521        cx: &mut ViewContext<Self>,
1522    ) -> AnyElement<Self> {
1523        let title = item.tab_content(detail, &tab_style, cx);
1524        Self::render_tab_with_title(title, item, pane, first, hovered, tab_style, cx)
1525    }
1526
1527    fn render_dragged_tab(
1528        item: &Box<dyn ItemHandle>,
1529        pane: WeakViewHandle<Pane>,
1530        first: bool,
1531        detail: Option<usize>,
1532        hovered: bool,
1533        tab_style: &theme::Tab,
1534        cx: &mut ViewContext<Workspace>,
1535    ) -> AnyElement<Workspace> {
1536        let title = item.dragged_tab_content(detail, &tab_style, cx);
1537        Self::render_tab_with_title(title, item, pane, first, hovered, tab_style, cx)
1538    }
1539
1540    fn render_tab_with_title<T: View>(
1541        title: AnyElement<T>,
1542        item: &Box<dyn ItemHandle>,
1543        pane: WeakViewHandle<Pane>,
1544        first: bool,
1545        hovered: bool,
1546        tab_style: &theme::Tab,
1547        cx: &mut ViewContext<T>,
1548    ) -> AnyElement<T> {
1549        let mut container = tab_style.container.clone();
1550        if first {
1551            container.border.left = false;
1552        }
1553
1554        let buffer_jewel_element = {
1555            let diameter = 7.0;
1556            let icon_color = if item.has_conflict(cx) {
1557                Some(tab_style.icon_conflict)
1558            } else if item.is_dirty(cx) {
1559                Some(tab_style.icon_dirty)
1560            } else {
1561                None
1562            };
1563
1564            Canvas::new(move |bounds, _, _, cx| {
1565                if let Some(color) = icon_color {
1566                    let square = RectF::new(bounds.origin(), vec2f(diameter, diameter));
1567                    cx.scene().push_quad(Quad {
1568                        bounds: square,
1569                        background: Some(color),
1570                        border: Default::default(),
1571                        corner_radii: (diameter / 2.).into(),
1572                    });
1573                }
1574            })
1575            .constrained()
1576            .with_width(diameter)
1577            .with_height(diameter)
1578            .aligned()
1579        };
1580
1581        let title_element = title.aligned().contained().with_style(ContainerStyle {
1582            margin: Margin {
1583                left: tab_style.spacing,
1584                right: tab_style.spacing,
1585                ..Default::default()
1586            },
1587            ..Default::default()
1588        });
1589
1590        let close_element = if hovered {
1591            let item_id = item.id();
1592            enum TabCloseButton {}
1593            let icon = Svg::new("icons/x.svg");
1594            MouseEventHandler::new::<TabCloseButton, _>(item_id, cx, |mouse_state, _| {
1595                if mouse_state.hovered() {
1596                    icon.with_color(tab_style.icon_close_active)
1597                } else {
1598                    icon.with_color(tab_style.icon_close)
1599                }
1600            })
1601            .with_padding(Padding::uniform(4.))
1602            .with_cursor_style(CursorStyle::PointingHand)
1603            .on_click(MouseButton::Left, {
1604                let pane = pane.clone();
1605                move |_, _, cx| {
1606                    let pane = pane.clone();
1607                    cx.window_context().defer(move |cx| {
1608                        if let Some(pane) = pane.upgrade(cx) {
1609                            pane.update(cx, |pane, cx| {
1610                                pane.close_item_by_id(item_id, SaveBehavior::PromptOnWrite, cx)
1611                                    .detach_and_log_err(cx);
1612                            });
1613                        }
1614                    });
1615                }
1616            })
1617            .into_any_named("close-tab-icon")
1618            .constrained()
1619        } else {
1620            Empty::new().constrained()
1621        }
1622        .with_width(tab_style.close_icon_width)
1623        .aligned();
1624
1625        let close_right = settings::get::<ItemSettings>(cx).close_position.right();
1626
1627        if close_right {
1628            Flex::row()
1629                .with_child(buffer_jewel_element)
1630                .with_child(title_element)
1631                .with_child(close_element)
1632        } else {
1633            Flex::row()
1634                .with_child(close_element)
1635                .with_child(title_element)
1636                .with_child(buffer_jewel_element)
1637        }
1638        .contained()
1639        .with_style(container)
1640        .constrained()
1641        .with_height(tab_style.height)
1642        .into_any()
1643    }
1644
1645    pub fn render_tab_bar_button<
1646        F1: 'static + Fn(&mut Pane, &mut EventContext<Pane>),
1647        F2: 'static + Fn(&mut Pane, &mut EventContext<Pane>),
1648    >(
1649        index: usize,
1650        icon: &'static str,
1651        is_active: bool,
1652        tooltip: Option<(&'static str, Option<Box<dyn Action>>)>,
1653        cx: &mut ViewContext<Pane>,
1654        on_click: F1,
1655        on_down: F2,
1656        context_menu: Option<ViewHandle<ContextMenu>>,
1657    ) -> AnyElement<Pane> {
1658        enum TabBarButton {}
1659
1660        let mut button = MouseEventHandler::new::<TabBarButton, _>(index, cx, |mouse_state, cx| {
1661            let theme = &settings::get::<ThemeSettings>(cx).theme.workspace.tab_bar;
1662            let style = theme.pane_button.in_state(is_active).style_for(mouse_state);
1663            Svg::new(icon)
1664                .with_color(style.color)
1665                .constrained()
1666                .with_width(style.icon_width)
1667                .aligned()
1668                .constrained()
1669                .with_width(style.button_width)
1670                .with_height(style.button_width)
1671        })
1672        .with_cursor_style(CursorStyle::PointingHand)
1673        .on_down(MouseButton::Left, move |_, pane, cx| on_down(pane, cx))
1674        .on_click(MouseButton::Left, move |_, pane, cx| on_click(pane, cx))
1675        .into_any();
1676        if let Some((tooltip, action)) = tooltip {
1677            let tooltip_style = settings::get::<ThemeSettings>(cx).theme.tooltip.clone();
1678            button = button
1679                .with_tooltip::<TabBarButton>(index, tooltip, action, tooltip_style, cx)
1680                .into_any();
1681        }
1682
1683        Stack::new()
1684            .with_child(button)
1685            .with_children(
1686                context_menu.map(|menu| ChildView::new(&menu, cx).aligned().bottom().right()),
1687            )
1688            .flex(1., false)
1689            .into_any_named("tab bar button")
1690    }
1691
1692    fn render_blank_pane(&self, theme: &Theme, _cx: &mut ViewContext<Self>) -> AnyElement<Self> {
1693        let background = theme.workspace.background;
1694        Empty::new()
1695            .contained()
1696            .with_background_color(background)
1697            .into_any()
1698    }
1699
1700    pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
1701        self.zoomed = zoomed;
1702        cx.notify();
1703    }
1704
1705    pub fn is_zoomed(&self) -> bool {
1706        self.zoomed
1707    }
1708}
1709
1710impl Entity for Pane {
1711    type Event = Event;
1712}
1713
1714impl View for Pane {
1715    fn ui_name() -> &'static str {
1716        "Pane"
1717    }
1718
1719    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
1720        enum MouseNavigationHandler {}
1721
1722        MouseEventHandler::new::<MouseNavigationHandler, _>(0, cx, |_, cx| {
1723            let active_item_index = self.active_item_index;
1724
1725            if let Some(active_item) = self.active_item() {
1726                Flex::column()
1727                    .with_child({
1728                        let theme = theme::current(cx).clone();
1729
1730                        let mut stack = Stack::new();
1731
1732                        enum TabBarEventHandler {}
1733                        stack.add_child(
1734                            MouseEventHandler::new::<TabBarEventHandler, _>(0, cx, |_, _| {
1735                                Empty::new()
1736                                    .contained()
1737                                    .with_style(theme.workspace.tab_bar.container)
1738                            })
1739                            .on_down(
1740                                MouseButton::Left,
1741                                move |_, this, cx| {
1742                                    this.activate_item(active_item_index, true, true, cx);
1743                                },
1744                            ),
1745                        );
1746                        let tooltip_style = theme.tooltip.clone();
1747                        let tab_bar_theme = theme.workspace.tab_bar.clone();
1748
1749                        let nav_button_height = tab_bar_theme.height;
1750                        let button_style = tab_bar_theme.nav_button;
1751                        let border_for_nav_buttons = tab_bar_theme
1752                            .tab_style(false, false)
1753                            .container
1754                            .border
1755                            .clone();
1756
1757                        let mut tab_row = Flex::row()
1758                            .with_child(nav_button(
1759                                "icons/arrow_left.svg",
1760                                button_style.clone(),
1761                                nav_button_height,
1762                                tooltip_style.clone(),
1763                                self.can_navigate_backward(),
1764                                {
1765                                    move |pane, cx| {
1766                                        if let Some(workspace) = pane.workspace.upgrade(cx) {
1767                                            let pane = cx.weak_handle();
1768                                            cx.window_context().defer(move |cx| {
1769                                                workspace.update(cx, |workspace, cx| {
1770                                                    workspace
1771                                                        .go_back(pane, cx)
1772                                                        .detach_and_log_err(cx)
1773                                                })
1774                                            })
1775                                        }
1776                                    }
1777                                },
1778                                super::GoBack,
1779                                "Go Back",
1780                                cx,
1781                            ))
1782                            .with_child(
1783                                nav_button(
1784                                    "icons/arrow_right.svg",
1785                                    button_style.clone(),
1786                                    nav_button_height,
1787                                    tooltip_style,
1788                                    self.can_navigate_forward(),
1789                                    {
1790                                        move |pane, cx| {
1791                                            if let Some(workspace) = pane.workspace.upgrade(cx) {
1792                                                let pane = cx.weak_handle();
1793                                                cx.window_context().defer(move |cx| {
1794                                                    workspace.update(cx, |workspace, cx| {
1795                                                        workspace
1796                                                            .go_forward(pane, cx)
1797                                                            .detach_and_log_err(cx)
1798                                                    })
1799                                                })
1800                                            }
1801                                        }
1802                                    },
1803                                    super::GoForward,
1804                                    "Go Forward",
1805                                    cx,
1806                                )
1807                                .contained()
1808                                .with_border(border_for_nav_buttons),
1809                            )
1810                            .with_child(self.render_tabs(cx).flex(1., true).into_any_named("tabs"));
1811
1812                        if self.has_focus {
1813                            let render_tab_bar_buttons = self.render_tab_bar_buttons.clone();
1814                            tab_row.add_child(
1815                                (render_tab_bar_buttons)(self, cx)
1816                                    .contained()
1817                                    .with_style(theme.workspace.tab_bar.pane_button_container)
1818                                    .flex(1., false)
1819                                    .into_any(),
1820                            )
1821                        }
1822
1823                        stack.add_child(tab_row);
1824                        stack
1825                            .constrained()
1826                            .with_height(theme.workspace.tab_bar.height)
1827                            .flex(1., false)
1828                            .into_any_named("tab bar")
1829                    })
1830                    .with_child({
1831                        enum PaneContentTabDropTarget {}
1832                        dragged_item_receiver::<PaneContentTabDropTarget, _, _>(
1833                            self,
1834                            0,
1835                            self.active_item_index + 1,
1836                            !self.can_split,
1837                            if self.can_split { Some(100.) } else { None },
1838                            cx,
1839                            {
1840                                let toolbar = self.toolbar.clone();
1841                                let toolbar_hidden = toolbar.read(cx).hidden();
1842                                move |_, cx| {
1843                                    Flex::column()
1844                                        .with_children(
1845                                            (!toolbar_hidden)
1846                                                .then(|| ChildView::new(&toolbar, cx).expanded()),
1847                                        )
1848                                        .with_child(
1849                                            ChildView::new(active_item.as_any(), cx).flex(1., true),
1850                                        )
1851                                }
1852                            },
1853                        )
1854                        .flex(1., true)
1855                    })
1856                    .with_child(ChildView::new(&self.tab_context_menu, cx))
1857                    .into_any()
1858            } else {
1859                enum EmptyPane {}
1860                let theme = theme::current(cx).clone();
1861
1862                dragged_item_receiver::<EmptyPane, _, _>(self, 0, 0, false, None, cx, |_, cx| {
1863                    self.render_blank_pane(&theme, cx)
1864                })
1865                .on_down(MouseButton::Left, |_, _, cx| {
1866                    cx.focus_parent();
1867                })
1868                .into_any()
1869            }
1870        })
1871        .on_down(
1872            MouseButton::Navigate(NavigationDirection::Back),
1873            move |_, pane, cx| {
1874                if let Some(workspace) = pane.workspace.upgrade(cx) {
1875                    let pane = cx.weak_handle();
1876                    cx.window_context().defer(move |cx| {
1877                        workspace.update(cx, |workspace, cx| {
1878                            workspace.go_back(pane, cx).detach_and_log_err(cx)
1879                        })
1880                    })
1881                }
1882            },
1883        )
1884        .on_down(MouseButton::Navigate(NavigationDirection::Forward), {
1885            move |_, pane, cx| {
1886                if let Some(workspace) = pane.workspace.upgrade(cx) {
1887                    let pane = cx.weak_handle();
1888                    cx.window_context().defer(move |cx| {
1889                        workspace.update(cx, |workspace, cx| {
1890                            workspace.go_forward(pane, cx).detach_and_log_err(cx)
1891                        })
1892                    })
1893                }
1894            }
1895        })
1896        .into_any_named("pane")
1897    }
1898
1899    fn focus_in(&mut self, focused: AnyViewHandle, cx: &mut ViewContext<Self>) {
1900        if !self.has_focus {
1901            self.has_focus = true;
1902            cx.emit(Event::Focus);
1903            cx.notify();
1904        }
1905
1906        self.toolbar.update(cx, |toolbar, cx| {
1907            toolbar.focus_changed(true, cx);
1908        });
1909
1910        if let Some(active_item) = self.active_item() {
1911            if cx.is_self_focused() {
1912                // Pane was focused directly. We need to either focus a view inside the active item,
1913                // or focus the active item itself
1914                if let Some(weak_last_focused_view) =
1915                    self.last_focused_view_by_item.get(&active_item.id())
1916                {
1917                    if let Some(last_focused_view) = weak_last_focused_view.upgrade(cx) {
1918                        cx.focus(&last_focused_view);
1919                        return;
1920                    } else {
1921                        self.last_focused_view_by_item.remove(&active_item.id());
1922                    }
1923                }
1924
1925                cx.focus(active_item.as_any());
1926            } else if focused != self.tab_bar_context_menu.handle {
1927                self.last_focused_view_by_item
1928                    .insert(active_item.id(), focused.downgrade());
1929            }
1930        }
1931    }
1932
1933    fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
1934        self.has_focus = false;
1935        self.toolbar.update(cx, |toolbar, cx| {
1936            toolbar.focus_changed(false, cx);
1937        });
1938        cx.notify();
1939    }
1940
1941    fn update_keymap_context(&self, keymap: &mut KeymapContext, _: &AppContext) {
1942        Self::reset_to_default_keymap_context(keymap);
1943    }
1944}
1945
1946impl ItemNavHistory {
1947    pub fn push<D: 'static + Any>(&mut self, data: Option<D>, cx: &mut WindowContext) {
1948        self.history.push(data, self.item.clone(), cx);
1949    }
1950
1951    pub fn pop_backward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
1952        self.history.pop(NavigationMode::GoingBack, cx)
1953    }
1954
1955    pub fn pop_forward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
1956        self.history.pop(NavigationMode::GoingForward, cx)
1957    }
1958}
1959
1960impl NavHistory {
1961    pub fn for_each_entry(
1962        &self,
1963        cx: &AppContext,
1964        mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
1965    ) {
1966        let borrowed_history = self.0.borrow();
1967        borrowed_history
1968            .forward_stack
1969            .iter()
1970            .chain(borrowed_history.backward_stack.iter())
1971            .chain(borrowed_history.closed_stack.iter())
1972            .for_each(|entry| {
1973                if let Some(project_and_abs_path) =
1974                    borrowed_history.paths_by_item.get(&entry.item.id())
1975                {
1976                    f(entry, project_and_abs_path.clone());
1977                } else if let Some(item) = entry.item.upgrade(cx) {
1978                    if let Some(path) = item.project_path(cx) {
1979                        f(entry, (path, None));
1980                    }
1981                }
1982            })
1983    }
1984
1985    pub fn set_mode(&mut self, mode: NavigationMode) {
1986        self.0.borrow_mut().mode = mode;
1987    }
1988
1989    pub fn mode(&self) -> NavigationMode {
1990        self.0.borrow().mode
1991    }
1992
1993    pub fn disable(&mut self) {
1994        self.0.borrow_mut().mode = NavigationMode::Disabled;
1995    }
1996
1997    pub fn enable(&mut self) {
1998        self.0.borrow_mut().mode = NavigationMode::Normal;
1999    }
2000
2001    pub fn pop(&mut self, mode: NavigationMode, cx: &mut WindowContext) -> Option<NavigationEntry> {
2002        let mut state = self.0.borrow_mut();
2003        let entry = match mode {
2004            NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
2005                return None
2006            }
2007            NavigationMode::GoingBack => &mut state.backward_stack,
2008            NavigationMode::GoingForward => &mut state.forward_stack,
2009            NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
2010        }
2011        .pop_back();
2012        if entry.is_some() {
2013            state.did_update(cx);
2014        }
2015        entry
2016    }
2017
2018    pub fn push<D: 'static + Any>(
2019        &mut self,
2020        data: Option<D>,
2021        item: Rc<dyn WeakItemHandle>,
2022        cx: &mut WindowContext,
2023    ) {
2024        let state = &mut *self.0.borrow_mut();
2025        match state.mode {
2026            NavigationMode::Disabled => {}
2027            NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
2028                if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2029                    state.backward_stack.pop_front();
2030                }
2031                state.backward_stack.push_back(NavigationEntry {
2032                    item,
2033                    data: data.map(|data| Box::new(data) as Box<dyn Any>),
2034                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2035                });
2036                state.forward_stack.clear();
2037            }
2038            NavigationMode::GoingBack => {
2039                if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2040                    state.forward_stack.pop_front();
2041                }
2042                state.forward_stack.push_back(NavigationEntry {
2043                    item,
2044                    data: data.map(|data| Box::new(data) as Box<dyn Any>),
2045                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2046                });
2047            }
2048            NavigationMode::GoingForward => {
2049                if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2050                    state.backward_stack.pop_front();
2051                }
2052                state.backward_stack.push_back(NavigationEntry {
2053                    item,
2054                    data: data.map(|data| Box::new(data) as Box<dyn Any>),
2055                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2056                });
2057            }
2058            NavigationMode::ClosingItem => {
2059                if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2060                    state.closed_stack.pop_front();
2061                }
2062                state.closed_stack.push_back(NavigationEntry {
2063                    item,
2064                    data: data.map(|data| Box::new(data) as Box<dyn Any>),
2065                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2066                });
2067            }
2068        }
2069        state.did_update(cx);
2070    }
2071
2072    pub fn remove_item(&mut self, item_id: usize) {
2073        let mut state = self.0.borrow_mut();
2074        state.paths_by_item.remove(&item_id);
2075        state
2076            .backward_stack
2077            .retain(|entry| entry.item.id() != item_id);
2078        state
2079            .forward_stack
2080            .retain(|entry| entry.item.id() != item_id);
2081        state
2082            .closed_stack
2083            .retain(|entry| entry.item.id() != item_id);
2084    }
2085
2086    pub fn path_for_item(&self, item_id: usize) -> Option<(ProjectPath, Option<PathBuf>)> {
2087        self.0.borrow().paths_by_item.get(&item_id).cloned()
2088    }
2089}
2090
2091impl NavHistoryState {
2092    pub fn did_update(&self, cx: &mut WindowContext) {
2093        if let Some(pane) = self.pane.upgrade(cx) {
2094            cx.defer(move |cx| {
2095                pane.update(cx, |pane, cx| pane.history_updated(cx));
2096            });
2097        }
2098    }
2099}
2100
2101pub struct PaneBackdrop<V> {
2102    child_view: usize,
2103    child: AnyElement<V>,
2104}
2105
2106impl<V> PaneBackdrop<V> {
2107    pub fn new(pane_item_view: usize, child: AnyElement<V>) -> Self {
2108        PaneBackdrop {
2109            child,
2110            child_view: pane_item_view,
2111        }
2112    }
2113}
2114
2115impl<V: 'static> Element<V> for PaneBackdrop<V> {
2116    type LayoutState = ();
2117
2118    type PaintState = ();
2119
2120    fn layout(
2121        &mut self,
2122        constraint: gpui::SizeConstraint,
2123        view: &mut V,
2124        cx: &mut ViewContext<V>,
2125    ) -> (Vector2F, Self::LayoutState) {
2126        let size = self.child.layout(constraint, view, cx);
2127        (size, ())
2128    }
2129
2130    fn paint(
2131        &mut self,
2132        bounds: RectF,
2133        visible_bounds: RectF,
2134        _: &mut Self::LayoutState,
2135        view: &mut V,
2136        cx: &mut ViewContext<V>,
2137    ) -> Self::PaintState {
2138        let background = theme::current(cx).editor.background;
2139
2140        let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
2141
2142        cx.scene().push_quad(gpui::Quad {
2143            bounds: RectF::new(bounds.origin(), bounds.size()),
2144            background: Some(background),
2145            ..Default::default()
2146        });
2147
2148        let child_view_id = self.child_view;
2149        cx.scene().push_mouse_region(
2150            MouseRegion::new::<Self>(child_view_id, 0, visible_bounds).on_down(
2151                gpui::platform::MouseButton::Left,
2152                move |_, _: &mut V, cx| {
2153                    let window = cx.window();
2154                    cx.app_context().focus(window, Some(child_view_id))
2155                },
2156            ),
2157        );
2158
2159        cx.scene().push_layer(Some(bounds));
2160        self.child.paint(bounds.origin(), visible_bounds, view, cx);
2161        cx.scene().pop_layer();
2162    }
2163
2164    fn rect_for_text_range(
2165        &self,
2166        range_utf16: std::ops::Range<usize>,
2167        _bounds: RectF,
2168        _visible_bounds: RectF,
2169        _layout: &Self::LayoutState,
2170        _paint: &Self::PaintState,
2171        view: &V,
2172        cx: &gpui::ViewContext<V>,
2173    ) -> Option<RectF> {
2174        self.child.rect_for_text_range(range_utf16, view, cx)
2175    }
2176
2177    fn debug(
2178        &self,
2179        _bounds: RectF,
2180        _layout: &Self::LayoutState,
2181        _paint: &Self::PaintState,
2182        view: &V,
2183        cx: &gpui::ViewContext<V>,
2184    ) -> serde_json::Value {
2185        gpui::json::json!({
2186            "type": "Pane Back Drop",
2187            "view": self.child_view,
2188            "child": self.child.debug(view, cx),
2189        })
2190    }
2191}
2192
2193fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
2194    let path = buffer_path
2195        .as_ref()
2196        .and_then(|p| p.path.to_str())
2197        .unwrap_or(&"Untitled buffer");
2198    let path = truncate_and_remove_front(path, 80);
2199    format!("{path} contains unsaved edits. Do you want to save it?")
2200}
2201
2202#[cfg(test)]
2203mod tests {
2204    use super::*;
2205    use crate::item::test::{TestItem, TestProjectItem};
2206    use gpui::TestAppContext;
2207    use project::FakeFs;
2208    use settings::SettingsStore;
2209
2210    #[gpui::test]
2211    async fn test_remove_active_empty(cx: &mut TestAppContext) {
2212        init_test(cx);
2213        let fs = FakeFs::new(cx.background());
2214
2215        let project = Project::test(fs, None, cx).await;
2216        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2217        let workspace = window.root(cx);
2218        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
2219
2220        pane.update(cx, |pane, cx| {
2221            assert!(pane
2222                .close_active_item(
2223                    &CloseActiveItem {
2224                        save_behavior: None
2225                    },
2226                    cx
2227                )
2228                .is_none())
2229        });
2230    }
2231
2232    #[gpui::test]
2233    async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
2234        cx.foreground().forbid_parking();
2235        init_test(cx);
2236        let fs = FakeFs::new(cx.background());
2237
2238        let project = Project::test(fs, None, cx).await;
2239        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2240        let workspace = window.root(cx);
2241        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
2242
2243        // 1. Add with a destination index
2244        //   a. Add before the active item
2245        set_labeled_items(&pane, ["A", "B*", "C"], cx);
2246        pane.update(cx, |pane, cx| {
2247            pane.add_item(
2248                Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
2249                false,
2250                false,
2251                Some(0),
2252                cx,
2253            );
2254        });
2255        assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
2256
2257        //   b. Add after the active item
2258        set_labeled_items(&pane, ["A", "B*", "C"], cx);
2259        pane.update(cx, |pane, cx| {
2260            pane.add_item(
2261                Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
2262                false,
2263                false,
2264                Some(2),
2265                cx,
2266            );
2267        });
2268        assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
2269
2270        //   c. Add at the end of the item list (including off the length)
2271        set_labeled_items(&pane, ["A", "B*", "C"], cx);
2272        pane.update(cx, |pane, cx| {
2273            pane.add_item(
2274                Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
2275                false,
2276                false,
2277                Some(5),
2278                cx,
2279            );
2280        });
2281        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2282
2283        // 2. Add without a destination index
2284        //   a. Add with active item at the start of the item list
2285        set_labeled_items(&pane, ["A*", "B", "C"], cx);
2286        pane.update(cx, |pane, cx| {
2287            pane.add_item(
2288                Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
2289                false,
2290                false,
2291                None,
2292                cx,
2293            );
2294        });
2295        set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
2296
2297        //   b. Add with active item at the end of the item list
2298        set_labeled_items(&pane, ["A", "B", "C*"], cx);
2299        pane.update(cx, |pane, cx| {
2300            pane.add_item(
2301                Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
2302                false,
2303                false,
2304                None,
2305                cx,
2306            );
2307        });
2308        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2309    }
2310
2311    #[gpui::test]
2312    async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
2313        cx.foreground().forbid_parking();
2314        init_test(cx);
2315        let fs = FakeFs::new(cx.background());
2316
2317        let project = Project::test(fs, None, cx).await;
2318        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2319        let workspace = window.root(cx);
2320        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
2321
2322        // 1. Add with a destination index
2323        //   1a. Add before the active item
2324        let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
2325        pane.update(cx, |pane, cx| {
2326            pane.add_item(d, false, false, Some(0), cx);
2327        });
2328        assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
2329
2330        //   1b. Add after the active item
2331        let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
2332        pane.update(cx, |pane, cx| {
2333            pane.add_item(d, false, false, Some(2), cx);
2334        });
2335        assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
2336
2337        //   1c. Add at the end of the item list (including off the length)
2338        let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
2339        pane.update(cx, |pane, cx| {
2340            pane.add_item(a, false, false, Some(5), cx);
2341        });
2342        assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
2343
2344        //   1d. Add same item to active index
2345        let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
2346        pane.update(cx, |pane, cx| {
2347            pane.add_item(b, false, false, Some(1), cx);
2348        });
2349        assert_item_labels(&pane, ["A", "B*", "C"], cx);
2350
2351        //   1e. Add item to index after same item in last position
2352        let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
2353        pane.update(cx, |pane, cx| {
2354            pane.add_item(c, false, false, Some(2), cx);
2355        });
2356        assert_item_labels(&pane, ["A", "B", "C*"], cx);
2357
2358        // 2. Add without a destination index
2359        //   2a. Add with active item at the start of the item list
2360        let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
2361        pane.update(cx, |pane, cx| {
2362            pane.add_item(d, false, false, None, cx);
2363        });
2364        assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
2365
2366        //   2b. Add with active item at the end of the item list
2367        let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
2368        pane.update(cx, |pane, cx| {
2369            pane.add_item(a, false, false, None, cx);
2370        });
2371        assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
2372
2373        //   2c. Add active item to active item at end of list
2374        let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
2375        pane.update(cx, |pane, cx| {
2376            pane.add_item(c, false, false, None, cx);
2377        });
2378        assert_item_labels(&pane, ["A", "B", "C*"], cx);
2379
2380        //   2d. Add active item to active item at start of list
2381        let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
2382        pane.update(cx, |pane, cx| {
2383            pane.add_item(a, false, false, None, cx);
2384        });
2385        assert_item_labels(&pane, ["A*", "B", "C"], cx);
2386    }
2387
2388    #[gpui::test]
2389    async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
2390        cx.foreground().forbid_parking();
2391        init_test(cx);
2392        let fs = FakeFs::new(cx.background());
2393
2394        let project = Project::test(fs, None, cx).await;
2395        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2396        let workspace = window.root(cx);
2397        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
2398
2399        // singleton view
2400        pane.update(cx, |pane, cx| {
2401            let item = TestItem::new()
2402                .with_singleton(true)
2403                .with_label("buffer 1")
2404                .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)]);
2405
2406            pane.add_item(Box::new(cx.add_view(|_| item)), false, false, None, cx);
2407        });
2408        assert_item_labels(&pane, ["buffer 1*"], cx);
2409
2410        // new singleton view with the same project entry
2411        pane.update(cx, |pane, cx| {
2412            let item = TestItem::new()
2413                .with_singleton(true)
2414                .with_label("buffer 1")
2415                .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]);
2416
2417            pane.add_item(Box::new(cx.add_view(|_| item)), false, false, None, cx);
2418        });
2419        assert_item_labels(&pane, ["buffer 1*"], cx);
2420
2421        // new singleton view with different project entry
2422        pane.update(cx, |pane, cx| {
2423            let item = TestItem::new()
2424                .with_singleton(true)
2425                .with_label("buffer 2")
2426                .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)]);
2427            pane.add_item(Box::new(cx.add_view(|_| item)), false, false, None, cx);
2428        });
2429        assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
2430
2431        // new multibuffer view with the same project entry
2432        pane.update(cx, |pane, cx| {
2433            let item = TestItem::new()
2434                .with_singleton(false)
2435                .with_label("multibuffer 1")
2436                .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]);
2437
2438            pane.add_item(Box::new(cx.add_view(|_| item)), false, false, None, cx);
2439        });
2440        assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
2441
2442        // another multibuffer view with the same project entry
2443        pane.update(cx, |pane, cx| {
2444            let item = TestItem::new()
2445                .with_singleton(false)
2446                .with_label("multibuffer 1b")
2447                .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]);
2448
2449            pane.add_item(Box::new(cx.add_view(|_| item)), false, false, None, cx);
2450        });
2451        assert_item_labels(
2452            &pane,
2453            ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
2454            cx,
2455        );
2456    }
2457
2458    #[gpui::test]
2459    async fn test_remove_item_ordering(cx: &mut TestAppContext) {
2460        init_test(cx);
2461        let fs = FakeFs::new(cx.background());
2462
2463        let project = Project::test(fs, None, cx).await;
2464        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2465        let workspace = window.root(cx);
2466        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
2467
2468        add_labeled_item(&pane, "A", false, cx);
2469        add_labeled_item(&pane, "B", false, cx);
2470        add_labeled_item(&pane, "C", false, cx);
2471        add_labeled_item(&pane, "D", false, cx);
2472        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2473
2474        pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
2475        add_labeled_item(&pane, "1", false, cx);
2476        assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
2477
2478        pane.update(cx, |pane, cx| {
2479            pane.close_active_item(
2480                &CloseActiveItem {
2481                    save_behavior: None,
2482                },
2483                cx,
2484            )
2485        })
2486        .unwrap()
2487        .await
2488        .unwrap();
2489        assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
2490
2491        pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
2492        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2493
2494        pane.update(cx, |pane, cx| {
2495            pane.close_active_item(
2496                &CloseActiveItem {
2497                    save_behavior: None,
2498                },
2499                cx,
2500            )
2501        })
2502        .unwrap()
2503        .await
2504        .unwrap();
2505        assert_item_labels(&pane, ["A", "B*", "C"], cx);
2506
2507        pane.update(cx, |pane, cx| {
2508            pane.close_active_item(
2509                &CloseActiveItem {
2510                    save_behavior: None,
2511                },
2512                cx,
2513            )
2514        })
2515        .unwrap()
2516        .await
2517        .unwrap();
2518        assert_item_labels(&pane, ["A", "C*"], cx);
2519
2520        pane.update(cx, |pane, cx| {
2521            pane.close_active_item(
2522                &CloseActiveItem {
2523                    save_behavior: None,
2524                },
2525                cx,
2526            )
2527        })
2528        .unwrap()
2529        .await
2530        .unwrap();
2531        assert_item_labels(&pane, ["A*"], cx);
2532    }
2533
2534    #[gpui::test]
2535    async fn test_close_inactive_items(cx: &mut TestAppContext) {
2536        init_test(cx);
2537        let fs = FakeFs::new(cx.background());
2538
2539        let project = Project::test(fs, None, cx).await;
2540        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2541        let workspace = window.root(cx);
2542        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
2543
2544        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
2545
2546        let task = pane
2547            .update(cx, |pane, cx| {
2548                pane.close_inactive_items(&CloseInactiveItems, cx)
2549            })
2550            .unwrap();
2551        cx.foreground().run_until_parked();
2552        window.simulate_prompt_answer(2, cx);
2553        task.await.unwrap();
2554        assert_item_labels(&pane, ["C*"], cx);
2555    }
2556
2557    #[gpui::test]
2558    async fn test_close_clean_items(cx: &mut TestAppContext) {
2559        init_test(cx);
2560        let fs = FakeFs::new(cx.background());
2561
2562        let project = Project::test(fs, None, cx).await;
2563        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2564        let workspace = window.root(cx);
2565        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
2566
2567        add_labeled_item(&pane, "A", true, cx);
2568        add_labeled_item(&pane, "B", false, cx);
2569        add_labeled_item(&pane, "C", true, cx);
2570        add_labeled_item(&pane, "D", false, cx);
2571        add_labeled_item(&pane, "E", false, cx);
2572        assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
2573
2574        let task = pane
2575            .update(cx, |pane, cx| pane.close_clean_items(&CloseCleanItems, cx))
2576            .unwrap();
2577        cx.foreground().run_until_parked();
2578        window.simulate_prompt_answer(2, cx);
2579        task.await.unwrap();
2580        assert_item_labels(&pane, ["A^", "C*^"], cx);
2581    }
2582
2583    #[gpui::test]
2584    async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
2585        init_test(cx);
2586        let fs = FakeFs::new(cx.background());
2587
2588        let project = Project::test(fs, None, cx).await;
2589        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2590        let workspace = window.root(cx);
2591        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
2592
2593        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
2594
2595        let task = pane
2596            .update(cx, |pane, cx| {
2597                pane.close_items_to_the_left(&CloseItemsToTheLeft, cx)
2598            })
2599            .unwrap();
2600        cx.foreground().run_until_parked();
2601        window.simulate_prompt_answer(2, cx);
2602        task.await.unwrap();
2603        assert_item_labels(&pane, ["C*", "D", "E"], cx);
2604    }
2605
2606    #[gpui::test]
2607    async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
2608        init_test(cx);
2609        let fs = FakeFs::new(cx.background());
2610
2611        let project = Project::test(fs, None, cx).await;
2612        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2613        let workspace = window.root(cx);
2614        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
2615
2616        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
2617
2618        let task = pane
2619            .update(cx, |pane, cx| {
2620                pane.close_items_to_the_right(&CloseItemsToTheRight, cx)
2621            })
2622            .unwrap();
2623        cx.foreground().run_until_parked();
2624        window.simulate_prompt_answer(2, cx);
2625        task.await.unwrap();
2626        assert_item_labels(&pane, ["A", "B", "C*"], cx);
2627    }
2628
2629    #[gpui::test]
2630    async fn test_close_all_items(cx: &mut TestAppContext) {
2631        init_test(cx);
2632        let fs = FakeFs::new(cx.background());
2633
2634        let project = Project::test(fs, None, cx).await;
2635        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2636        let workspace = window.root(cx);
2637        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
2638
2639        add_labeled_item(&pane, "A", false, cx);
2640        add_labeled_item(&pane, "B", false, cx);
2641        add_labeled_item(&pane, "C", false, cx);
2642        assert_item_labels(&pane, ["A", "B", "C*"], cx);
2643
2644        let t = pane
2645            .update(cx, |pane, cx| pane.close_all_items(&CloseAllItems, cx))
2646            .unwrap();
2647        cx.foreground().run_until_parked();
2648        window.simulate_prompt_answer(2, cx);
2649        t.await.unwrap();
2650        assert_item_labels(&pane, [], cx);
2651    }
2652
2653    fn init_test(cx: &mut TestAppContext) {
2654        cx.update(|cx| {
2655            cx.set_global(SettingsStore::test(cx));
2656            theme::init((), cx);
2657            crate::init_settings(cx);
2658            Project::init_settings(cx);
2659        });
2660    }
2661
2662    fn add_labeled_item(
2663        pane: &ViewHandle<Pane>,
2664        label: &str,
2665        is_dirty: bool,
2666        cx: &mut TestAppContext,
2667    ) -> Box<ViewHandle<TestItem>> {
2668        pane.update(cx, |pane, cx| {
2669            let labeled_item =
2670                Box::new(cx.add_view(|_| TestItem::new().with_label(label).with_dirty(is_dirty)));
2671            pane.add_item(labeled_item.clone(), false, false, None, cx);
2672            labeled_item
2673        })
2674    }
2675
2676    fn set_labeled_items<const COUNT: usize>(
2677        pane: &ViewHandle<Pane>,
2678        labels: [&str; COUNT],
2679        cx: &mut TestAppContext,
2680    ) -> [Box<ViewHandle<TestItem>>; COUNT] {
2681        pane.update(cx, |pane, cx| {
2682            pane.items.clear();
2683            let mut active_item_index = 0;
2684
2685            let mut index = 0;
2686            let items = labels.map(|mut label| {
2687                if label.ends_with("*") {
2688                    label = label.trim_end_matches("*");
2689                    active_item_index = index;
2690                }
2691
2692                let labeled_item = Box::new(cx.add_view(|_| TestItem::new().with_label(label)));
2693                pane.add_item(labeled_item.clone(), false, false, None, cx);
2694                index += 1;
2695                labeled_item
2696            });
2697
2698            pane.activate_item(active_item_index, false, false, cx);
2699
2700            items
2701        })
2702    }
2703
2704    // Assert the item label, with the active item label suffixed with a '*'
2705    fn assert_item_labels<const COUNT: usize>(
2706        pane: &ViewHandle<Pane>,
2707        expected_states: [&str; COUNT],
2708        cx: &mut TestAppContext,
2709    ) {
2710        pane.read_with(cx, |pane, cx| {
2711            let actual_states = pane
2712                .items
2713                .iter()
2714                .enumerate()
2715                .map(|(ix, item)| {
2716                    let mut state = item
2717                        .as_any()
2718                        .downcast_ref::<TestItem>()
2719                        .unwrap()
2720                        .read(cx)
2721                        .label
2722                        .clone();
2723                    if ix == pane.active_item_index {
2724                        state.push('*');
2725                    }
2726                    if item.is_dirty(cx) {
2727                        state.push('^');
2728                    }
2729                    state
2730                })
2731                .collect::<Vec<_>>();
2732
2733            assert_eq!(
2734                actual_states, expected_states,
2735                "pane items do not match expectation"
2736            );
2737        })
2738    }
2739}