pane.rs

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