pane.rs

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