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