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