pane.rs

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