pane.rs

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