pane.rs

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