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