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