pane.rs

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