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