pane.rs

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