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