pane.rs

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