pane.rs

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