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