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().absolute().bottom_0().right_0().size_0().child(
1566            deferred(
1567                anchored()
1568                    .anchor(AnchorCorner::TopRight)
1569                    .child(menu.clone()),
1570            )
1571            .with_priority(1),
1572        )
1573    }
1574
1575    pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
1576        self.zoomed = zoomed;
1577        cx.notify();
1578    }
1579
1580    pub fn is_zoomed(&self) -> bool {
1581        self.zoomed
1582    }
1583
1584    fn handle_drag_move<T>(&mut self, event: &DragMoveEvent<T>, cx: &mut ViewContext<Self>) {
1585        if !self.can_split {
1586            return;
1587        }
1588
1589        let edge_width = cx.rem_size() * 8;
1590        let cursor = event.event.position;
1591        let direction = if cursor.x < event.bounds.left() + edge_width {
1592            Some(SplitDirection::Left)
1593        } else if cursor.x > event.bounds.right() - edge_width {
1594            Some(SplitDirection::Right)
1595        } else if cursor.y < event.bounds.top() + edge_width {
1596            Some(SplitDirection::Up)
1597        } else if cursor.y > event.bounds.bottom() - edge_width {
1598            Some(SplitDirection::Down)
1599        } else {
1600            None
1601        };
1602
1603        if direction != self.drag_split_direction {
1604            self.drag_split_direction = direction;
1605        }
1606    }
1607
1608    fn handle_tab_drop(
1609        &mut self,
1610        dragged_tab: &DraggedTab,
1611        ix: usize,
1612        cx: &mut ViewContext<'_, Self>,
1613    ) {
1614        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
1615            if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_tab, cx) {
1616                return;
1617            }
1618        }
1619        let mut to_pane = cx.view().clone();
1620        let split_direction = self.drag_split_direction;
1621        let item_id = dragged_tab.item.item_id();
1622        let from_pane = dragged_tab.pane.clone();
1623        self.workspace
1624            .update(cx, |_, cx| {
1625                cx.defer(move |workspace, cx| {
1626                    if let Some(split_direction) = split_direction {
1627                        to_pane = workspace.split_pane(to_pane, split_direction, cx);
1628                    }
1629                    workspace.move_item(from_pane, to_pane, item_id, ix, cx);
1630                });
1631            })
1632            .log_err();
1633    }
1634
1635    fn handle_project_entry_drop(
1636        &mut self,
1637        project_entry_id: &ProjectEntryId,
1638        cx: &mut ViewContext<'_, Self>,
1639    ) {
1640        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
1641            if let ControlFlow::Break(()) = custom_drop_handle(self, project_entry_id, cx) {
1642                return;
1643            }
1644        }
1645        let mut to_pane = cx.view().clone();
1646        let split_direction = self.drag_split_direction;
1647        let project_entry_id = *project_entry_id;
1648        self.workspace
1649            .update(cx, |_, cx| {
1650                cx.defer(move |workspace, cx| {
1651                    if let Some(path) = workspace
1652                        .project()
1653                        .read(cx)
1654                        .path_for_entry(project_entry_id, cx)
1655                    {
1656                        if let Some(split_direction) = split_direction {
1657                            to_pane = workspace.split_pane(to_pane, split_direction, cx);
1658                        }
1659                        workspace
1660                            .open_path(path, Some(to_pane.downgrade()), true, cx)
1661                            .detach_and_log_err(cx);
1662                    }
1663                });
1664            })
1665            .log_err();
1666    }
1667
1668    fn handle_external_paths_drop(
1669        &mut self,
1670        paths: &ExternalPaths,
1671        cx: &mut ViewContext<'_, Self>,
1672    ) {
1673        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
1674            if let ControlFlow::Break(()) = custom_drop_handle(self, paths, cx) {
1675                return;
1676            }
1677        }
1678        let mut to_pane = cx.view().clone();
1679        let mut split_direction = self.drag_split_direction;
1680        let paths = paths.paths().to_vec();
1681        self.workspace
1682            .update(cx, |workspace, cx| {
1683                let fs = Arc::clone(workspace.project().read(cx).fs());
1684                cx.spawn(|workspace, mut cx| async move {
1685                    let mut is_file_checks = FuturesUnordered::new();
1686                    for path in &paths {
1687                        is_file_checks.push(fs.is_file(path))
1688                    }
1689                    let mut has_files_to_open = false;
1690                    while let Some(is_file) = is_file_checks.next().await {
1691                        if is_file {
1692                            has_files_to_open = true;
1693                            break;
1694                        }
1695                    }
1696                    drop(is_file_checks);
1697                    if !has_files_to_open {
1698                        split_direction = None;
1699                    }
1700
1701                    if let Some(open_task) = workspace
1702                        .update(&mut cx, |workspace, cx| {
1703                            if let Some(split_direction) = split_direction {
1704                                to_pane = workspace.split_pane(to_pane, split_direction, cx);
1705                            }
1706                            workspace.open_paths(
1707                                paths,
1708                                OpenVisible::OnlyDirectories,
1709                                Some(to_pane.downgrade()),
1710                                cx,
1711                            )
1712                        })
1713                        .ok()
1714                    {
1715                        let _opened_items: Vec<_> = open_task.await;
1716                    }
1717                })
1718                .detach();
1719            })
1720            .log_err();
1721    }
1722
1723    pub fn display_nav_history_buttons(&mut self, display: bool) {
1724        self.display_nav_history_buttons = display;
1725    }
1726}
1727
1728impl FocusableView for Pane {
1729    fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
1730        self.focus_handle.clone()
1731    }
1732}
1733
1734impl Render for Pane {
1735    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
1736        v_flex()
1737            .key_context("Pane")
1738            .track_focus(&self.focus_handle)
1739            .size_full()
1740            .flex_none()
1741            .overflow_hidden()
1742            .on_action(cx.listener(|pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx)))
1743            .on_action(cx.listener(|pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx)))
1744            .on_action(
1745                cx.listener(|pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx)),
1746            )
1747            .on_action(cx.listener(|pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx)))
1748            .on_action(cx.listener(|pane, _: &GoBack, cx| pane.navigate_backward(cx)))
1749            .on_action(cx.listener(|pane, _: &GoForward, cx| pane.navigate_forward(cx)))
1750            .on_action(cx.listener(Pane::toggle_zoom))
1751            .on_action(cx.listener(|pane: &mut Pane, action: &ActivateItem, cx| {
1752                pane.activate_item(action.0, true, true, cx);
1753            }))
1754            .on_action(cx.listener(|pane: &mut Pane, _: &ActivateLastItem, cx| {
1755                pane.activate_item(pane.items.len() - 1, true, true, cx);
1756            }))
1757            .on_action(cx.listener(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
1758                pane.activate_prev_item(true, cx);
1759            }))
1760            .on_action(cx.listener(|pane: &mut Pane, _: &ActivateNextItem, cx| {
1761                pane.activate_next_item(true, cx);
1762            }))
1763            .on_action(
1764                cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
1765                    if let Some(task) = pane.close_active_item(action, cx) {
1766                        task.detach_and_log_err(cx)
1767                    }
1768                }),
1769            )
1770            .on_action(
1771                cx.listener(|pane: &mut Self, action: &CloseInactiveItems, cx| {
1772                    if let Some(task) = pane.close_inactive_items(action, cx) {
1773                        task.detach_and_log_err(cx)
1774                    }
1775                }),
1776            )
1777            .on_action(
1778                cx.listener(|pane: &mut Self, action: &CloseCleanItems, cx| {
1779                    if let Some(task) = pane.close_clean_items(action, cx) {
1780                        task.detach_and_log_err(cx)
1781                    }
1782                }),
1783            )
1784            .on_action(
1785                cx.listener(|pane: &mut Self, action: &CloseItemsToTheLeft, cx| {
1786                    if let Some(task) = pane.close_items_to_the_left(action, cx) {
1787                        task.detach_and_log_err(cx)
1788                    }
1789                }),
1790            )
1791            .on_action(
1792                cx.listener(|pane: &mut Self, action: &CloseItemsToTheRight, cx| {
1793                    if let Some(task) = pane.close_items_to_the_right(action, cx) {
1794                        task.detach_and_log_err(cx)
1795                    }
1796                }),
1797            )
1798            .on_action(cx.listener(|pane: &mut Self, action: &CloseAllItems, cx| {
1799                if let Some(task) = pane.close_all_items(action, cx) {
1800                    task.detach_and_log_err(cx)
1801                }
1802            }))
1803            .on_action(
1804                cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
1805                    if let Some(task) = pane.close_active_item(action, cx) {
1806                        task.detach_and_log_err(cx)
1807                    }
1808                }),
1809            )
1810            .on_action(
1811                cx.listener(|pane: &mut Self, action: &RevealInProjectPanel, cx| {
1812                    let entry_id = action
1813                        .entry_id
1814                        .map(ProjectEntryId::from_proto)
1815                        .or_else(|| pane.active_item()?.project_entry_ids(cx).first().copied());
1816                    if let Some(entry_id) = entry_id {
1817                        pane.project.update(cx, |_, cx| {
1818                            cx.emit(project::Event::RevealInProjectPanel(entry_id))
1819                        });
1820                    }
1821                }),
1822            )
1823            .when(self.active_item().is_some(), |pane| {
1824                pane.child(self.render_tab_bar(cx))
1825            })
1826            .child({
1827                let has_worktrees = self.project.read(cx).worktrees().next().is_some();
1828                // main content
1829                div()
1830                    .flex_1()
1831                    .relative()
1832                    .group("")
1833                    .on_drag_move::<DraggedTab>(cx.listener(Self::handle_drag_move))
1834                    .on_drag_move::<ProjectEntryId>(cx.listener(Self::handle_drag_move))
1835                    .on_drag_move::<ExternalPaths>(cx.listener(Self::handle_drag_move))
1836                    .map(|div| {
1837                        if let Some(item) = self.active_item() {
1838                            div.v_flex()
1839                                .child(self.toolbar.clone())
1840                                .child(item.to_any())
1841                        } else {
1842                            let placeholder = div.h_flex().size_full().justify_center();
1843                            if has_worktrees {
1844                                placeholder
1845                            } else {
1846                                placeholder.child(
1847                                    Label::new("Open a file or project to get started.")
1848                                        .color(Color::Muted),
1849                                )
1850                            }
1851                        }
1852                    })
1853                    .child(
1854                        // drag target
1855                        div()
1856                            .invisible()
1857                            .absolute()
1858                            .bg(theme::color_alpha(
1859                                cx.theme().colors().drop_target_background,
1860                                0.75,
1861                            ))
1862                            .group_drag_over::<DraggedTab>("", |style| style.visible())
1863                            .group_drag_over::<ProjectEntryId>("", |style| style.visible())
1864                            .group_drag_over::<ExternalPaths>("", |style| style.visible())
1865                            .when_some(self.can_drop_predicate.clone(), |this, p| {
1866                                this.can_drop(move |a, cx| p(a, cx))
1867                            })
1868                            .on_drop(cx.listener(move |this, dragged_tab, cx| {
1869                                this.handle_tab_drop(dragged_tab, this.active_item_index(), cx)
1870                            }))
1871                            .on_drop(cx.listener(move |this, entry_id, cx| {
1872                                this.handle_project_entry_drop(entry_id, cx)
1873                            }))
1874                            .on_drop(cx.listener(move |this, paths, cx| {
1875                                this.handle_external_paths_drop(paths, cx)
1876                            }))
1877                            .map(|div| match self.drag_split_direction {
1878                                None => div.top_0().left_0().right_0().bottom_0(),
1879                                Some(SplitDirection::Up) => div.top_0().left_0().right_0().h_32(),
1880                                Some(SplitDirection::Down) => {
1881                                    div.left_0().bottom_0().right_0().h_32()
1882                                }
1883                                Some(SplitDirection::Left) => {
1884                                    div.top_0().left_0().bottom_0().w_32()
1885                                }
1886                                Some(SplitDirection::Right) => {
1887                                    div.top_0().bottom_0().right_0().w_32()
1888                                }
1889                            }),
1890                    )
1891            })
1892            .on_mouse_down(
1893                MouseButton::Navigate(NavigationDirection::Back),
1894                cx.listener(|pane, _, cx| {
1895                    if let Some(workspace) = pane.workspace.upgrade() {
1896                        let pane = cx.view().downgrade();
1897                        cx.window_context().defer(move |cx| {
1898                            workspace.update(cx, |workspace, cx| {
1899                                workspace.go_back(pane, cx).detach_and_log_err(cx)
1900                            })
1901                        })
1902                    }
1903                }),
1904            )
1905            .on_mouse_down(
1906                MouseButton::Navigate(NavigationDirection::Forward),
1907                cx.listener(|pane, _, cx| {
1908                    if let Some(workspace) = pane.workspace.upgrade() {
1909                        let pane = cx.view().downgrade();
1910                        cx.window_context().defer(move |cx| {
1911                            workspace.update(cx, |workspace, cx| {
1912                                workspace.go_forward(pane, cx).detach_and_log_err(cx)
1913                            })
1914                        })
1915                    }
1916                }),
1917            )
1918    }
1919}
1920
1921impl ItemNavHistory {
1922    pub fn push<D: 'static + Send + Any>(&mut self, data: Option<D>, cx: &mut WindowContext) {
1923        self.history.push(data, self.item.clone(), cx);
1924    }
1925
1926    pub fn pop_backward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
1927        self.history.pop(NavigationMode::GoingBack, cx)
1928    }
1929
1930    pub fn pop_forward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
1931        self.history.pop(NavigationMode::GoingForward, cx)
1932    }
1933}
1934
1935impl NavHistory {
1936    pub fn for_each_entry(
1937        &self,
1938        cx: &AppContext,
1939        mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
1940    ) {
1941        let borrowed_history = self.0.lock();
1942        borrowed_history
1943            .forward_stack
1944            .iter()
1945            .chain(borrowed_history.backward_stack.iter())
1946            .chain(borrowed_history.closed_stack.iter())
1947            .for_each(|entry| {
1948                if let Some(project_and_abs_path) =
1949                    borrowed_history.paths_by_item.get(&entry.item.id())
1950                {
1951                    f(entry, project_and_abs_path.clone());
1952                } else if let Some(item) = entry.item.upgrade() {
1953                    if let Some(path) = item.project_path(cx) {
1954                        f(entry, (path, None));
1955                    }
1956                }
1957            })
1958    }
1959
1960    pub fn set_mode(&mut self, mode: NavigationMode) {
1961        self.0.lock().mode = mode;
1962    }
1963
1964    pub fn mode(&self) -> NavigationMode {
1965        self.0.lock().mode
1966    }
1967
1968    pub fn disable(&mut self) {
1969        self.0.lock().mode = NavigationMode::Disabled;
1970    }
1971
1972    pub fn enable(&mut self) {
1973        self.0.lock().mode = NavigationMode::Normal;
1974    }
1975
1976    pub fn pop(&mut self, mode: NavigationMode, cx: &mut WindowContext) -> Option<NavigationEntry> {
1977        let mut state = self.0.lock();
1978        let entry = match mode {
1979            NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
1980                return None
1981            }
1982            NavigationMode::GoingBack => &mut state.backward_stack,
1983            NavigationMode::GoingForward => &mut state.forward_stack,
1984            NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
1985        }
1986        .pop_back();
1987        if entry.is_some() {
1988            state.did_update(cx);
1989        }
1990        entry
1991    }
1992
1993    pub fn push<D: 'static + Send + Any>(
1994        &mut self,
1995        data: Option<D>,
1996        item: Arc<dyn WeakItemHandle>,
1997        cx: &mut WindowContext,
1998    ) {
1999        let state = &mut *self.0.lock();
2000        match state.mode {
2001            NavigationMode::Disabled => {}
2002            NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
2003                if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2004                    state.backward_stack.pop_front();
2005                }
2006                state.backward_stack.push_back(NavigationEntry {
2007                    item,
2008                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2009                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2010                });
2011                state.forward_stack.clear();
2012            }
2013            NavigationMode::GoingBack => {
2014                if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2015                    state.forward_stack.pop_front();
2016                }
2017                state.forward_stack.push_back(NavigationEntry {
2018                    item,
2019                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2020                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2021                });
2022            }
2023            NavigationMode::GoingForward => {
2024                if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2025                    state.backward_stack.pop_front();
2026                }
2027                state.backward_stack.push_back(NavigationEntry {
2028                    item,
2029                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2030                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2031                });
2032            }
2033            NavigationMode::ClosingItem => {
2034                if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2035                    state.closed_stack.pop_front();
2036                }
2037                state.closed_stack.push_back(NavigationEntry {
2038                    item,
2039                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2040                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2041                });
2042            }
2043        }
2044        state.did_update(cx);
2045    }
2046
2047    pub fn remove_item(&mut self, item_id: EntityId) {
2048        let mut state = self.0.lock();
2049        state.paths_by_item.remove(&item_id);
2050        state
2051            .backward_stack
2052            .retain(|entry| entry.item.id() != item_id);
2053        state
2054            .forward_stack
2055            .retain(|entry| entry.item.id() != item_id);
2056        state
2057            .closed_stack
2058            .retain(|entry| entry.item.id() != item_id);
2059    }
2060
2061    pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option<PathBuf>)> {
2062        self.0.lock().paths_by_item.get(&item_id).cloned()
2063    }
2064}
2065
2066impl NavHistoryState {
2067    pub fn did_update(&self, cx: &mut WindowContext) {
2068        if let Some(pane) = self.pane.upgrade() {
2069            cx.defer(move |cx| {
2070                pane.update(cx, |pane, cx| pane.history_updated(cx));
2071            });
2072        }
2073    }
2074}
2075
2076fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
2077    let path = buffer_path
2078        .as_ref()
2079        .and_then(|p| {
2080            p.path
2081                .to_str()
2082                .and_then(|s| if s == "" { None } else { Some(s) })
2083        })
2084        .unwrap_or("This buffer");
2085    let path = truncate_and_remove_front(path, 80);
2086    format!("{path} contains unsaved edits. Do you want to save it?")
2087}
2088
2089pub fn tab_details(items: &Vec<Box<dyn ItemHandle>>, cx: &AppContext) -> Vec<usize> {
2090    let mut tab_details = items.iter().map(|_| 0).collect::<Vec<_>>();
2091    let mut tab_descriptions = HashMap::default();
2092    let mut done = false;
2093    while !done {
2094        done = true;
2095
2096        // Store item indices by their tab description.
2097        for (ix, (item, detail)) in items.iter().zip(&tab_details).enumerate() {
2098            if let Some(description) = item.tab_description(*detail, cx) {
2099                if *detail == 0
2100                    || Some(&description) != item.tab_description(detail - 1, cx).as_ref()
2101                {
2102                    tab_descriptions
2103                        .entry(description)
2104                        .or_insert(Vec::new())
2105                        .push(ix);
2106                }
2107            }
2108        }
2109
2110        // If two or more items have the same tab description, increase their level
2111        // of detail and try again.
2112        for (_, item_ixs) in tab_descriptions.drain() {
2113            if item_ixs.len() > 1 {
2114                done = false;
2115                for ix in item_ixs {
2116                    tab_details[ix] += 1;
2117                }
2118            }
2119        }
2120    }
2121
2122    tab_details
2123}
2124
2125pub fn render_item_indicator(item: Box<dyn ItemHandle>, cx: &WindowContext) -> Option<Indicator> {
2126    maybe!({
2127        let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
2128            (true, _) => Color::Warning,
2129            (_, true) => Color::Accent,
2130            (false, false) => return None,
2131        };
2132
2133        Some(Indicator::dot().color(indicator_color))
2134    })
2135}
2136
2137#[cfg(test)]
2138mod tests {
2139    use super::*;
2140    use crate::item::test::{TestItem, TestProjectItem};
2141    use gpui::{TestAppContext, VisualTestContext};
2142    use project::FakeFs;
2143    use settings::SettingsStore;
2144    use theme::LoadThemes;
2145
2146    #[gpui::test]
2147    async fn test_remove_active_empty(cx: &mut TestAppContext) {
2148        init_test(cx);
2149        let fs = FakeFs::new(cx.executor());
2150
2151        let project = Project::test(fs, None, cx).await;
2152        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2153        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2154
2155        pane.update(cx, |pane, cx| {
2156            assert!(pane
2157                .close_active_item(&CloseActiveItem { save_intent: None }, cx)
2158                .is_none())
2159        });
2160    }
2161
2162    #[gpui::test]
2163    async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
2164        init_test(cx);
2165        let fs = FakeFs::new(cx.executor());
2166
2167        let project = Project::test(fs, None, cx).await;
2168        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2169        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2170
2171        // 1. Add with a destination index
2172        //   a. Add before the active item
2173        set_labeled_items(&pane, ["A", "B*", "C"], cx);
2174        pane.update(cx, |pane, cx| {
2175            pane.add_item(
2176                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2177                false,
2178                false,
2179                Some(0),
2180                cx,
2181            );
2182        });
2183        assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
2184
2185        //   b. Add after the active item
2186        set_labeled_items(&pane, ["A", "B*", "C"], cx);
2187        pane.update(cx, |pane, cx| {
2188            pane.add_item(
2189                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2190                false,
2191                false,
2192                Some(2),
2193                cx,
2194            );
2195        });
2196        assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
2197
2198        //   c. Add at the end of the item list (including off the length)
2199        set_labeled_items(&pane, ["A", "B*", "C"], cx);
2200        pane.update(cx, |pane, cx| {
2201            pane.add_item(
2202                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2203                false,
2204                false,
2205                Some(5),
2206                cx,
2207            );
2208        });
2209        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2210
2211        // 2. Add without a destination index
2212        //   a. Add with active item at the start of the item list
2213        set_labeled_items(&pane, ["A*", "B", "C"], cx);
2214        pane.update(cx, |pane, cx| {
2215            pane.add_item(
2216                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2217                false,
2218                false,
2219                None,
2220                cx,
2221            );
2222        });
2223        set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
2224
2225        //   b. Add with active item at the end of the item list
2226        set_labeled_items(&pane, ["A", "B", "C*"], cx);
2227        pane.update(cx, |pane, cx| {
2228            pane.add_item(
2229                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2230                false,
2231                false,
2232                None,
2233                cx,
2234            );
2235        });
2236        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2237    }
2238
2239    #[gpui::test]
2240    async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
2241        init_test(cx);
2242        let fs = FakeFs::new(cx.executor());
2243
2244        let project = Project::test(fs, None, cx).await;
2245        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2246        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2247
2248        // 1. Add with a destination index
2249        //   1a. Add before the active item
2250        let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
2251        pane.update(cx, |pane, cx| {
2252            pane.add_item(d, false, false, Some(0), cx);
2253        });
2254        assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
2255
2256        //   1b. Add after the active item
2257        let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
2258        pane.update(cx, |pane, cx| {
2259            pane.add_item(d, false, false, Some(2), cx);
2260        });
2261        assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
2262
2263        //   1c. Add at the end of the item list (including off the length)
2264        let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
2265        pane.update(cx, |pane, cx| {
2266            pane.add_item(a, false, false, Some(5), cx);
2267        });
2268        assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
2269
2270        //   1d. Add same item to active index
2271        let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
2272        pane.update(cx, |pane, cx| {
2273            pane.add_item(b, false, false, Some(1), cx);
2274        });
2275        assert_item_labels(&pane, ["A", "B*", "C"], cx);
2276
2277        //   1e. Add item to index after same item in last position
2278        let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
2279        pane.update(cx, |pane, cx| {
2280            pane.add_item(c, false, false, Some(2), cx);
2281        });
2282        assert_item_labels(&pane, ["A", "B", "C*"], cx);
2283
2284        // 2. Add without a destination index
2285        //   2a. Add with active item at the start of the item list
2286        let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
2287        pane.update(cx, |pane, cx| {
2288            pane.add_item(d, false, false, None, cx);
2289        });
2290        assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
2291
2292        //   2b. Add with active item at the end of the item list
2293        let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
2294        pane.update(cx, |pane, cx| {
2295            pane.add_item(a, false, false, None, cx);
2296        });
2297        assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
2298
2299        //   2c. Add active item to active item at end of list
2300        let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
2301        pane.update(cx, |pane, cx| {
2302            pane.add_item(c, false, false, None, cx);
2303        });
2304        assert_item_labels(&pane, ["A", "B", "C*"], cx);
2305
2306        //   2d. Add active item to active item at start of list
2307        let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
2308        pane.update(cx, |pane, cx| {
2309            pane.add_item(a, false, false, None, cx);
2310        });
2311        assert_item_labels(&pane, ["A*", "B", "C"], cx);
2312    }
2313
2314    #[gpui::test]
2315    async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
2316        init_test(cx);
2317        let fs = FakeFs::new(cx.executor());
2318
2319        let project = Project::test(fs, None, cx).await;
2320        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2321        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2322
2323        // singleton view
2324        pane.update(cx, |pane, cx| {
2325            pane.add_item(
2326                Box::new(cx.new_view(|cx| {
2327                    TestItem::new(cx)
2328                        .with_singleton(true)
2329                        .with_label("buffer 1")
2330                        .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
2331                })),
2332                false,
2333                false,
2334                None,
2335                cx,
2336            );
2337        });
2338        assert_item_labels(&pane, ["buffer 1*"], cx);
2339
2340        // new singleton view with the same project entry
2341        pane.update(cx, |pane, cx| {
2342            pane.add_item(
2343                Box::new(cx.new_view(|cx| {
2344                    TestItem::new(cx)
2345                        .with_singleton(true)
2346                        .with_label("buffer 1")
2347                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
2348                })),
2349                false,
2350                false,
2351                None,
2352                cx,
2353            );
2354        });
2355        assert_item_labels(&pane, ["buffer 1*"], cx);
2356
2357        // new singleton view with different project entry
2358        pane.update(cx, |pane, cx| {
2359            pane.add_item(
2360                Box::new(cx.new_view(|cx| {
2361                    TestItem::new(cx)
2362                        .with_singleton(true)
2363                        .with_label("buffer 2")
2364                        .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
2365                })),
2366                false,
2367                false,
2368                None,
2369                cx,
2370            );
2371        });
2372        assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
2373
2374        // new multibuffer view with the same project entry
2375        pane.update(cx, |pane, cx| {
2376            pane.add_item(
2377                Box::new(cx.new_view(|cx| {
2378                    TestItem::new(cx)
2379                        .with_singleton(false)
2380                        .with_label("multibuffer 1")
2381                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
2382                })),
2383                false,
2384                false,
2385                None,
2386                cx,
2387            );
2388        });
2389        assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
2390
2391        // another multibuffer view with the same project entry
2392        pane.update(cx, |pane, cx| {
2393            pane.add_item(
2394                Box::new(cx.new_view(|cx| {
2395                    TestItem::new(cx)
2396                        .with_singleton(false)
2397                        .with_label("multibuffer 1b")
2398                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
2399                })),
2400                false,
2401                false,
2402                None,
2403                cx,
2404            );
2405        });
2406        assert_item_labels(
2407            &pane,
2408            ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
2409            cx,
2410        );
2411    }
2412
2413    #[gpui::test]
2414    async fn test_remove_item_ordering(cx: &mut TestAppContext) {
2415        init_test(cx);
2416        let fs = FakeFs::new(cx.executor());
2417
2418        let project = Project::test(fs, None, cx).await;
2419        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2420        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2421
2422        add_labeled_item(&pane, "A", false, cx);
2423        add_labeled_item(&pane, "B", false, cx);
2424        add_labeled_item(&pane, "C", false, cx);
2425        add_labeled_item(&pane, "D", false, cx);
2426        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2427
2428        pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
2429        add_labeled_item(&pane, "1", false, cx);
2430        assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
2431
2432        pane.update(cx, |pane, cx| {
2433            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
2434        })
2435        .unwrap()
2436        .await
2437        .unwrap();
2438        assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
2439
2440        pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
2441        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2442
2443        pane.update(cx, |pane, cx| {
2444            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
2445        })
2446        .unwrap()
2447        .await
2448        .unwrap();
2449        assert_item_labels(&pane, ["A", "B*", "C"], cx);
2450
2451        pane.update(cx, |pane, cx| {
2452            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
2453        })
2454        .unwrap()
2455        .await
2456        .unwrap();
2457        assert_item_labels(&pane, ["A", "C*"], cx);
2458
2459        pane.update(cx, |pane, cx| {
2460            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
2461        })
2462        .unwrap()
2463        .await
2464        .unwrap();
2465        assert_item_labels(&pane, ["A*"], cx);
2466    }
2467
2468    #[gpui::test]
2469    async fn test_close_inactive_items(cx: &mut TestAppContext) {
2470        init_test(cx);
2471        let fs = FakeFs::new(cx.executor());
2472
2473        let project = Project::test(fs, None, cx).await;
2474        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2475        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2476
2477        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
2478
2479        pane.update(cx, |pane, cx| {
2480            pane.close_inactive_items(&CloseInactiveItems { save_intent: None }, cx)
2481        })
2482        .unwrap()
2483        .await
2484        .unwrap();
2485        assert_item_labels(&pane, ["C*"], cx);
2486    }
2487
2488    #[gpui::test]
2489    async fn test_close_clean_items(cx: &mut TestAppContext) {
2490        init_test(cx);
2491        let fs = FakeFs::new(cx.executor());
2492
2493        let project = Project::test(fs, None, cx).await;
2494        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2495        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2496
2497        add_labeled_item(&pane, "A", true, cx);
2498        add_labeled_item(&pane, "B", false, cx);
2499        add_labeled_item(&pane, "C", true, cx);
2500        add_labeled_item(&pane, "D", false, cx);
2501        add_labeled_item(&pane, "E", false, cx);
2502        assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
2503
2504        pane.update(cx, |pane, cx| pane.close_clean_items(&CloseCleanItems, cx))
2505            .unwrap()
2506            .await
2507            .unwrap();
2508        assert_item_labels(&pane, ["A^", "C*^"], cx);
2509    }
2510
2511    #[gpui::test]
2512    async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
2513        init_test(cx);
2514        let fs = FakeFs::new(cx.executor());
2515
2516        let project = Project::test(fs, None, cx).await;
2517        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2518        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2519
2520        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
2521
2522        pane.update(cx, |pane, cx| {
2523            pane.close_items_to_the_left(&CloseItemsToTheLeft, cx)
2524        })
2525        .unwrap()
2526        .await
2527        .unwrap();
2528        assert_item_labels(&pane, ["C*", "D", "E"], cx);
2529    }
2530
2531    #[gpui::test]
2532    async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
2533        init_test(cx);
2534        let fs = FakeFs::new(cx.executor());
2535
2536        let project = Project::test(fs, None, cx).await;
2537        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2538        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2539
2540        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
2541
2542        pane.update(cx, |pane, cx| {
2543            pane.close_items_to_the_right(&CloseItemsToTheRight, cx)
2544        })
2545        .unwrap()
2546        .await
2547        .unwrap();
2548        assert_item_labels(&pane, ["A", "B", "C*"], cx);
2549    }
2550
2551    #[gpui::test]
2552    async fn test_close_all_items(cx: &mut TestAppContext) {
2553        init_test(cx);
2554        let fs = FakeFs::new(cx.executor());
2555
2556        let project = Project::test(fs, None, cx).await;
2557        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2558        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2559
2560        add_labeled_item(&pane, "A", false, cx);
2561        add_labeled_item(&pane, "B", false, cx);
2562        add_labeled_item(&pane, "C", false, cx);
2563        assert_item_labels(&pane, ["A", "B", "C*"], cx);
2564
2565        pane.update(cx, |pane, cx| {
2566            pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
2567        })
2568        .unwrap()
2569        .await
2570        .unwrap();
2571        assert_item_labels(&pane, [], cx);
2572
2573        add_labeled_item(&pane, "A", true, cx);
2574        add_labeled_item(&pane, "B", true, cx);
2575        add_labeled_item(&pane, "C", true, cx);
2576        assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
2577
2578        let save = pane
2579            .update(cx, |pane, cx| {
2580                pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
2581            })
2582            .unwrap();
2583
2584        cx.executor().run_until_parked();
2585        cx.simulate_prompt_answer(2);
2586        save.await.unwrap();
2587        assert_item_labels(&pane, [], cx);
2588    }
2589
2590    fn init_test(cx: &mut TestAppContext) {
2591        cx.update(|cx| {
2592            let settings_store = SettingsStore::test(cx);
2593            cx.set_global(settings_store);
2594            theme::init(LoadThemes::JustBase, cx);
2595            crate::init_settings(cx);
2596            Project::init_settings(cx);
2597        });
2598    }
2599
2600    fn add_labeled_item(
2601        pane: &View<Pane>,
2602        label: &str,
2603        is_dirty: bool,
2604        cx: &mut VisualTestContext,
2605    ) -> Box<View<TestItem>> {
2606        pane.update(cx, |pane, cx| {
2607            let labeled_item = Box::new(
2608                cx.new_view(|cx| TestItem::new(cx).with_label(label).with_dirty(is_dirty)),
2609            );
2610            pane.add_item(labeled_item.clone(), false, false, None, cx);
2611            labeled_item
2612        })
2613    }
2614
2615    fn set_labeled_items<const COUNT: usize>(
2616        pane: &View<Pane>,
2617        labels: [&str; COUNT],
2618        cx: &mut VisualTestContext,
2619    ) -> [Box<View<TestItem>>; COUNT] {
2620        pane.update(cx, |pane, cx| {
2621            pane.items.clear();
2622            let mut active_item_index = 0;
2623
2624            let mut index = 0;
2625            let items = labels.map(|mut label| {
2626                if label.ends_with('*') {
2627                    label = label.trim_end_matches('*');
2628                    active_item_index = index;
2629                }
2630
2631                let labeled_item = Box::new(cx.new_view(|cx| TestItem::new(cx).with_label(label)));
2632                pane.add_item(labeled_item.clone(), false, false, None, cx);
2633                index += 1;
2634                labeled_item
2635            });
2636
2637            pane.activate_item(active_item_index, false, false, cx);
2638
2639            items
2640        })
2641    }
2642
2643    // Assert the item label, with the active item label suffixed with a '*'
2644    fn assert_item_labels<const COUNT: usize>(
2645        pane: &View<Pane>,
2646        expected_states: [&str; COUNT],
2647        cx: &mut VisualTestContext,
2648    ) {
2649        pane.update(cx, |pane, cx| {
2650            let actual_states = pane
2651                .items
2652                .iter()
2653                .enumerate()
2654                .map(|(ix, item)| {
2655                    let mut state = item
2656                        .to_any()
2657                        .downcast::<TestItem>()
2658                        .unwrap()
2659                        .read(cx)
2660                        .label
2661                        .clone();
2662                    if ix == pane.active_item_index {
2663                        state.push('*');
2664                    }
2665                    if item.is_dirty(cx) {
2666                        state.push('^');
2667                    }
2668                    state
2669                })
2670                .collect::<Vec<_>>();
2671
2672            assert_eq!(
2673                actual_states, expected_states,
2674                "pane items do not match expectation"
2675            );
2676        })
2677    }
2678}
2679
2680impl Render for DraggedTab {
2681    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2682        let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
2683        let label = self.item.tab_content(Some(self.detail), false, cx);
2684        Tab::new("")
2685            .selected(self.is_active)
2686            .child(label)
2687            .render(cx)
2688            .font(ui_font)
2689    }
2690}