pane.rs

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