pane.rs

   1use crate::{
   2    item::{ClosePosition, Item, ItemHandle, ItemSettings, WeakItemHandle},
   3    toolbar::Toolbar,
   4    workspace_settings::{AutosaveSetting, WorkspaceSettings},
   5    NewCenterTerminal, NewFile, NewSearch, OpenVisible, SplitDirection, ToggleZoom, Workspace,
   6};
   7use anyhow::Result;
   8use collections::{HashMap, HashSet, VecDeque};
   9use futures::{stream::FuturesUnordered, StreamExt};
  10use gpui::{
  11    actions, impl_actions, overlay, prelude::*, Action, AnchorCorner, AnyElement, AppContext,
  12    AsyncWindowContext, ClickEvent, DismissEvent, Div, DragMoveEvent, EntityId, EventEmitter,
  13    ExternalPaths, FocusHandle, FocusableView, Model, MouseButton, NavigationDirection, Pixels,
  14    Point, PromptLevel, Render, ScrollHandle, Subscription, Task, View, ViewContext, VisualContext,
  15    WeakFocusHandle, WeakView, WindowContext,
  16};
  17use parking_lot::Mutex;
  18use project::{Project, ProjectEntryId, ProjectPath};
  19use serde::Deserialize;
  20use settings::Settings;
  21use std::{
  22    any::Any,
  23    cmp, fmt, mem,
  24    ops::ControlFlow,
  25    path::{Path, PathBuf},
  26    rc::Rc,
  27    sync::{
  28        atomic::{AtomicUsize, Ordering},
  29        Arc,
  30    },
  31};
  32use theme::ThemeSettings;
  33
  34use ui::{
  35    prelude::*, right_click_menu, ButtonSize, Color, IconButton, IconButtonShape, IconName,
  36    IconSize, Indicator, Label, Tab, TabBar, TabPosition, Tooltip,
  37};
  38use ui::{v_flex, ContextMenu};
  39use util::{maybe, truncate_and_remove_front, ResultExt};
  40
  41#[derive(PartialEq, Clone, Copy, Deserialize, Debug)]
  42#[serde(rename_all = "camelCase")]
  43pub enum SaveIntent {
  44    /// write all files (even if unchanged)
  45    /// prompt before overwriting on-disk changes
  46    Save,
  47    /// same as Save, but without auto formatting
  48    SaveWithoutFormat,
  49    /// write any files that have local changes
  50    /// prompt before overwriting on-disk changes
  51    SaveAll,
  52    /// always prompt for a new path
  53    SaveAs,
  54    /// prompt "you have unsaved changes" before writing
  55    Close,
  56    /// write all dirty files, don't prompt on conflict
  57    Overwrite,
  58    /// skip all save-related behavior
  59    Skip,
  60}
  61
  62#[derive(Clone, Deserialize, PartialEq, Debug)]
  63pub struct ActivateItem(pub usize);
  64
  65#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
  66#[serde(rename_all = "camelCase")]
  67pub struct CloseActiveItem {
  68    pub save_intent: Option<SaveIntent>,
  69}
  70
  71#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
  72#[serde(rename_all = "camelCase")]
  73pub struct CloseInactiveItems {
  74    pub save_intent: Option<SaveIntent>,
  75}
  76
  77#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
  78#[serde(rename_all = "camelCase")]
  79pub struct CloseAllItems {
  80    pub save_intent: Option<SaveIntent>,
  81}
  82
  83#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
  84#[serde(rename_all = "camelCase")]
  85pub struct RevealInProjectPanel {
  86    pub entry_id: Option<u64>,
  87}
  88
  89impl_actions!(
  90    pane,
  91    [
  92        CloseAllItems,
  93        CloseActiveItem,
  94        CloseInactiveItems,
  95        ActivateItem,
  96        RevealInProjectPanel
  97    ]
  98);
  99
 100actions!(
 101    pane,
 102    [
 103        ActivatePrevItem,
 104        ActivateNextItem,
 105        ActivateLastItem,
 106        CloseCleanItems,
 107        CloseItemsToTheLeft,
 108        CloseItemsToTheRight,
 109        GoBack,
 110        DeploySearch,
 111        GoForward,
 112        ReopenClosedItem,
 113        SplitLeft,
 114        SplitUp,
 115        SplitRight,
 116        SplitDown,
 117    ]
 118);
 119
 120const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
 121
 122pub enum Event {
 123    AddItem { item: Box<dyn ItemHandle> },
 124    ActivateItem { local: bool },
 125    Remove,
 126    RemoveItem { item_id: EntityId },
 127    Split(SplitDirection),
 128    ChangeItemTitle,
 129    Focus,
 130    ZoomIn,
 131    ZoomOut,
 132}
 133
 134impl fmt::Debug for Event {
 135    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
 136        match self {
 137            Event::AddItem { item } => f
 138                .debug_struct("AddItem")
 139                .field("item", &item.item_id())
 140                .finish(),
 141            Event::ActivateItem { local } => f
 142                .debug_struct("ActivateItem")
 143                .field("local", local)
 144                .finish(),
 145            Event::Remove => f.write_str("Remove"),
 146            Event::RemoveItem { item_id } => f
 147                .debug_struct("RemoveItem")
 148                .field("item_id", item_id)
 149                .finish(),
 150            Event::Split(direction) => f
 151                .debug_struct("Split")
 152                .field("direction", direction)
 153                .finish(),
 154            Event::ChangeItemTitle => f.write_str("ChangeItemTitle"),
 155            Event::Focus => f.write_str("Focus"),
 156            Event::ZoomIn => f.write_str("ZoomIn"),
 157            Event::ZoomOut => f.write_str("ZoomOut"),
 158        }
 159    }
 160}
 161
 162/// A container for 0 to many items that are open in the workspace.
 163/// Treats all items uniformly via the [`ItemHandle`] trait, whether it's an editor, search results multibuffer, terminal or something else,
 164/// responsible for managing item tabs, focus and zoom states and drag and drop features.
 165/// Can be split, see `PaneGroup` for more details.
 166pub struct Pane {
 167    focus_handle: FocusHandle,
 168    items: Vec<Box<dyn ItemHandle>>,
 169    activation_history: Vec<EntityId>,
 170    zoomed: bool,
 171    was_focused: bool,
 172    active_item_index: usize,
 173    last_focus_handle_by_item: HashMap<EntityId, WeakFocusHandle>,
 174    nav_history: NavHistory,
 175    toolbar: View<Toolbar>,
 176    new_item_menu: Option<View<ContextMenu>>,
 177    split_item_menu: Option<View<ContextMenu>>,
 178    //     tab_context_menu: View<ContextMenu>,
 179    pub(crate) workspace: WeakView<Workspace>,
 180    project: Model<Project>,
 181    drag_split_direction: Option<SplitDirection>,
 182    can_drop_predicate: Option<Arc<dyn Fn(&dyn Any, &mut WindowContext) -> bool>>,
 183    custom_drop_handle:
 184        Option<Arc<dyn Fn(&mut Pane, &dyn Any, &mut ViewContext<Pane>) -> ControlFlow<(), ()>>>,
 185    can_split: bool,
 186    render_tab_bar_buttons: Rc<dyn Fn(&mut Pane, &mut ViewContext<Pane>) -> AnyElement>,
 187    _subscriptions: Vec<Subscription>,
 188    tab_bar_scroll_handle: ScrollHandle,
 189    display_nav_history_buttons: bool,
 190    double_click_dispatch_action: Box<dyn Action>,
 191}
 192
 193pub struct ItemNavHistory {
 194    history: NavHistory,
 195    item: Arc<dyn WeakItemHandle>,
 196}
 197
 198#[derive(Clone)]
 199pub struct NavHistory(Arc<Mutex<NavHistoryState>>);
 200
 201struct NavHistoryState {
 202    mode: NavigationMode,
 203    backward_stack: VecDeque<NavigationEntry>,
 204    forward_stack: VecDeque<NavigationEntry>,
 205    closed_stack: VecDeque<NavigationEntry>,
 206    paths_by_item: HashMap<EntityId, (ProjectPath, Option<PathBuf>)>,
 207    pane: WeakView<Pane>,
 208    next_timestamp: Arc<AtomicUsize>,
 209}
 210
 211#[derive(Debug, Copy, Clone)]
 212pub enum NavigationMode {
 213    Normal,
 214    GoingBack,
 215    GoingForward,
 216    ClosingItem,
 217    ReopeningClosedItem,
 218    Disabled,
 219}
 220
 221impl Default for NavigationMode {
 222    fn default() -> Self {
 223        Self::Normal
 224    }
 225}
 226
 227pub struct NavigationEntry {
 228    pub item: Arc<dyn WeakItemHandle>,
 229    pub data: Option<Box<dyn Any + Send>>,
 230    pub timestamp: usize,
 231}
 232
 233#[derive(Clone)]
 234pub struct DraggedTab {
 235    pub pane: View<Pane>,
 236    pub item: Box<dyn ItemHandle>,
 237    pub ix: usize,
 238    pub detail: usize,
 239    pub is_active: bool,
 240}
 241
 242impl EventEmitter<Event> for Pane {}
 243
 244impl Pane {
 245    pub fn new(
 246        workspace: WeakView<Workspace>,
 247        project: Model<Project>,
 248        next_timestamp: Arc<AtomicUsize>,
 249        can_drop_predicate: Option<Arc<dyn Fn(&dyn Any, &mut WindowContext) -> bool + 'static>>,
 250        double_click_dispatch_action: Box<dyn Action>,
 251        cx: &mut ViewContext<Self>,
 252    ) -> Self {
 253        let focus_handle = cx.focus_handle();
 254
 255        let subscriptions = vec![
 256            cx.on_focus(&focus_handle, Pane::focus_in),
 257            cx.on_focus_in(&focus_handle, Pane::focus_in),
 258            cx.on_focus_out(&focus_handle, Pane::focus_out),
 259        ];
 260
 261        let handle = cx.view().downgrade();
 262        Self {
 263            focus_handle,
 264            items: Vec::new(),
 265            activation_history: Vec::new(),
 266            was_focused: false,
 267            zoomed: false,
 268            active_item_index: 0,
 269            last_focus_handle_by_item: Default::default(),
 270            nav_history: NavHistory(Arc::new(Mutex::new(NavHistoryState {
 271                mode: NavigationMode::Normal,
 272                backward_stack: Default::default(),
 273                forward_stack: Default::default(),
 274                closed_stack: Default::default(),
 275                paths_by_item: Default::default(),
 276                pane: handle.clone(),
 277                next_timestamp,
 278            }))),
 279            toolbar: cx.new_view(|_| Toolbar::new()),
 280            new_item_menu: None,
 281            split_item_menu: None,
 282            tab_bar_scroll_handle: ScrollHandle::new(),
 283            drag_split_direction: None,
 284            workspace,
 285            project,
 286            can_drop_predicate,
 287            custom_drop_handle: None,
 288            can_split: true,
 289            render_tab_bar_buttons: Rc::new(move |pane, cx| {
 290                h_flex()
 291                    .gap_2()
 292                    .child(
 293                        IconButton::new("plus", IconName::Plus)
 294                            .icon_size(IconSize::Small)
 295                            .icon_color(Color::Muted)
 296                            .on_click(cx.listener(|pane, _, cx| {
 297                                let menu = ContextMenu::build(cx, |menu, _| {
 298                                    menu.action("New File", NewFile.boxed_clone())
 299                                        .action("New Terminal", NewCenterTerminal.boxed_clone())
 300                                        .action("New Search", NewSearch.boxed_clone())
 301                                });
 302                                cx.subscribe(&menu, |pane, _, _: &DismissEvent, cx| {
 303                                    pane.focus(cx);
 304                                    pane.new_item_menu = None;
 305                                })
 306                                .detach();
 307                                pane.new_item_menu = Some(menu);
 308                            }))
 309                            .tooltip(|cx| Tooltip::text("New...", cx)),
 310                    )
 311                    .when_some(pane.new_item_menu.as_ref(), |el, new_item_menu| {
 312                        el.child(Self::render_menu_overlay(new_item_menu))
 313                    })
 314                    .child(
 315                        IconButton::new("split", IconName::Split)
 316                            .icon_size(IconSize::Small)
 317                            .icon_color(Color::Muted)
 318                            .on_click(cx.listener(|pane, _, cx| {
 319                                let menu = ContextMenu::build(cx, |menu, _| {
 320                                    menu.action("Split Right", SplitRight.boxed_clone())
 321                                        .action("Split Left", SplitLeft.boxed_clone())
 322                                        .action("Split Up", SplitUp.boxed_clone())
 323                                        .action("Split Down", SplitDown.boxed_clone())
 324                                });
 325                                cx.subscribe(&menu, |pane, _, _: &DismissEvent, cx| {
 326                                    pane.focus(cx);
 327                                    pane.split_item_menu = None;
 328                                })
 329                                .detach();
 330                                pane.split_item_menu = Some(menu);
 331                            }))
 332                            .tooltip(|cx| Tooltip::text("Split Pane", cx)),
 333                    )
 334                    .child({
 335                        let zoomed = pane.is_zoomed();
 336                        IconButton::new("toggle_zoom", IconName::Maximize)
 337                            .icon_size(IconSize::Small)
 338                            .icon_color(Color::Muted)
 339                            .selected(zoomed)
 340                            .selected_icon(IconName::Minimize)
 341                            .on_click(cx.listener(|pane, _, cx| {
 342                                pane.toggle_zoom(&crate::ToggleZoom, cx);
 343                            }))
 344                            .tooltip(move |cx| {
 345                                Tooltip::text(if zoomed { "Zoom Out" } else { "Zoom In" }, cx)
 346                            })
 347                    })
 348                    .when_some(pane.split_item_menu.as_ref(), |el, split_item_menu| {
 349                        el.child(Self::render_menu_overlay(split_item_menu))
 350                    })
 351                    .into_any_element()
 352            }),
 353            display_nav_history_buttons: true,
 354            _subscriptions: subscriptions,
 355            double_click_dispatch_action,
 356        }
 357    }
 358
 359    pub fn has_focus(&self, cx: &WindowContext) -> bool {
 360        // We not only check whether our focus handle contains focus, but also
 361        // whether the active_item might have focus, because we might have just activated an item
 362        // but that hasn't rendered yet.
 363        // So before the next render, we might have transferred focus
 364        // to the item and `focus_handle.contains_focus` returns false because the `active_item`
 365        // is not hooked up to us in the dispatch tree.
 366        self.focus_handle.contains_focused(cx)
 367            || self
 368                .active_item()
 369                .map_or(false, |item| item.focus_handle(cx).contains_focused(cx))
 370    }
 371
 372    fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
 373        if !self.was_focused {
 374            self.was_focused = true;
 375            cx.emit(Event::Focus);
 376            cx.notify();
 377        }
 378
 379        self.toolbar.update(cx, |toolbar, cx| {
 380            toolbar.focus_changed(true, cx);
 381        });
 382
 383        if let Some(active_item) = self.active_item() {
 384            if self.focus_handle.is_focused(cx) {
 385                // Pane was focused directly. We need to either focus a view inside the active item,
 386                // or focus the active item itself
 387                if let Some(weak_last_focus_handle) =
 388                    self.last_focus_handle_by_item.get(&active_item.item_id())
 389                {
 390                    if let Some(focus_handle) = weak_last_focus_handle.upgrade() {
 391                        focus_handle.focus(cx);
 392                        return;
 393                    }
 394                }
 395
 396                active_item.focus_handle(cx).focus(cx);
 397            } else if let Some(focused) = cx.focused() {
 398                if !self.context_menu_focused(cx) {
 399                    self.last_focus_handle_by_item
 400                        .insert(active_item.item_id(), focused.downgrade());
 401                }
 402            }
 403        }
 404    }
 405
 406    fn context_menu_focused(&self, cx: &mut ViewContext<Self>) -> bool {
 407        self.new_item_menu
 408            .as_ref()
 409            .or(self.split_item_menu.as_ref())
 410            .map_or(false, |menu| menu.focus_handle(cx).is_focused(cx))
 411    }
 412
 413    fn focus_out(&mut self, cx: &mut ViewContext<Self>) {
 414        self.was_focused = false;
 415        self.toolbar.update(cx, |toolbar, cx| {
 416            toolbar.focus_changed(false, cx);
 417        });
 418        cx.notify();
 419    }
 420
 421    pub fn active_item_index(&self) -> usize {
 422        self.active_item_index
 423    }
 424
 425    pub fn 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()
1566            .absolute()
1567            .bottom_0()
1568            .right_0()
1569            .size_0()
1570            .child(overlay().anchor(AnchorCorner::TopRight).child(menu.clone()))
1571    }
1572
1573    pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
1574        self.zoomed = zoomed;
1575        cx.notify();
1576    }
1577
1578    pub fn is_zoomed(&self) -> bool {
1579        self.zoomed
1580    }
1581
1582    fn handle_drag_move<T>(&mut self, event: &DragMoveEvent<T>, cx: &mut ViewContext<Self>) {
1583        if !self.can_split {
1584            return;
1585        }
1586
1587        let edge_width = cx.rem_size() * 8;
1588        let cursor = event.event.position;
1589        let direction = if cursor.x < event.bounds.left() + edge_width {
1590            Some(SplitDirection::Left)
1591        } else if cursor.x > event.bounds.right() - edge_width {
1592            Some(SplitDirection::Right)
1593        } else if cursor.y < event.bounds.top() + edge_width {
1594            Some(SplitDirection::Up)
1595        } else if cursor.y > event.bounds.bottom() - edge_width {
1596            Some(SplitDirection::Down)
1597        } else {
1598            None
1599        };
1600
1601        if direction != self.drag_split_direction {
1602            self.drag_split_direction = direction;
1603        }
1604    }
1605
1606    fn handle_tab_drop(
1607        &mut self,
1608        dragged_tab: &DraggedTab,
1609        ix: usize,
1610        cx: &mut ViewContext<'_, Self>,
1611    ) {
1612        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
1613            if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_tab, cx) {
1614                return;
1615            }
1616        }
1617        let mut to_pane = cx.view().clone();
1618        let split_direction = self.drag_split_direction;
1619        let item_id = dragged_tab.item.item_id();
1620        let from_pane = dragged_tab.pane.clone();
1621        self.workspace
1622            .update(cx, |_, cx| {
1623                cx.defer(move |workspace, cx| {
1624                    if let Some(split_direction) = split_direction {
1625                        to_pane = workspace.split_pane(to_pane, split_direction, cx);
1626                    }
1627                    workspace.move_item(from_pane, to_pane, item_id, ix, cx);
1628                });
1629            })
1630            .log_err();
1631    }
1632
1633    fn handle_project_entry_drop(
1634        &mut self,
1635        project_entry_id: &ProjectEntryId,
1636        cx: &mut ViewContext<'_, Self>,
1637    ) {
1638        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
1639            if let ControlFlow::Break(()) = custom_drop_handle(self, project_entry_id, cx) {
1640                return;
1641            }
1642        }
1643        let mut to_pane = cx.view().clone();
1644        let split_direction = self.drag_split_direction;
1645        let project_entry_id = *project_entry_id;
1646        self.workspace
1647            .update(cx, |_, cx| {
1648                cx.defer(move |workspace, cx| {
1649                    if let Some(path) = workspace
1650                        .project()
1651                        .read(cx)
1652                        .path_for_entry(project_entry_id, cx)
1653                    {
1654                        if let Some(split_direction) = split_direction {
1655                            to_pane = workspace.split_pane(to_pane, split_direction, cx);
1656                        }
1657                        workspace
1658                            .open_path(path, Some(to_pane.downgrade()), true, cx)
1659                            .detach_and_log_err(cx);
1660                    }
1661                });
1662            })
1663            .log_err();
1664    }
1665
1666    fn handle_external_paths_drop(
1667        &mut self,
1668        paths: &ExternalPaths,
1669        cx: &mut ViewContext<'_, Self>,
1670    ) {
1671        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
1672            if let ControlFlow::Break(()) = custom_drop_handle(self, paths, cx) {
1673                return;
1674            }
1675        }
1676        let mut to_pane = cx.view().clone();
1677        let mut split_direction = self.drag_split_direction;
1678        let paths = paths.paths().to_vec();
1679        self.workspace
1680            .update(cx, |workspace, cx| {
1681                let fs = Arc::clone(workspace.project().read(cx).fs());
1682                cx.spawn(|workspace, mut cx| async move {
1683                    let mut is_file_checks = FuturesUnordered::new();
1684                    for path in &paths {
1685                        is_file_checks.push(fs.is_file(path))
1686                    }
1687                    let mut has_files_to_open = false;
1688                    while let Some(is_file) = is_file_checks.next().await {
1689                        if is_file {
1690                            has_files_to_open = true;
1691                            break;
1692                        }
1693                    }
1694                    drop(is_file_checks);
1695                    if !has_files_to_open {
1696                        split_direction = None;
1697                    }
1698
1699                    if let Some(open_task) = workspace
1700                        .update(&mut cx, |workspace, cx| {
1701                            if let Some(split_direction) = split_direction {
1702                                to_pane = workspace.split_pane(to_pane, split_direction, cx);
1703                            }
1704                            workspace.open_paths(
1705                                paths,
1706                                OpenVisible::OnlyDirectories,
1707                                Some(to_pane.downgrade()),
1708                                cx,
1709                            )
1710                        })
1711                        .ok()
1712                    {
1713                        let _opened_items: Vec<_> = open_task.await;
1714                    }
1715                })
1716                .detach();
1717            })
1718            .log_err();
1719    }
1720
1721    pub fn display_nav_history_buttons(&mut self, display: bool) {
1722        self.display_nav_history_buttons = display;
1723    }
1724}
1725
1726impl FocusableView for Pane {
1727    fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
1728        self.focus_handle.clone()
1729    }
1730}
1731
1732impl Render for Pane {
1733    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
1734        v_flex()
1735            .key_context("Pane")
1736            .track_focus(&self.focus_handle)
1737            .size_full()
1738            .flex_none()
1739            .overflow_hidden()
1740            .on_action(cx.listener(|pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx)))
1741            .on_action(cx.listener(|pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx)))
1742            .on_action(
1743                cx.listener(|pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx)),
1744            )
1745            .on_action(cx.listener(|pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx)))
1746            .on_action(cx.listener(|pane, _: &GoBack, cx| pane.navigate_backward(cx)))
1747            .on_action(cx.listener(|pane, _: &GoForward, cx| pane.navigate_forward(cx)))
1748            .on_action(cx.listener(Pane::toggle_zoom))
1749            .on_action(cx.listener(|pane: &mut Pane, action: &ActivateItem, cx| {
1750                pane.activate_item(action.0, true, true, cx);
1751            }))
1752            .on_action(cx.listener(|pane: &mut Pane, _: &ActivateLastItem, cx| {
1753                pane.activate_item(pane.items.len() - 1, true, true, cx);
1754            }))
1755            .on_action(cx.listener(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
1756                pane.activate_prev_item(true, cx);
1757            }))
1758            .on_action(cx.listener(|pane: &mut Pane, _: &ActivateNextItem, cx| {
1759                pane.activate_next_item(true, cx);
1760            }))
1761            .on_action(
1762                cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
1763                    if let Some(task) = pane.close_active_item(action, cx) {
1764                        task.detach_and_log_err(cx)
1765                    }
1766                }),
1767            )
1768            .on_action(
1769                cx.listener(|pane: &mut Self, action: &CloseInactiveItems, cx| {
1770                    if let Some(task) = pane.close_inactive_items(action, cx) {
1771                        task.detach_and_log_err(cx)
1772                    }
1773                }),
1774            )
1775            .on_action(
1776                cx.listener(|pane: &mut Self, action: &CloseCleanItems, cx| {
1777                    if let Some(task) = pane.close_clean_items(action, cx) {
1778                        task.detach_and_log_err(cx)
1779                    }
1780                }),
1781            )
1782            .on_action(
1783                cx.listener(|pane: &mut Self, action: &CloseItemsToTheLeft, cx| {
1784                    if let Some(task) = pane.close_items_to_the_left(action, cx) {
1785                        task.detach_and_log_err(cx)
1786                    }
1787                }),
1788            )
1789            .on_action(
1790                cx.listener(|pane: &mut Self, action: &CloseItemsToTheRight, cx| {
1791                    if let Some(task) = pane.close_items_to_the_right(action, cx) {
1792                        task.detach_and_log_err(cx)
1793                    }
1794                }),
1795            )
1796            .on_action(cx.listener(|pane: &mut Self, action: &CloseAllItems, cx| {
1797                if let Some(task) = pane.close_all_items(action, cx) {
1798                    task.detach_and_log_err(cx)
1799                }
1800            }))
1801            .on_action(
1802                cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
1803                    if let Some(task) = pane.close_active_item(action, cx) {
1804                        task.detach_and_log_err(cx)
1805                    }
1806                }),
1807            )
1808            .on_action(
1809                cx.listener(|pane: &mut Self, action: &RevealInProjectPanel, cx| {
1810                    let entry_id = action
1811                        .entry_id
1812                        .map(ProjectEntryId::from_proto)
1813                        .or_else(|| pane.active_item()?.project_entry_ids(cx).first().copied());
1814                    if let Some(entry_id) = entry_id {
1815                        pane.project.update(cx, |_, cx| {
1816                            cx.emit(project::Event::RevealInProjectPanel(entry_id))
1817                        });
1818                    }
1819                }),
1820            )
1821            .when(self.active_item().is_some(), |pane| {
1822                pane.child(self.render_tab_bar(cx))
1823            })
1824            .child({
1825                let has_worktrees = self.project.read(cx).worktrees().next().is_some();
1826                // main content
1827                div()
1828                    .flex_1()
1829                    .relative()
1830                    .group("")
1831                    .on_drag_move::<DraggedTab>(cx.listener(Self::handle_drag_move))
1832                    .on_drag_move::<ProjectEntryId>(cx.listener(Self::handle_drag_move))
1833                    .on_drag_move::<ExternalPaths>(cx.listener(Self::handle_drag_move))
1834                    .map(|div| {
1835                        if let Some(item) = self.active_item() {
1836                            div.v_flex()
1837                                .child(self.toolbar.clone())
1838                                .child(item.to_any())
1839                        } else {
1840                            let placeholder = div.h_flex().size_full().justify_center();
1841                            if has_worktrees {
1842                                placeholder
1843                            } else {
1844                                placeholder.child(
1845                                    Label::new("Open a file or project to get started.")
1846                                        .color(Color::Muted),
1847                                )
1848                            }
1849                        }
1850                    })
1851                    .child(
1852                        // drag target
1853                        div()
1854                            .invisible()
1855                            .absolute()
1856                            .bg(theme::color_alpha(
1857                                cx.theme().colors().drop_target_background,
1858                                0.75,
1859                            ))
1860                            .group_drag_over::<DraggedTab>("", |style| style.visible())
1861                            .group_drag_over::<ProjectEntryId>("", |style| style.visible())
1862                            .group_drag_over::<ExternalPaths>("", |style| style.visible())
1863                            .when_some(self.can_drop_predicate.clone(), |this, p| {
1864                                this.can_drop(move |a, cx| p(a, cx))
1865                            })
1866                            .on_drop(cx.listener(move |this, dragged_tab, cx| {
1867                                this.handle_tab_drop(dragged_tab, this.active_item_index(), cx)
1868                            }))
1869                            .on_drop(cx.listener(move |this, entry_id, cx| {
1870                                this.handle_project_entry_drop(entry_id, cx)
1871                            }))
1872                            .on_drop(cx.listener(move |this, paths, cx| {
1873                                this.handle_external_paths_drop(paths, cx)
1874                            }))
1875                            .map(|div| match self.drag_split_direction {
1876                                None => div.top_0().left_0().right_0().bottom_0(),
1877                                Some(SplitDirection::Up) => div.top_0().left_0().right_0().h_32(),
1878                                Some(SplitDirection::Down) => {
1879                                    div.left_0().bottom_0().right_0().h_32()
1880                                }
1881                                Some(SplitDirection::Left) => {
1882                                    div.top_0().left_0().bottom_0().w_32()
1883                                }
1884                                Some(SplitDirection::Right) => {
1885                                    div.top_0().bottom_0().right_0().w_32()
1886                                }
1887                            }),
1888                    )
1889            })
1890            .on_mouse_down(
1891                MouseButton::Navigate(NavigationDirection::Back),
1892                cx.listener(|pane, _, cx| {
1893                    if let Some(workspace) = pane.workspace.upgrade() {
1894                        let pane = cx.view().downgrade();
1895                        cx.window_context().defer(move |cx| {
1896                            workspace.update(cx, |workspace, cx| {
1897                                workspace.go_back(pane, cx).detach_and_log_err(cx)
1898                            })
1899                        })
1900                    }
1901                }),
1902            )
1903            .on_mouse_down(
1904                MouseButton::Navigate(NavigationDirection::Forward),
1905                cx.listener(|pane, _, cx| {
1906                    if let Some(workspace) = pane.workspace.upgrade() {
1907                        let pane = cx.view().downgrade();
1908                        cx.window_context().defer(move |cx| {
1909                            workspace.update(cx, |workspace, cx| {
1910                                workspace.go_forward(pane, cx).detach_and_log_err(cx)
1911                            })
1912                        })
1913                    }
1914                }),
1915            )
1916    }
1917}
1918
1919impl ItemNavHistory {
1920    pub fn push<D: 'static + Send + Any>(&mut self, data: Option<D>, cx: &mut WindowContext) {
1921        self.history.push(data, self.item.clone(), cx);
1922    }
1923
1924    pub fn pop_backward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
1925        self.history.pop(NavigationMode::GoingBack, cx)
1926    }
1927
1928    pub fn pop_forward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
1929        self.history.pop(NavigationMode::GoingForward, cx)
1930    }
1931}
1932
1933impl NavHistory {
1934    pub fn for_each_entry(
1935        &self,
1936        cx: &AppContext,
1937        mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
1938    ) {
1939        let borrowed_history = self.0.lock();
1940        borrowed_history
1941            .forward_stack
1942            .iter()
1943            .chain(borrowed_history.backward_stack.iter())
1944            .chain(borrowed_history.closed_stack.iter())
1945            .for_each(|entry| {
1946                if let Some(project_and_abs_path) =
1947                    borrowed_history.paths_by_item.get(&entry.item.id())
1948                {
1949                    f(entry, project_and_abs_path.clone());
1950                } else if let Some(item) = entry.item.upgrade() {
1951                    if let Some(path) = item.project_path(cx) {
1952                        f(entry, (path, None));
1953                    }
1954                }
1955            })
1956    }
1957
1958    pub fn set_mode(&mut self, mode: NavigationMode) {
1959        self.0.lock().mode = mode;
1960    }
1961
1962    pub fn mode(&self) -> NavigationMode {
1963        self.0.lock().mode
1964    }
1965
1966    pub fn disable(&mut self) {
1967        self.0.lock().mode = NavigationMode::Disabled;
1968    }
1969
1970    pub fn enable(&mut self) {
1971        self.0.lock().mode = NavigationMode::Normal;
1972    }
1973
1974    pub fn pop(&mut self, mode: NavigationMode, cx: &mut WindowContext) -> Option<NavigationEntry> {
1975        let mut state = self.0.lock();
1976        let entry = match mode {
1977            NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
1978                return None
1979            }
1980            NavigationMode::GoingBack => &mut state.backward_stack,
1981            NavigationMode::GoingForward => &mut state.forward_stack,
1982            NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
1983        }
1984        .pop_back();
1985        if entry.is_some() {
1986            state.did_update(cx);
1987        }
1988        entry
1989    }
1990
1991    pub fn push<D: 'static + Send + Any>(
1992        &mut self,
1993        data: Option<D>,
1994        item: Arc<dyn WeakItemHandle>,
1995        cx: &mut WindowContext,
1996    ) {
1997        let state = &mut *self.0.lock();
1998        match state.mode {
1999            NavigationMode::Disabled => {}
2000            NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
2001                if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2002                    state.backward_stack.pop_front();
2003                }
2004                state.backward_stack.push_back(NavigationEntry {
2005                    item,
2006                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2007                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2008                });
2009                state.forward_stack.clear();
2010            }
2011            NavigationMode::GoingBack => {
2012                if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2013                    state.forward_stack.pop_front();
2014                }
2015                state.forward_stack.push_back(NavigationEntry {
2016                    item,
2017                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2018                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2019                });
2020            }
2021            NavigationMode::GoingForward => {
2022                if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2023                    state.backward_stack.pop_front();
2024                }
2025                state.backward_stack.push_back(NavigationEntry {
2026                    item,
2027                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2028                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2029                });
2030            }
2031            NavigationMode::ClosingItem => {
2032                if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2033                    state.closed_stack.pop_front();
2034                }
2035                state.closed_stack.push_back(NavigationEntry {
2036                    item,
2037                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2038                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2039                });
2040            }
2041        }
2042        state.did_update(cx);
2043    }
2044
2045    pub fn remove_item(&mut self, item_id: EntityId) {
2046        let mut state = self.0.lock();
2047        state.paths_by_item.remove(&item_id);
2048        state
2049            .backward_stack
2050            .retain(|entry| entry.item.id() != item_id);
2051        state
2052            .forward_stack
2053            .retain(|entry| entry.item.id() != item_id);
2054        state
2055            .closed_stack
2056            .retain(|entry| entry.item.id() != item_id);
2057    }
2058
2059    pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option<PathBuf>)> {
2060        self.0.lock().paths_by_item.get(&item_id).cloned()
2061    }
2062}
2063
2064impl NavHistoryState {
2065    pub fn did_update(&self, cx: &mut WindowContext) {
2066        if let Some(pane) = self.pane.upgrade() {
2067            cx.defer(move |cx| {
2068                pane.update(cx, |pane, cx| pane.history_updated(cx));
2069            });
2070        }
2071    }
2072}
2073
2074fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
2075    let path = buffer_path
2076        .as_ref()
2077        .and_then(|p| {
2078            p.path
2079                .to_str()
2080                .and_then(|s| if s == "" { None } else { Some(s) })
2081        })
2082        .unwrap_or("This buffer");
2083    let path = truncate_and_remove_front(path, 80);
2084    format!("{path} contains unsaved edits. Do you want to save it?")
2085}
2086
2087pub fn tab_details(items: &Vec<Box<dyn ItemHandle>>, cx: &AppContext) -> Vec<usize> {
2088    let mut tab_details = items.iter().map(|_| 0).collect::<Vec<_>>();
2089    let mut tab_descriptions = HashMap::default();
2090    let mut done = false;
2091    while !done {
2092        done = true;
2093
2094        // Store item indices by their tab description.
2095        for (ix, (item, detail)) in items.iter().zip(&tab_details).enumerate() {
2096            if let Some(description) = item.tab_description(*detail, cx) {
2097                if *detail == 0
2098                    || Some(&description) != item.tab_description(detail - 1, cx).as_ref()
2099                {
2100                    tab_descriptions
2101                        .entry(description)
2102                        .or_insert(Vec::new())
2103                        .push(ix);
2104                }
2105            }
2106        }
2107
2108        // If two or more items have the same tab description, increase their level
2109        // of detail and try again.
2110        for (_, item_ixs) in tab_descriptions.drain() {
2111            if item_ixs.len() > 1 {
2112                done = false;
2113                for ix in item_ixs {
2114                    tab_details[ix] += 1;
2115                }
2116            }
2117        }
2118    }
2119
2120    tab_details
2121}
2122
2123pub fn render_item_indicator(item: Box<dyn ItemHandle>, cx: &WindowContext) -> Option<Indicator> {
2124    maybe!({
2125        let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
2126            (true, _) => Color::Warning,
2127            (_, true) => Color::Accent,
2128            (false, false) => return None,
2129        };
2130
2131        Some(Indicator::dot().color(indicator_color))
2132    })
2133}
2134
2135#[cfg(test)]
2136mod tests {
2137    use super::*;
2138    use crate::item::test::{TestItem, TestProjectItem};
2139    use gpui::{TestAppContext, VisualTestContext};
2140    use project::FakeFs;
2141    use settings::SettingsStore;
2142    use theme::LoadThemes;
2143
2144    #[gpui::test]
2145    async fn test_remove_active_empty(cx: &mut TestAppContext) {
2146        init_test(cx);
2147        let fs = FakeFs::new(cx.executor());
2148
2149        let project = Project::test(fs, None, cx).await;
2150        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2151        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2152
2153        pane.update(cx, |pane, cx| {
2154            assert!(pane
2155                .close_active_item(&CloseActiveItem { save_intent: None }, cx)
2156                .is_none())
2157        });
2158    }
2159
2160    #[gpui::test]
2161    async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
2162        init_test(cx);
2163        let fs = FakeFs::new(cx.executor());
2164
2165        let project = Project::test(fs, None, cx).await;
2166        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2167        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2168
2169        // 1. Add with a destination index
2170        //   a. Add before the active item
2171        set_labeled_items(&pane, ["A", "B*", "C"], cx);
2172        pane.update(cx, |pane, cx| {
2173            pane.add_item(
2174                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2175                false,
2176                false,
2177                Some(0),
2178                cx,
2179            );
2180        });
2181        assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
2182
2183        //   b. Add after the active item
2184        set_labeled_items(&pane, ["A", "B*", "C"], cx);
2185        pane.update(cx, |pane, cx| {
2186            pane.add_item(
2187                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2188                false,
2189                false,
2190                Some(2),
2191                cx,
2192            );
2193        });
2194        assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
2195
2196        //   c. Add at the end of the item list (including off the length)
2197        set_labeled_items(&pane, ["A", "B*", "C"], cx);
2198        pane.update(cx, |pane, cx| {
2199            pane.add_item(
2200                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2201                false,
2202                false,
2203                Some(5),
2204                cx,
2205            );
2206        });
2207        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2208
2209        // 2. Add without a destination index
2210        //   a. Add with active item at the start of the item list
2211        set_labeled_items(&pane, ["A*", "B", "C"], cx);
2212        pane.update(cx, |pane, cx| {
2213            pane.add_item(
2214                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2215                false,
2216                false,
2217                None,
2218                cx,
2219            );
2220        });
2221        set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
2222
2223        //   b. Add with active item at the end of the item list
2224        set_labeled_items(&pane, ["A", "B", "C*"], cx);
2225        pane.update(cx, |pane, cx| {
2226            pane.add_item(
2227                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2228                false,
2229                false,
2230                None,
2231                cx,
2232            );
2233        });
2234        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2235    }
2236
2237    #[gpui::test]
2238    async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
2239        init_test(cx);
2240        let fs = FakeFs::new(cx.executor());
2241
2242        let project = Project::test(fs, None, cx).await;
2243        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2244        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2245
2246        // 1. Add with a destination index
2247        //   1a. Add before the active item
2248        let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
2249        pane.update(cx, |pane, cx| {
2250            pane.add_item(d, false, false, Some(0), cx);
2251        });
2252        assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
2253
2254        //   1b. Add after the active item
2255        let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
2256        pane.update(cx, |pane, cx| {
2257            pane.add_item(d, false, false, Some(2), cx);
2258        });
2259        assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
2260
2261        //   1c. Add at the end of the item list (including off the length)
2262        let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
2263        pane.update(cx, |pane, cx| {
2264            pane.add_item(a, false, false, Some(5), cx);
2265        });
2266        assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
2267
2268        //   1d. Add same item to active index
2269        let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
2270        pane.update(cx, |pane, cx| {
2271            pane.add_item(b, false, false, Some(1), cx);
2272        });
2273        assert_item_labels(&pane, ["A", "B*", "C"], cx);
2274
2275        //   1e. Add item to index after same item in last position
2276        let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
2277        pane.update(cx, |pane, cx| {
2278            pane.add_item(c, false, false, Some(2), cx);
2279        });
2280        assert_item_labels(&pane, ["A", "B", "C*"], cx);
2281
2282        // 2. Add without a destination index
2283        //   2a. Add with active item at the start of the item list
2284        let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
2285        pane.update(cx, |pane, cx| {
2286            pane.add_item(d, false, false, None, cx);
2287        });
2288        assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
2289
2290        //   2b. Add with active item at the end of the item list
2291        let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
2292        pane.update(cx, |pane, cx| {
2293            pane.add_item(a, false, false, None, cx);
2294        });
2295        assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
2296
2297        //   2c. Add active item to active item at end of list
2298        let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
2299        pane.update(cx, |pane, cx| {
2300            pane.add_item(c, false, false, None, cx);
2301        });
2302        assert_item_labels(&pane, ["A", "B", "C*"], cx);
2303
2304        //   2d. Add active item to active item at start of list
2305        let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
2306        pane.update(cx, |pane, cx| {
2307            pane.add_item(a, false, false, None, cx);
2308        });
2309        assert_item_labels(&pane, ["A*", "B", "C"], cx);
2310    }
2311
2312    #[gpui::test]
2313    async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
2314        init_test(cx);
2315        let fs = FakeFs::new(cx.executor());
2316
2317        let project = Project::test(fs, None, cx).await;
2318        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2319        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2320
2321        // singleton view
2322        pane.update(cx, |pane, cx| {
2323            pane.add_item(
2324                Box::new(cx.new_view(|cx| {
2325                    TestItem::new(cx)
2326                        .with_singleton(true)
2327                        .with_label("buffer 1")
2328                        .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
2329                })),
2330                false,
2331                false,
2332                None,
2333                cx,
2334            );
2335        });
2336        assert_item_labels(&pane, ["buffer 1*"], cx);
2337
2338        // new singleton view with the same project entry
2339        pane.update(cx, |pane, cx| {
2340            pane.add_item(
2341                Box::new(cx.new_view(|cx| {
2342                    TestItem::new(cx)
2343                        .with_singleton(true)
2344                        .with_label("buffer 1")
2345                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
2346                })),
2347                false,
2348                false,
2349                None,
2350                cx,
2351            );
2352        });
2353        assert_item_labels(&pane, ["buffer 1*"], cx);
2354
2355        // new singleton view with different project entry
2356        pane.update(cx, |pane, cx| {
2357            pane.add_item(
2358                Box::new(cx.new_view(|cx| {
2359                    TestItem::new(cx)
2360                        .with_singleton(true)
2361                        .with_label("buffer 2")
2362                        .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
2363                })),
2364                false,
2365                false,
2366                None,
2367                cx,
2368            );
2369        });
2370        assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
2371
2372        // new multibuffer view with the same project entry
2373        pane.update(cx, |pane, cx| {
2374            pane.add_item(
2375                Box::new(cx.new_view(|cx| {
2376                    TestItem::new(cx)
2377                        .with_singleton(false)
2378                        .with_label("multibuffer 1")
2379                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
2380                })),
2381                false,
2382                false,
2383                None,
2384                cx,
2385            );
2386        });
2387        assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
2388
2389        // another multibuffer view with the same project entry
2390        pane.update(cx, |pane, cx| {
2391            pane.add_item(
2392                Box::new(cx.new_view(|cx| {
2393                    TestItem::new(cx)
2394                        .with_singleton(false)
2395                        .with_label("multibuffer 1b")
2396                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
2397                })),
2398                false,
2399                false,
2400                None,
2401                cx,
2402            );
2403        });
2404        assert_item_labels(
2405            &pane,
2406            ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
2407            cx,
2408        );
2409    }
2410
2411    #[gpui::test]
2412    async fn test_remove_item_ordering(cx: &mut TestAppContext) {
2413        init_test(cx);
2414        let fs = FakeFs::new(cx.executor());
2415
2416        let project = Project::test(fs, None, cx).await;
2417        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2418        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2419
2420        add_labeled_item(&pane, "A", false, cx);
2421        add_labeled_item(&pane, "B", false, cx);
2422        add_labeled_item(&pane, "C", false, cx);
2423        add_labeled_item(&pane, "D", false, cx);
2424        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2425
2426        pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
2427        add_labeled_item(&pane, "1", false, cx);
2428        assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
2429
2430        pane.update(cx, |pane, cx| {
2431            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
2432        })
2433        .unwrap()
2434        .await
2435        .unwrap();
2436        assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
2437
2438        pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
2439        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2440
2441        pane.update(cx, |pane, cx| {
2442            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
2443        })
2444        .unwrap()
2445        .await
2446        .unwrap();
2447        assert_item_labels(&pane, ["A", "B*", "C"], 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", "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*"], cx);
2464    }
2465
2466    #[gpui::test]
2467    async fn test_close_inactive_items(cx: &mut TestAppContext) {
2468        init_test(cx);
2469        let fs = FakeFs::new(cx.executor());
2470
2471        let project = Project::test(fs, None, cx).await;
2472        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2473        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2474
2475        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
2476
2477        pane.update(cx, |pane, cx| {
2478            pane.close_inactive_items(&CloseInactiveItems { save_intent: None }, cx)
2479        })
2480        .unwrap()
2481        .await
2482        .unwrap();
2483        assert_item_labels(&pane, ["C*"], cx);
2484    }
2485
2486    #[gpui::test]
2487    async fn test_close_clean_items(cx: &mut TestAppContext) {
2488        init_test(cx);
2489        let fs = FakeFs::new(cx.executor());
2490
2491        let project = Project::test(fs, None, cx).await;
2492        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2493        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2494
2495        add_labeled_item(&pane, "A", true, cx);
2496        add_labeled_item(&pane, "B", false, cx);
2497        add_labeled_item(&pane, "C", true, cx);
2498        add_labeled_item(&pane, "D", false, cx);
2499        add_labeled_item(&pane, "E", false, cx);
2500        assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
2501
2502        pane.update(cx, |pane, cx| pane.close_clean_items(&CloseCleanItems, cx))
2503            .unwrap()
2504            .await
2505            .unwrap();
2506        assert_item_labels(&pane, ["A^", "C*^"], cx);
2507    }
2508
2509    #[gpui::test]
2510    async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
2511        init_test(cx);
2512        let fs = FakeFs::new(cx.executor());
2513
2514        let project = Project::test(fs, None, cx).await;
2515        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2516        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2517
2518        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
2519
2520        pane.update(cx, |pane, cx| {
2521            pane.close_items_to_the_left(&CloseItemsToTheLeft, cx)
2522        })
2523        .unwrap()
2524        .await
2525        .unwrap();
2526        assert_item_labels(&pane, ["C*", "D", "E"], cx);
2527    }
2528
2529    #[gpui::test]
2530    async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
2531        init_test(cx);
2532        let fs = FakeFs::new(cx.executor());
2533
2534        let project = Project::test(fs, None, cx).await;
2535        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2536        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2537
2538        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
2539
2540        pane.update(cx, |pane, cx| {
2541            pane.close_items_to_the_right(&CloseItemsToTheRight, cx)
2542        })
2543        .unwrap()
2544        .await
2545        .unwrap();
2546        assert_item_labels(&pane, ["A", "B", "C*"], cx);
2547    }
2548
2549    #[gpui::test]
2550    async fn test_close_all_items(cx: &mut TestAppContext) {
2551        init_test(cx);
2552        let fs = FakeFs::new(cx.executor());
2553
2554        let project = Project::test(fs, None, cx).await;
2555        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2556        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2557
2558        add_labeled_item(&pane, "A", false, cx);
2559        add_labeled_item(&pane, "B", false, cx);
2560        add_labeled_item(&pane, "C", false, cx);
2561        assert_item_labels(&pane, ["A", "B", "C*"], cx);
2562
2563        pane.update(cx, |pane, cx| {
2564            pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
2565        })
2566        .unwrap()
2567        .await
2568        .unwrap();
2569        assert_item_labels(&pane, [], cx);
2570
2571        add_labeled_item(&pane, "A", true, cx);
2572        add_labeled_item(&pane, "B", true, cx);
2573        add_labeled_item(&pane, "C", true, cx);
2574        assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
2575
2576        let save = pane
2577            .update(cx, |pane, cx| {
2578                pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
2579            })
2580            .unwrap();
2581
2582        cx.executor().run_until_parked();
2583        cx.simulate_prompt_answer(2);
2584        save.await.unwrap();
2585        assert_item_labels(&pane, [], cx);
2586    }
2587
2588    fn init_test(cx: &mut TestAppContext) {
2589        cx.update(|cx| {
2590            let settings_store = SettingsStore::test(cx);
2591            cx.set_global(settings_store);
2592            theme::init(LoadThemes::JustBase, cx);
2593            crate::init_settings(cx);
2594            Project::init_settings(cx);
2595        });
2596    }
2597
2598    fn add_labeled_item(
2599        pane: &View<Pane>,
2600        label: &str,
2601        is_dirty: bool,
2602        cx: &mut VisualTestContext,
2603    ) -> Box<View<TestItem>> {
2604        pane.update(cx, |pane, cx| {
2605            let labeled_item = Box::new(
2606                cx.new_view(|cx| TestItem::new(cx).with_label(label).with_dirty(is_dirty)),
2607            );
2608            pane.add_item(labeled_item.clone(), false, false, None, cx);
2609            labeled_item
2610        })
2611    }
2612
2613    fn set_labeled_items<const COUNT: usize>(
2614        pane: &View<Pane>,
2615        labels: [&str; COUNT],
2616        cx: &mut VisualTestContext,
2617    ) -> [Box<View<TestItem>>; COUNT] {
2618        pane.update(cx, |pane, cx| {
2619            pane.items.clear();
2620            let mut active_item_index = 0;
2621
2622            let mut index = 0;
2623            let items = labels.map(|mut label| {
2624                if label.ends_with('*') {
2625                    label = label.trim_end_matches('*');
2626                    active_item_index = index;
2627                }
2628
2629                let labeled_item = Box::new(cx.new_view(|cx| TestItem::new(cx).with_label(label)));
2630                pane.add_item(labeled_item.clone(), false, false, None, cx);
2631                index += 1;
2632                labeled_item
2633            });
2634
2635            pane.activate_item(active_item_index, false, false, cx);
2636
2637            items
2638        })
2639    }
2640
2641    // Assert the item label, with the active item label suffixed with a '*'
2642    fn assert_item_labels<const COUNT: usize>(
2643        pane: &View<Pane>,
2644        expected_states: [&str; COUNT],
2645        cx: &mut VisualTestContext,
2646    ) {
2647        pane.update(cx, |pane, cx| {
2648            let actual_states = pane
2649                .items
2650                .iter()
2651                .enumerate()
2652                .map(|(ix, item)| {
2653                    let mut state = item
2654                        .to_any()
2655                        .downcast::<TestItem>()
2656                        .unwrap()
2657                        .read(cx)
2658                        .label
2659                        .clone();
2660                    if ix == pane.active_item_index {
2661                        state.push('*');
2662                    }
2663                    if item.is_dirty(cx) {
2664                        state.push('^');
2665                    }
2666                    state
2667                })
2668                .collect::<Vec<_>>();
2669
2670            assert_eq!(
2671                actual_states, expected_states,
2672                "pane items do not match expectation"
2673            );
2674        })
2675    }
2676}
2677
2678impl Render for DraggedTab {
2679    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2680        let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
2681        let label = self.item.tab_content(Some(self.detail), false, cx);
2682        Tab::new("")
2683            .selected(self.is_active)
2684            .child(label)
2685            .render(cx)
2686            .font(ui_font)
2687    }
2688}