pane.rs

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