pane.rs

   1use crate::{
   2    item::{
   3        ActivateOnClose, ClosePosition, Item, ItemHandle, ItemSettings, PreviewTabsSettings,
   4        TabContentParams, 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        let activate_on_close = &ItemSettings::get_global(cx).activate_on_close;
1404        self.activation_history
1405            .retain(|entry| entry.entity_id != self.items[item_index].item_id());
1406
1407        if self.is_tab_pinned(item_index) {
1408            self.pinned_tab_count -= 1;
1409        }
1410        if item_index == self.active_item_index {
1411            let index_to_activate = match activate_on_close {
1412                ActivateOnClose::History => self
1413                    .activation_history
1414                    .pop()
1415                    .and_then(|last_activated_item| {
1416                        self.items.iter().enumerate().find_map(|(index, item)| {
1417                            (item.item_id() == last_activated_item.entity_id).then_some(index)
1418                        })
1419                    })
1420                    // We didn't have a valid activation history entry, so fallback
1421                    // to activating the item to the left
1422                    .unwrap_or_else(|| item_index.min(self.items.len()).saturating_sub(1)),
1423                ActivateOnClose::Neighbour => {
1424                    self.activation_history.pop();
1425                    if item_index + 1 < self.items.len() {
1426                        item_index + 1
1427                    } else {
1428                        item_index.saturating_sub(1)
1429                    }
1430                }
1431            };
1432
1433            let should_activate = activate_pane || self.has_focus(cx);
1434            if self.items.len() == 1 && should_activate {
1435                self.focus_handle.focus(cx);
1436            } else {
1437                self.activate_item(index_to_activate, should_activate, should_activate, cx);
1438            }
1439        }
1440
1441        cx.emit(Event::RemoveItem { idx: item_index });
1442
1443        let item = self.items.remove(item_index);
1444
1445        cx.emit(Event::RemovedItem {
1446            item_id: item.item_id(),
1447        });
1448        if self.items.is_empty() {
1449            item.deactivated(cx);
1450            if close_pane_if_empty {
1451                self.update_toolbar(cx);
1452                cx.emit(Event::Remove {
1453                    focus_on_pane: focus_on_pane_if_closed,
1454                });
1455            }
1456        }
1457
1458        if item_index < self.active_item_index {
1459            self.active_item_index -= 1;
1460        }
1461
1462        let mode = self.nav_history.mode();
1463        self.nav_history.set_mode(NavigationMode::ClosingItem);
1464        item.deactivated(cx);
1465        self.nav_history.set_mode(mode);
1466
1467        if self.is_active_preview_item(item.item_id()) {
1468            self.set_preview_item_id(None, cx);
1469        }
1470
1471        if let Some(path) = item.project_path(cx) {
1472            let abs_path = self
1473                .nav_history
1474                .0
1475                .lock()
1476                .paths_by_item
1477                .get(&item.item_id())
1478                .and_then(|(_, abs_path)| abs_path.clone());
1479
1480            self.nav_history
1481                .0
1482                .lock()
1483                .paths_by_item
1484                .insert(item.item_id(), (path, abs_path));
1485        } else {
1486            self.nav_history
1487                .0
1488                .lock()
1489                .paths_by_item
1490                .remove(&item.item_id());
1491        }
1492
1493        if self.items.is_empty() && close_pane_if_empty && self.zoomed {
1494            cx.emit(Event::ZoomOut);
1495        }
1496
1497        cx.notify();
1498    }
1499
1500    pub async fn save_item(
1501        project: Model<Project>,
1502        pane: &WeakView<Pane>,
1503        item_ix: usize,
1504        item: &dyn ItemHandle,
1505        save_intent: SaveIntent,
1506        cx: &mut AsyncWindowContext,
1507    ) -> Result<bool> {
1508        const CONFLICT_MESSAGE: &str =
1509                "This file has changed on disk since you started editing it. Do you want to overwrite it?";
1510
1511        if save_intent == SaveIntent::Skip {
1512            return Ok(true);
1513        }
1514
1515        let (mut has_conflict, mut is_dirty, mut can_save, can_save_as) = cx.update(|cx| {
1516            (
1517                item.has_conflict(cx),
1518                item.is_dirty(cx),
1519                item.can_save(cx),
1520                item.is_singleton(cx),
1521            )
1522        })?;
1523
1524        // when saving a single buffer, we ignore whether or not it's dirty.
1525        if save_intent == SaveIntent::Save || save_intent == SaveIntent::SaveWithoutFormat {
1526            is_dirty = true;
1527        }
1528
1529        if save_intent == SaveIntent::SaveAs {
1530            is_dirty = true;
1531            has_conflict = false;
1532            can_save = false;
1533        }
1534
1535        if save_intent == SaveIntent::Overwrite {
1536            has_conflict = false;
1537        }
1538
1539        let should_format = save_intent != SaveIntent::SaveWithoutFormat;
1540
1541        if has_conflict && can_save {
1542            let answer = pane.update(cx, |pane, cx| {
1543                pane.activate_item(item_ix, true, true, cx);
1544                cx.prompt(
1545                    PromptLevel::Warning,
1546                    CONFLICT_MESSAGE,
1547                    None,
1548                    &["Overwrite", "Discard", "Cancel"],
1549                )
1550            })?;
1551            match answer.await {
1552                Ok(0) => {
1553                    pane.update(cx, |_, cx| item.save(should_format, project, cx))?
1554                        .await?
1555                }
1556                Ok(1) => pane.update(cx, |_, cx| item.reload(project, cx))?.await?,
1557                _ => return Ok(false),
1558            }
1559        } else if is_dirty && (can_save || can_save_as) {
1560            if save_intent == SaveIntent::Close {
1561                let will_autosave = cx.update(|cx| {
1562                    matches!(
1563                        item.workspace_settings(cx).autosave,
1564                        AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange
1565                    ) && Self::can_autosave_item(item, cx)
1566                })?;
1567                if !will_autosave {
1568                    let item_id = item.item_id();
1569                    let answer_task = pane.update(cx, |pane, cx| {
1570                        if pane.save_modals_spawned.insert(item_id) {
1571                            pane.activate_item(item_ix, true, true, cx);
1572                            let prompt = dirty_message_for(item.project_path(cx));
1573                            Some(cx.prompt(
1574                                PromptLevel::Warning,
1575                                &prompt,
1576                                None,
1577                                &["Save", "Don't Save", "Cancel"],
1578                            ))
1579                        } else {
1580                            None
1581                        }
1582                    })?;
1583                    if let Some(answer_task) = answer_task {
1584                        let answer = answer_task.await;
1585                        pane.update(cx, |pane, _| {
1586                            if !pane.save_modals_spawned.remove(&item_id) {
1587                                debug_panic!(
1588                                    "save modal was not present in spawned modals after awaiting for its answer"
1589                                )
1590                            }
1591                        })?;
1592                        match answer {
1593                            Ok(0) => {}
1594                            Ok(1) => {
1595                                // Don't save this file
1596                                pane.update(cx, |pane, cx| {
1597                                    if pane.is_tab_pinned(item_ix) && !item.can_save(cx) {
1598                                        pane.pinned_tab_count -= 1;
1599                                    }
1600                                    item.discarded(project, cx)
1601                                })
1602                                .log_err();
1603                                return Ok(true);
1604                            }
1605                            _ => return Ok(false), // Cancel
1606                        }
1607                    } else {
1608                        return Ok(false);
1609                    }
1610                }
1611            }
1612
1613            if can_save {
1614                pane.update(cx, |pane, cx| {
1615                    if pane.is_active_preview_item(item.item_id()) {
1616                        pane.set_preview_item_id(None, cx);
1617                    }
1618                    item.save(should_format, project, cx)
1619                })?
1620                .await?;
1621            } else if can_save_as {
1622                let abs_path = pane.update(cx, |pane, cx| {
1623                    pane.workspace
1624                        .update(cx, |workspace, cx| workspace.prompt_for_new_path(cx))
1625                })??;
1626                if let Some(abs_path) = abs_path.await.ok().flatten() {
1627                    pane.update(cx, |pane, cx| {
1628                        if let Some(item) = pane.item_for_path(abs_path.clone(), cx) {
1629                            if let Some(idx) = pane.index_for_item(&*item) {
1630                                pane.remove_item(idx, false, false, cx);
1631                            }
1632                        }
1633
1634                        item.save_as(project, abs_path, cx)
1635                    })?
1636                    .await?;
1637                } else {
1638                    return Ok(false);
1639                }
1640            }
1641        }
1642
1643        pane.update(cx, |_, cx| {
1644            cx.emit(Event::UserSavedItem {
1645                item: item.downgrade_item(),
1646                save_intent,
1647            });
1648            true
1649        })
1650    }
1651
1652    fn can_autosave_item(item: &dyn ItemHandle, cx: &AppContext) -> bool {
1653        let is_deleted = item.project_entry_ids(cx).is_empty();
1654        item.is_dirty(cx) && !item.has_conflict(cx) && item.can_save(cx) && !is_deleted
1655    }
1656
1657    pub fn autosave_item(
1658        item: &dyn ItemHandle,
1659        project: Model<Project>,
1660        cx: &mut WindowContext,
1661    ) -> Task<Result<()>> {
1662        let format = !matches!(
1663            item.workspace_settings(cx).autosave,
1664            AutosaveSetting::AfterDelay { .. }
1665        );
1666        if Self::can_autosave_item(item, cx) {
1667            item.save(format, project, cx)
1668        } else {
1669            Task::ready(Ok(()))
1670        }
1671    }
1672
1673    pub fn focus(&mut self, cx: &mut ViewContext<Pane>) {
1674        cx.focus(&self.focus_handle);
1675    }
1676
1677    pub fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
1678        if let Some(active_item) = self.active_item() {
1679            let focus_handle = active_item.focus_handle(cx);
1680            cx.focus(&focus_handle);
1681        }
1682    }
1683
1684    pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext<Self>) {
1685        cx.emit(Event::Split(direction));
1686    }
1687
1688    pub fn toolbar(&self) -> &View<Toolbar> {
1689        &self.toolbar
1690    }
1691
1692    pub fn handle_deleted_project_item(
1693        &mut self,
1694        entry_id: ProjectEntryId,
1695        cx: &mut ViewContext<Pane>,
1696    ) -> Option<()> {
1697        let (item_index_to_delete, item_id) = self.items().enumerate().find_map(|(i, item)| {
1698            if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
1699                Some((i, item.item_id()))
1700            } else {
1701                None
1702            }
1703        })?;
1704
1705        self.remove_item(item_index_to_delete, false, true, cx);
1706        self.nav_history.remove_item(item_id);
1707
1708        Some(())
1709    }
1710
1711    fn update_toolbar(&mut self, cx: &mut ViewContext<Self>) {
1712        let active_item = self
1713            .items
1714            .get(self.active_item_index)
1715            .map(|item| item.as_ref());
1716        self.toolbar.update(cx, |toolbar, cx| {
1717            toolbar.set_active_item(active_item, cx);
1718        });
1719    }
1720
1721    fn update_status_bar(&mut self, cx: &mut ViewContext<Self>) {
1722        let workspace = self.workspace.clone();
1723        let pane = cx.view().clone();
1724
1725        cx.window_context().defer(move |cx| {
1726            let Ok(status_bar) = workspace.update(cx, |workspace, _| workspace.status_bar.clone())
1727            else {
1728                return;
1729            };
1730
1731            status_bar.update(cx, move |status_bar, cx| {
1732                status_bar.set_active_pane(&pane, cx);
1733            });
1734        });
1735    }
1736
1737    fn entry_abs_path(&self, entry: ProjectEntryId, cx: &WindowContext) -> Option<PathBuf> {
1738        let worktree = self
1739            .workspace
1740            .upgrade()?
1741            .read(cx)
1742            .project()
1743            .read(cx)
1744            .worktree_for_entry(entry, cx)?
1745            .read(cx);
1746        let entry = worktree.entry_for_id(entry)?;
1747        match &entry.canonical_path {
1748            Some(canonical_path) => Some(canonical_path.to_path_buf()),
1749            None => worktree.absolutize(&entry.path).ok(),
1750        }
1751    }
1752
1753    pub fn icon_color(selected: bool) -> Color {
1754        if selected {
1755            Color::Default
1756        } else {
1757            Color::Muted
1758        }
1759    }
1760
1761    pub fn git_aware_icon_color(
1762        git_status: Option<GitFileStatus>,
1763        ignored: bool,
1764        selected: bool,
1765    ) -> Color {
1766        if ignored {
1767            Color::Ignored
1768        } else {
1769            match git_status {
1770                Some(GitFileStatus::Added) => Color::Created,
1771                Some(GitFileStatus::Modified) => Color::Modified,
1772                Some(GitFileStatus::Conflict) => Color::Conflict,
1773                None => Self::icon_color(selected),
1774            }
1775        }
1776    }
1777
1778    fn toggle_pin_tab(&mut self, _: &TogglePinTab, cx: &mut ViewContext<'_, Self>) {
1779        if self.items.is_empty() {
1780            return;
1781        }
1782        let active_tab_ix = self.active_item_index();
1783        if self.is_tab_pinned(active_tab_ix) {
1784            self.unpin_tab_at(active_tab_ix, cx);
1785        } else {
1786            self.pin_tab_at(active_tab_ix, cx);
1787        }
1788    }
1789
1790    fn pin_tab_at(&mut self, ix: usize, cx: &mut ViewContext<'_, Self>) {
1791        maybe!({
1792            let pane = cx.view().clone();
1793            let destination_index = self.pinned_tab_count;
1794            self.pinned_tab_count += 1;
1795            let id = self.item_for_index(ix)?.item_id();
1796
1797            self.workspace
1798                .update(cx, |_, cx| {
1799                    cx.defer(move |_, cx| move_item(&pane, &pane, id, destination_index, cx));
1800                })
1801                .ok()?;
1802
1803            Some(())
1804        });
1805    }
1806
1807    fn unpin_tab_at(&mut self, ix: usize, cx: &mut ViewContext<'_, Self>) {
1808        maybe!({
1809            let pane = cx.view().clone();
1810            self.pinned_tab_count = self.pinned_tab_count.checked_sub(1).unwrap();
1811            let destination_index = self.pinned_tab_count;
1812
1813            let id = self.item_for_index(ix)?.item_id();
1814
1815            self.workspace
1816                .update(cx, |_, cx| {
1817                    cx.defer(move |_, cx| move_item(&pane, &pane, id, destination_index, cx));
1818                })
1819                .ok()?;
1820
1821            Some(())
1822        });
1823    }
1824
1825    fn is_tab_pinned(&self, ix: usize) -> bool {
1826        self.pinned_tab_count > ix
1827    }
1828
1829    fn has_pinned_tabs(&self) -> bool {
1830        self.pinned_tab_count != 0
1831    }
1832
1833    fn render_tab(
1834        &self,
1835        ix: usize,
1836        item: &dyn ItemHandle,
1837        detail: usize,
1838        focus_handle: &FocusHandle,
1839        cx: &mut ViewContext<'_, Pane>,
1840    ) -> impl IntoElement {
1841        let project_path = item.project_path(cx);
1842
1843        let is_active = ix == self.active_item_index;
1844        let is_preview = self
1845            .preview_item_id
1846            .map(|id| id == item.item_id())
1847            .unwrap_or(false);
1848
1849        let label = item.tab_content(
1850            TabContentParams {
1851                detail: Some(detail),
1852                selected: is_active,
1853                preview: is_preview,
1854            },
1855            cx,
1856        );
1857
1858        let icon_color = if ItemSettings::get_global(cx).git_status {
1859            project_path
1860                .as_ref()
1861                .and_then(|path| self.project.read(cx).entry_for_path(path, cx))
1862                .map(|entry| {
1863                    Self::git_aware_icon_color(entry.git_status, entry.is_ignored, is_active)
1864                })
1865                .unwrap_or_else(|| Self::icon_color(is_active))
1866        } else {
1867            Self::icon_color(is_active)
1868        };
1869
1870        let icon = item.tab_icon(cx);
1871        let close_side = &ItemSettings::get_global(cx).close_position;
1872        let indicator = render_item_indicator(item.boxed_clone(), cx);
1873        let item_id = item.item_id();
1874        let is_first_item = ix == 0;
1875        let is_last_item = ix == self.items.len() - 1;
1876        let is_pinned = self.is_tab_pinned(ix);
1877        let position_relative_to_active_item = ix.cmp(&self.active_item_index);
1878
1879        let tab = Tab::new(ix)
1880            .position(if is_first_item {
1881                TabPosition::First
1882            } else if is_last_item {
1883                TabPosition::Last
1884            } else {
1885                TabPosition::Middle(position_relative_to_active_item)
1886            })
1887            .close_side(match close_side {
1888                ClosePosition::Left => ui::TabCloseSide::Start,
1889                ClosePosition::Right => ui::TabCloseSide::End,
1890            })
1891            .selected(is_active)
1892            .on_click(
1893                cx.listener(move |pane: &mut Self, _, cx| pane.activate_item(ix, true, true, cx)),
1894            )
1895            // TODO: This should be a click listener with the middle mouse button instead of a mouse down listener.
1896            .on_mouse_down(
1897                MouseButton::Middle,
1898                cx.listener(move |pane, _event, cx| {
1899                    pane.close_item_by_id(item_id, SaveIntent::Close, cx)
1900                        .detach_and_log_err(cx);
1901                }),
1902            )
1903            .on_mouse_down(
1904                MouseButton::Left,
1905                cx.listener(move |pane, event: &MouseDownEvent, cx| {
1906                    if let Some(id) = pane.preview_item_id {
1907                        if id == item_id && event.click_count > 1 {
1908                            pane.set_preview_item_id(None, cx);
1909                        }
1910                    }
1911                }),
1912            )
1913            .on_drag(
1914                DraggedTab {
1915                    item: item.boxed_clone(),
1916                    pane: cx.view().clone(),
1917                    detail,
1918                    is_active,
1919                    ix,
1920                },
1921                |tab, cx| cx.new_view(|_| tab.clone()),
1922            )
1923            .drag_over::<DraggedTab>(|tab, _, cx| {
1924                tab.bg(cx.theme().colors().drop_target_background)
1925            })
1926            .drag_over::<DraggedSelection>(|tab, _, cx| {
1927                tab.bg(cx.theme().colors().drop_target_background)
1928            })
1929            .when_some(self.can_drop_predicate.clone(), |this, p| {
1930                this.can_drop(move |a, cx| p(a, cx))
1931            })
1932            .on_drop(cx.listener(move |this, dragged_tab: &DraggedTab, cx| {
1933                this.drag_split_direction = None;
1934                this.handle_tab_drop(dragged_tab, ix, cx)
1935            }))
1936            .on_drop(cx.listener(move |this, selection: &DraggedSelection, cx| {
1937                this.drag_split_direction = None;
1938                this.handle_dragged_selection_drop(selection, cx)
1939            }))
1940            .on_drop(cx.listener(move |this, paths, cx| {
1941                this.drag_split_direction = None;
1942                this.handle_external_paths_drop(paths, cx)
1943            }))
1944            .when_some(item.tab_tooltip_text(cx), |tab, text| {
1945                tab.tooltip(move |cx| Tooltip::text(text.clone(), cx))
1946            })
1947            .start_slot::<Indicator>(indicator)
1948            .map(|this| {
1949                let end_slot_action: &'static dyn Action;
1950                let end_slot_tooltip_text: &'static str;
1951                let end_slot = if is_pinned {
1952                    end_slot_action = &TogglePinTab;
1953                    end_slot_tooltip_text = "Unpin Tab";
1954                    IconButton::new("unpin tab", IconName::Pin)
1955                        .shape(IconButtonShape::Square)
1956                        .icon_color(Color::Muted)
1957                        .size(ButtonSize::None)
1958                        .icon_size(IconSize::XSmall)
1959                        .on_click(cx.listener(move |pane, _, cx| {
1960                            pane.unpin_tab_at(ix, cx);
1961                        }))
1962                } else {
1963                    end_slot_action = &CloseActiveItem { save_intent: None };
1964                    end_slot_tooltip_text = "Close Tab";
1965                    IconButton::new("close tab", IconName::Close)
1966                        .visible_on_hover("")
1967                        .shape(IconButtonShape::Square)
1968                        .icon_color(Color::Muted)
1969                        .size(ButtonSize::None)
1970                        .icon_size(IconSize::XSmall)
1971                        .on_click(cx.listener(move |pane, _, cx| {
1972                            pane.close_item_by_id(item_id, SaveIntent::Close, cx)
1973                                .detach_and_log_err(cx);
1974                        }))
1975                }
1976                .map(|this| {
1977                    if is_active {
1978                        let focus_handle = focus_handle.clone();
1979                        this.tooltip(move |cx| {
1980                            Tooltip::for_action_in(
1981                                end_slot_tooltip_text,
1982                                end_slot_action,
1983                                &focus_handle,
1984                                cx,
1985                            )
1986                        })
1987                    } else {
1988                        this.tooltip(move |cx| Tooltip::text(end_slot_tooltip_text, cx))
1989                    }
1990                });
1991                this.end_slot(end_slot)
1992            })
1993            .child(
1994                h_flex()
1995                    .gap_1()
1996                    .children(icon.map(|icon| icon.size(IconSize::Small).color(icon_color)))
1997                    .child(label),
1998            );
1999
2000        let single_entry_to_resolve = {
2001            let item_entries = self.items[ix].project_entry_ids(cx);
2002            if item_entries.len() == 1 {
2003                Some(item_entries[0])
2004            } else {
2005                None
2006            }
2007        };
2008
2009        let is_pinned = self.is_tab_pinned(ix);
2010        let pane = cx.view().downgrade();
2011        right_click_menu(ix).trigger(tab).menu(move |cx| {
2012            let pane = pane.clone();
2013            ContextMenu::build(cx, move |mut menu, cx| {
2014                if let Some(pane) = pane.upgrade() {
2015                    menu = menu
2016                        .entry(
2017                            "Close",
2018                            Some(Box::new(CloseActiveItem { save_intent: None })),
2019                            cx.handler_for(&pane, move |pane, cx| {
2020                                pane.close_item_by_id(item_id, SaveIntent::Close, cx)
2021                                    .detach_and_log_err(cx);
2022                            }),
2023                        )
2024                        .entry(
2025                            "Close Others",
2026                            Some(Box::new(CloseInactiveItems { save_intent: None })),
2027                            cx.handler_for(&pane, move |pane, cx| {
2028                                pane.close_items(cx, SaveIntent::Close, |id| id != item_id)
2029                                    .detach_and_log_err(cx);
2030                            }),
2031                        )
2032                        .separator()
2033                        .entry(
2034                            "Close Left",
2035                            Some(Box::new(CloseItemsToTheLeft)),
2036                            cx.handler_for(&pane, move |pane, cx| {
2037                                pane.close_items_to_the_left_by_id(item_id, cx)
2038                                    .detach_and_log_err(cx);
2039                            }),
2040                        )
2041                        .entry(
2042                            "Close Right",
2043                            Some(Box::new(CloseItemsToTheRight)),
2044                            cx.handler_for(&pane, move |pane, cx| {
2045                                pane.close_items_to_the_right_by_id(item_id, cx)
2046                                    .detach_and_log_err(cx);
2047                            }),
2048                        )
2049                        .separator()
2050                        .entry(
2051                            "Close Clean",
2052                            Some(Box::new(CloseCleanItems)),
2053                            cx.handler_for(&pane, move |pane, cx| {
2054                                if let Some(task) = pane.close_clean_items(&CloseCleanItems, cx) {
2055                                    task.detach_and_log_err(cx)
2056                                }
2057                            }),
2058                        )
2059                        .entry(
2060                            "Close All",
2061                            Some(Box::new(CloseAllItems { save_intent: None })),
2062                            cx.handler_for(&pane, |pane, cx| {
2063                                if let Some(task) =
2064                                    pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
2065                                {
2066                                    task.detach_and_log_err(cx)
2067                                }
2068                            }),
2069                        );
2070
2071                    let pin_tab_entries = |menu: ContextMenu| {
2072                        menu.separator().map(|this| {
2073                            if is_pinned {
2074                                this.entry(
2075                                    "Unpin Tab",
2076                                    Some(TogglePinTab.boxed_clone()),
2077                                    cx.handler_for(&pane, move |pane, cx| {
2078                                        pane.unpin_tab_at(ix, cx);
2079                                    }),
2080                                )
2081                            } else {
2082                                this.entry(
2083                                    "Pin Tab",
2084                                    Some(TogglePinTab.boxed_clone()),
2085                                    cx.handler_for(&pane, move |pane, cx| {
2086                                        pane.pin_tab_at(ix, cx);
2087                                    }),
2088                                )
2089                            }
2090                        })
2091                    };
2092                    if let Some(entry) = single_entry_to_resolve {
2093                        let entry_abs_path = pane.read(cx).entry_abs_path(entry, cx);
2094                        let parent_abs_path = entry_abs_path
2095                            .as_deref()
2096                            .and_then(|abs_path| Some(abs_path.parent()?.to_path_buf()));
2097                        let relative_path = pane
2098                            .read(cx)
2099                            .item_for_entry(entry, cx)
2100                            .and_then(|item| item.project_path(cx))
2101                            .map(|project_path| project_path.path);
2102
2103                        let entry_id = entry.to_proto();
2104                        menu = menu
2105                            .separator()
2106                            .when_some(entry_abs_path, |menu, abs_path| {
2107                                menu.entry(
2108                                    "Copy Path",
2109                                    Some(Box::new(CopyPath)),
2110                                    cx.handler_for(&pane, move |_, cx| {
2111                                        cx.write_to_clipboard(ClipboardItem::new_string(
2112                                            abs_path.to_string_lossy().to_string(),
2113                                        ));
2114                                    }),
2115                                )
2116                            })
2117                            .when_some(relative_path, |menu, relative_path| {
2118                                menu.entry(
2119                                    "Copy Relative Path",
2120                                    Some(Box::new(CopyRelativePath)),
2121                                    cx.handler_for(&pane, move |_, cx| {
2122                                        cx.write_to_clipboard(ClipboardItem::new_string(
2123                                            relative_path.to_string_lossy().to_string(),
2124                                        ));
2125                                    }),
2126                                )
2127                            })
2128                            .map(pin_tab_entries)
2129                            .separator()
2130                            .entry(
2131                                "Reveal In Project Panel",
2132                                Some(Box::new(RevealInProjectPanel {
2133                                    entry_id: Some(entry_id),
2134                                })),
2135                                cx.handler_for(&pane, move |pane, cx| {
2136                                    pane.project.update(cx, |_, cx| {
2137                                        cx.emit(project::Event::RevealInProjectPanel(
2138                                            ProjectEntryId::from_proto(entry_id),
2139                                        ))
2140                                    });
2141                                }),
2142                            )
2143                            .when_some(parent_abs_path, |menu, parent_abs_path| {
2144                                menu.entry(
2145                                    "Open in Terminal",
2146                                    Some(Box::new(OpenInTerminal)),
2147                                    cx.handler_for(&pane, move |_, cx| {
2148                                        cx.dispatch_action(
2149                                            OpenTerminal {
2150                                                working_directory: parent_abs_path.clone(),
2151                                            }
2152                                            .boxed_clone(),
2153                                        );
2154                                    }),
2155                                )
2156                            });
2157                    } else {
2158                        menu = menu.map(pin_tab_entries);
2159                    }
2160                }
2161
2162                menu
2163            })
2164        })
2165    }
2166
2167    fn render_tab_bar(&mut self, cx: &mut ViewContext<'_, Pane>) -> impl IntoElement {
2168        let focus_handle = self.focus_handle.clone();
2169        let navigate_backward = IconButton::new("navigate_backward", IconName::ArrowLeft)
2170            .shape(IconButtonShape::Square)
2171            .icon_size(IconSize::Small)
2172            .on_click({
2173                let view = cx.view().clone();
2174                move |_, cx| view.update(cx, Self::navigate_backward)
2175            })
2176            .disabled(!self.can_navigate_backward())
2177            .tooltip({
2178                let focus_handle = focus_handle.clone();
2179                move |cx| Tooltip::for_action_in("Go Back", &GoBack, &focus_handle, cx)
2180            });
2181
2182        let navigate_forward = IconButton::new("navigate_forward", IconName::ArrowRight)
2183            .shape(IconButtonShape::Square)
2184            .icon_size(IconSize::Small)
2185            .on_click({
2186                let view = cx.view().clone();
2187                move |_, cx| view.update(cx, Self::navigate_forward)
2188            })
2189            .disabled(!self.can_navigate_forward())
2190            .tooltip({
2191                let focus_handle = focus_handle.clone();
2192                move |cx| Tooltip::for_action_in("Go Forward", &GoForward, &focus_handle, cx)
2193            });
2194
2195        let mut tab_items = self
2196            .items
2197            .iter()
2198            .enumerate()
2199            .zip(tab_details(&self.items, cx))
2200            .map(|((ix, item), detail)| self.render_tab(ix, &**item, detail, &focus_handle, cx))
2201            .collect::<Vec<_>>();
2202
2203        let unpinned_tabs = tab_items.split_off(self.pinned_tab_count);
2204        let pinned_tabs = tab_items;
2205        TabBar::new("tab_bar")
2206            .when(
2207                self.display_nav_history_buttons.unwrap_or_default(),
2208                |tab_bar| {
2209                    tab_bar
2210                        .start_child(navigate_backward)
2211                        .start_child(navigate_forward)
2212                },
2213            )
2214            .map(|tab_bar| {
2215                let render_tab_buttons = self.render_tab_bar_buttons.clone();
2216                let (left_children, right_children) = render_tab_buttons(self, cx);
2217
2218                tab_bar
2219                    .start_children(left_children)
2220                    .end_children(right_children)
2221            })
2222            .children(pinned_tabs.len().ne(&0).then(|| {
2223                h_flex()
2224                    .children(pinned_tabs)
2225                    .border_r_2()
2226                    .border_color(cx.theme().colors().border)
2227            }))
2228            .child(
2229                h_flex()
2230                    .id("unpinned tabs")
2231                    .overflow_x_scroll()
2232                    .w_full()
2233                    .track_scroll(&self.tab_bar_scroll_handle)
2234                    .children(unpinned_tabs)
2235                    .child(
2236                        div()
2237                            .id("tab_bar_drop_target")
2238                            .min_w_6()
2239                            // HACK: This empty child is currently necessary to force the drop target to appear
2240                            // despite us setting a min width above.
2241                            .child("")
2242                            .h_full()
2243                            .flex_grow()
2244                            .drag_over::<DraggedTab>(|bar, _, cx| {
2245                                bar.bg(cx.theme().colors().drop_target_background)
2246                            })
2247                            .drag_over::<DraggedSelection>(|bar, _, cx| {
2248                                bar.bg(cx.theme().colors().drop_target_background)
2249                            })
2250                            .on_drop(cx.listener(move |this, dragged_tab: &DraggedTab, cx| {
2251                                this.drag_split_direction = None;
2252                                this.handle_tab_drop(dragged_tab, this.items.len(), cx)
2253                            }))
2254                            .on_drop(cx.listener(move |this, selection: &DraggedSelection, cx| {
2255                                this.drag_split_direction = None;
2256                                this.handle_project_entry_drop(
2257                                    &selection.active_selection.entry_id,
2258                                    cx,
2259                                )
2260                            }))
2261                            .on_drop(cx.listener(move |this, paths, cx| {
2262                                this.drag_split_direction = None;
2263                                this.handle_external_paths_drop(paths, cx)
2264                            }))
2265                            .on_click(cx.listener(move |this, event: &ClickEvent, cx| {
2266                                if event.up.click_count == 2 {
2267                                    cx.dispatch_action(
2268                                        this.double_click_dispatch_action.boxed_clone(),
2269                                    )
2270                                }
2271                            })),
2272                    ),
2273            )
2274    }
2275
2276    pub fn render_menu_overlay(menu: &View<ContextMenu>) -> Div {
2277        div().absolute().bottom_0().right_0().size_0().child(
2278            deferred(
2279                anchored()
2280                    .anchor(AnchorCorner::TopRight)
2281                    .child(menu.clone()),
2282            )
2283            .with_priority(1),
2284        )
2285    }
2286
2287    pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
2288        self.zoomed = zoomed;
2289        cx.notify();
2290    }
2291
2292    pub fn is_zoomed(&self) -> bool {
2293        self.zoomed
2294    }
2295
2296    fn handle_drag_move<T>(&mut self, event: &DragMoveEvent<T>, cx: &mut ViewContext<Self>) {
2297        if !self.can_split {
2298            return;
2299        }
2300
2301        let rect = event.bounds.size;
2302
2303        let size = event.bounds.size.width.min(event.bounds.size.height)
2304            * WorkspaceSettings::get_global(cx).drop_target_size;
2305
2306        let relative_cursor = Point::new(
2307            event.event.position.x - event.bounds.left(),
2308            event.event.position.y - event.bounds.top(),
2309        );
2310
2311        let direction = if relative_cursor.x < size
2312            || relative_cursor.x > rect.width - size
2313            || relative_cursor.y < size
2314            || relative_cursor.y > rect.height - size
2315        {
2316            [
2317                SplitDirection::Up,
2318                SplitDirection::Right,
2319                SplitDirection::Down,
2320                SplitDirection::Left,
2321            ]
2322            .iter()
2323            .min_by_key(|side| match side {
2324                SplitDirection::Up => relative_cursor.y,
2325                SplitDirection::Right => rect.width - relative_cursor.x,
2326                SplitDirection::Down => rect.height - relative_cursor.y,
2327                SplitDirection::Left => relative_cursor.x,
2328            })
2329            .cloned()
2330        } else {
2331            None
2332        };
2333
2334        if direction != self.drag_split_direction {
2335            self.drag_split_direction = direction;
2336        }
2337    }
2338
2339    fn handle_tab_drop(
2340        &mut self,
2341        dragged_tab: &DraggedTab,
2342        ix: usize,
2343        cx: &mut ViewContext<'_, Self>,
2344    ) {
2345        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2346            if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_tab, cx) {
2347                return;
2348            }
2349        }
2350        let mut to_pane = cx.view().clone();
2351        let split_direction = self.drag_split_direction;
2352        let item_id = dragged_tab.item.item_id();
2353        if let Some(preview_item_id) = self.preview_item_id {
2354            if item_id == preview_item_id {
2355                self.set_preview_item_id(None, cx);
2356            }
2357        }
2358
2359        let from_pane = dragged_tab.pane.clone();
2360        self.workspace
2361            .update(cx, |_, cx| {
2362                cx.defer(move |workspace, cx| {
2363                    if let Some(split_direction) = split_direction {
2364                        to_pane = workspace.split_pane(to_pane, split_direction, cx);
2365                    }
2366                    let old_ix = from_pane.read(cx).index_for_item_id(item_id);
2367                    if to_pane == from_pane {
2368                        if let Some(old_index) = old_ix {
2369                            to_pane.update(cx, |this, _| {
2370                                if old_index < this.pinned_tab_count
2371                                    && (ix == this.items.len() || ix > this.pinned_tab_count)
2372                                {
2373                                    this.pinned_tab_count -= 1;
2374                                } else if this.has_pinned_tabs()
2375                                    && old_index >= this.pinned_tab_count
2376                                    && ix < this.pinned_tab_count
2377                                {
2378                                    this.pinned_tab_count += 1;
2379                                }
2380                            });
2381                        }
2382                    } else {
2383                        to_pane.update(cx, |this, _| {
2384                            if this.has_pinned_tabs() && ix < this.pinned_tab_count {
2385                                this.pinned_tab_count += 1;
2386                            }
2387                        });
2388                        from_pane.update(cx, |this, _| {
2389                            if let Some(index) = old_ix {
2390                                if this.pinned_tab_count > index {
2391                                    this.pinned_tab_count -= 1;
2392                                }
2393                            }
2394                        })
2395                    }
2396                    move_item(&from_pane, &to_pane, item_id, ix, cx);
2397                });
2398            })
2399            .log_err();
2400    }
2401
2402    fn handle_dragged_selection_drop(
2403        &mut self,
2404        dragged_selection: &DraggedSelection,
2405        cx: &mut ViewContext<'_, Self>,
2406    ) {
2407        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2408            if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_selection, cx) {
2409                return;
2410            }
2411        }
2412        self.handle_project_entry_drop(&dragged_selection.active_selection.entry_id, cx);
2413    }
2414
2415    fn handle_project_entry_drop(
2416        &mut self,
2417        project_entry_id: &ProjectEntryId,
2418        cx: &mut ViewContext<'_, Self>,
2419    ) {
2420        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2421            if let ControlFlow::Break(()) = custom_drop_handle(self, project_entry_id, cx) {
2422                return;
2423            }
2424        }
2425        let mut to_pane = cx.view().clone();
2426        let split_direction = self.drag_split_direction;
2427        let project_entry_id = *project_entry_id;
2428        self.workspace
2429            .update(cx, |_, cx| {
2430                cx.defer(move |workspace, cx| {
2431                    if let Some(path) = workspace
2432                        .project()
2433                        .read(cx)
2434                        .path_for_entry(project_entry_id, cx)
2435                    {
2436                        let load_path_task = workspace.load_path(path, cx);
2437                        cx.spawn(|workspace, mut cx| async move {
2438                            if let Some((project_entry_id, build_item)) =
2439                                load_path_task.await.notify_async_err(&mut cx)
2440                            {
2441                                let (to_pane, new_item_handle) = workspace
2442                                    .update(&mut cx, |workspace, cx| {
2443                                        if let Some(split_direction) = split_direction {
2444                                            to_pane =
2445                                                workspace.split_pane(to_pane, split_direction, cx);
2446                                        }
2447                                        let new_item_handle = to_pane.update(cx, |pane, cx| {
2448                                            pane.open_item(
2449                                                project_entry_id,
2450                                                true,
2451                                                false,
2452                                                cx,
2453                                                build_item,
2454                                            )
2455                                        });
2456                                        (to_pane, new_item_handle)
2457                                    })
2458                                    .log_err()?;
2459                                to_pane
2460                                    .update(&mut cx, |this, cx| {
2461                                        let Some(index) = this.index_for_item(&*new_item_handle)
2462                                        else {
2463                                            return;
2464                                        };
2465                                        if !this.is_tab_pinned(index) {
2466                                            this.pin_tab_at(index, cx);
2467                                        }
2468                                    })
2469                                    .ok()?
2470                            }
2471                            Some(())
2472                        })
2473                        .detach();
2474                    };
2475                });
2476            })
2477            .log_err();
2478    }
2479
2480    fn handle_external_paths_drop(
2481        &mut self,
2482        paths: &ExternalPaths,
2483        cx: &mut ViewContext<'_, Self>,
2484    ) {
2485        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2486            if let ControlFlow::Break(()) = custom_drop_handle(self, paths, cx) {
2487                return;
2488            }
2489        }
2490        let mut to_pane = cx.view().clone();
2491        let mut split_direction = self.drag_split_direction;
2492        let paths = paths.paths().to_vec();
2493        let is_remote = self
2494            .workspace
2495            .update(cx, |workspace, cx| {
2496                if workspace.project().read(cx).is_via_collab() {
2497                    workspace.show_error(
2498                        &anyhow::anyhow!("Cannot drop files on a remote project"),
2499                        cx,
2500                    );
2501                    true
2502                } else {
2503                    false
2504                }
2505            })
2506            .unwrap_or(true);
2507        if is_remote {
2508            return;
2509        }
2510
2511        self.workspace
2512            .update(cx, |workspace, cx| {
2513                let fs = Arc::clone(workspace.project().read(cx).fs());
2514                cx.spawn(|workspace, mut cx| async move {
2515                    let mut is_file_checks = FuturesUnordered::new();
2516                    for path in &paths {
2517                        is_file_checks.push(fs.is_file(path))
2518                    }
2519                    let mut has_files_to_open = false;
2520                    while let Some(is_file) = is_file_checks.next().await {
2521                        if is_file {
2522                            has_files_to_open = true;
2523                            break;
2524                        }
2525                    }
2526                    drop(is_file_checks);
2527                    if !has_files_to_open {
2528                        split_direction = None;
2529                    }
2530
2531                    if let Ok(open_task) = workspace.update(&mut cx, |workspace, cx| {
2532                        if let Some(split_direction) = split_direction {
2533                            to_pane = workspace.split_pane(to_pane, split_direction, cx);
2534                        }
2535                        workspace.open_paths(
2536                            paths,
2537                            OpenVisible::OnlyDirectories,
2538                            Some(to_pane.downgrade()),
2539                            cx,
2540                        )
2541                    }) {
2542                        let opened_items: Vec<_> = open_task.await;
2543                        _ = workspace.update(&mut cx, |workspace, cx| {
2544                            for item in opened_items.into_iter().flatten() {
2545                                if let Err(e) = item {
2546                                    workspace.show_error(&e, cx);
2547                                }
2548                            }
2549                        });
2550                    }
2551                })
2552                .detach();
2553            })
2554            .log_err();
2555    }
2556
2557    pub fn display_nav_history_buttons(&mut self, display: Option<bool>) {
2558        self.display_nav_history_buttons = display;
2559    }
2560}
2561
2562impl FocusableView for Pane {
2563    fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
2564        self.focus_handle.clone()
2565    }
2566}
2567
2568impl Render for Pane {
2569    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2570        let mut key_context = KeyContext::new_with_defaults();
2571        key_context.add("Pane");
2572        if self.active_item().is_none() {
2573            key_context.add("EmptyPane");
2574        }
2575
2576        let should_display_tab_bar = self.should_display_tab_bar.clone();
2577        let display_tab_bar = should_display_tab_bar(cx);
2578        let is_local = self.project.read(cx).is_local();
2579
2580        v_flex()
2581            .key_context(key_context)
2582            .track_focus(&self.focus_handle(cx))
2583            .size_full()
2584            .flex_none()
2585            .overflow_hidden()
2586            .on_action(cx.listener(|pane, _: &AlternateFile, cx| {
2587                pane.alternate_file(cx);
2588            }))
2589            .on_action(cx.listener(|pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx)))
2590            .on_action(cx.listener(|pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx)))
2591            .on_action(cx.listener(|pane, _: &SplitHorizontal, cx| {
2592                pane.split(SplitDirection::horizontal(cx), cx)
2593            }))
2594            .on_action(cx.listener(|pane, _: &SplitVertical, cx| {
2595                pane.split(SplitDirection::vertical(cx), cx)
2596            }))
2597            .on_action(
2598                cx.listener(|pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx)),
2599            )
2600            .on_action(cx.listener(|pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx)))
2601            .on_action(cx.listener(|pane, _: &GoBack, cx| pane.navigate_backward(cx)))
2602            .on_action(cx.listener(|pane, _: &GoForward, cx| pane.navigate_forward(cx)))
2603            .on_action(cx.listener(|pane, _: &JoinIntoNext, cx| pane.join_into_next(cx)))
2604            .on_action(cx.listener(|pane, _: &JoinAll, cx| pane.join_all(cx)))
2605            .on_action(cx.listener(Pane::toggle_zoom))
2606            .on_action(cx.listener(|pane: &mut Pane, action: &ActivateItem, cx| {
2607                pane.activate_item(action.0, true, true, cx);
2608            }))
2609            .on_action(cx.listener(|pane: &mut Pane, _: &ActivateLastItem, cx| {
2610                pane.activate_item(pane.items.len() - 1, true, true, cx);
2611            }))
2612            .on_action(cx.listener(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
2613                pane.activate_prev_item(true, cx);
2614            }))
2615            .on_action(cx.listener(|pane: &mut Pane, _: &ActivateNextItem, cx| {
2616                pane.activate_next_item(true, cx);
2617            }))
2618            .on_action(cx.listener(|pane, _: &SwapItemLeft, cx| pane.swap_item_left(cx)))
2619            .on_action(cx.listener(|pane, _: &SwapItemRight, cx| pane.swap_item_right(cx)))
2620            .on_action(cx.listener(|pane, action, cx| {
2621                pane.toggle_pin_tab(action, cx);
2622            }))
2623            .when(PreviewTabsSettings::get_global(cx).enabled, |this| {
2624                this.on_action(cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, cx| {
2625                    if let Some(active_item_id) = pane.active_item().map(|i| i.item_id()) {
2626                        if pane.is_active_preview_item(active_item_id) {
2627                            pane.set_preview_item_id(None, cx);
2628                        } else {
2629                            pane.set_preview_item_id(Some(active_item_id), cx);
2630                        }
2631                    }
2632                }))
2633            })
2634            .on_action(
2635                cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
2636                    if let Some(task) = pane.close_active_item(action, cx) {
2637                        task.detach_and_log_err(cx)
2638                    }
2639                }),
2640            )
2641            .on_action(
2642                cx.listener(|pane: &mut Self, action: &CloseInactiveItems, cx| {
2643                    if let Some(task) = pane.close_inactive_items(action, cx) {
2644                        task.detach_and_log_err(cx)
2645                    }
2646                }),
2647            )
2648            .on_action(
2649                cx.listener(|pane: &mut Self, action: &CloseCleanItems, cx| {
2650                    if let Some(task) = pane.close_clean_items(action, cx) {
2651                        task.detach_and_log_err(cx)
2652                    }
2653                }),
2654            )
2655            .on_action(
2656                cx.listener(|pane: &mut Self, action: &CloseItemsToTheLeft, cx| {
2657                    if let Some(task) = pane.close_items_to_the_left(action, cx) {
2658                        task.detach_and_log_err(cx)
2659                    }
2660                }),
2661            )
2662            .on_action(
2663                cx.listener(|pane: &mut Self, action: &CloseItemsToTheRight, cx| {
2664                    if let Some(task) = pane.close_items_to_the_right(action, cx) {
2665                        task.detach_and_log_err(cx)
2666                    }
2667                }),
2668            )
2669            .on_action(cx.listener(|pane: &mut Self, action: &CloseAllItems, cx| {
2670                if let Some(task) = pane.close_all_items(action, cx) {
2671                    task.detach_and_log_err(cx)
2672                }
2673            }))
2674            .on_action(
2675                cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
2676                    if let Some(task) = pane.close_active_item(action, cx) {
2677                        task.detach_and_log_err(cx)
2678                    }
2679                }),
2680            )
2681            .on_action(
2682                cx.listener(|pane: &mut Self, action: &RevealInProjectPanel, cx| {
2683                    let entry_id = action
2684                        .entry_id
2685                        .map(ProjectEntryId::from_proto)
2686                        .or_else(|| pane.active_item()?.project_entry_ids(cx).first().copied());
2687                    if let Some(entry_id) = entry_id {
2688                        pane.project.update(cx, |_, cx| {
2689                            cx.emit(project::Event::RevealInProjectPanel(entry_id))
2690                        });
2691                    }
2692                }),
2693            )
2694            .when(self.active_item().is_some() && display_tab_bar, |pane| {
2695                pane.child(self.render_tab_bar(cx))
2696            })
2697            .child({
2698                let has_worktrees = self.project.read(cx).worktrees(cx).next().is_some();
2699                // main content
2700                div()
2701                    .flex_1()
2702                    .relative()
2703                    .group("")
2704                    .overflow_hidden()
2705                    .on_drag_move::<DraggedTab>(cx.listener(Self::handle_drag_move))
2706                    .on_drag_move::<DraggedSelection>(cx.listener(Self::handle_drag_move))
2707                    .when(is_local, |div| {
2708                        div.on_drag_move::<ExternalPaths>(cx.listener(Self::handle_drag_move))
2709                    })
2710                    .map(|div| {
2711                        if let Some(item) = self.active_item() {
2712                            div.v_flex()
2713                                .size_full()
2714                                .overflow_hidden()
2715                                .child(self.toolbar.clone())
2716                                .child(item.to_any())
2717                        } else {
2718                            let placeholder = div.h_flex().size_full().justify_center();
2719                            if has_worktrees {
2720                                placeholder
2721                            } else {
2722                                placeholder.child(
2723                                    Label::new("Open a file or project to get started.")
2724                                        .color(Color::Muted),
2725                                )
2726                            }
2727                        }
2728                    })
2729                    .child(
2730                        // drag target
2731                        div()
2732                            .invisible()
2733                            .absolute()
2734                            .bg(cx.theme().colors().drop_target_background)
2735                            .group_drag_over::<DraggedTab>("", |style| style.visible())
2736                            .group_drag_over::<DraggedSelection>("", |style| style.visible())
2737                            .when(is_local, |div| {
2738                                div.group_drag_over::<ExternalPaths>("", |style| style.visible())
2739                            })
2740                            .when_some(self.can_drop_predicate.clone(), |this, p| {
2741                                this.can_drop(move |a, cx| p(a, cx))
2742                            })
2743                            .on_drop(cx.listener(move |this, dragged_tab, cx| {
2744                                this.handle_tab_drop(dragged_tab, this.active_item_index(), cx)
2745                            }))
2746                            .on_drop(cx.listener(move |this, selection: &DraggedSelection, cx| {
2747                                this.handle_dragged_selection_drop(selection, cx)
2748                            }))
2749                            .on_drop(cx.listener(move |this, paths, cx| {
2750                                this.handle_external_paths_drop(paths, cx)
2751                            }))
2752                            .map(|div| {
2753                                let size = DefiniteLength::Fraction(0.5);
2754                                match self.drag_split_direction {
2755                                    None => div.top_0().right_0().bottom_0().left_0(),
2756                                    Some(SplitDirection::Up) => {
2757                                        div.top_0().left_0().right_0().h(size)
2758                                    }
2759                                    Some(SplitDirection::Down) => {
2760                                        div.left_0().bottom_0().right_0().h(size)
2761                                    }
2762                                    Some(SplitDirection::Left) => {
2763                                        div.top_0().left_0().bottom_0().w(size)
2764                                    }
2765                                    Some(SplitDirection::Right) => {
2766                                        div.top_0().bottom_0().right_0().w(size)
2767                                    }
2768                                }
2769                            }),
2770                    )
2771            })
2772            .on_mouse_down(
2773                MouseButton::Navigate(NavigationDirection::Back),
2774                cx.listener(|pane, _, cx| {
2775                    if let Some(workspace) = pane.workspace.upgrade() {
2776                        let pane = cx.view().downgrade();
2777                        cx.window_context().defer(move |cx| {
2778                            workspace.update(cx, |workspace, cx| {
2779                                workspace.go_back(pane, cx).detach_and_log_err(cx)
2780                            })
2781                        })
2782                    }
2783                }),
2784            )
2785            .on_mouse_down(
2786                MouseButton::Navigate(NavigationDirection::Forward),
2787                cx.listener(|pane, _, cx| {
2788                    if let Some(workspace) = pane.workspace.upgrade() {
2789                        let pane = cx.view().downgrade();
2790                        cx.window_context().defer(move |cx| {
2791                            workspace.update(cx, |workspace, cx| {
2792                                workspace.go_forward(pane, cx).detach_and_log_err(cx)
2793                            })
2794                        })
2795                    }
2796                }),
2797            )
2798    }
2799}
2800
2801impl ItemNavHistory {
2802    pub fn push<D: 'static + Send + Any>(&mut self, data: Option<D>, cx: &mut WindowContext) {
2803        self.history
2804            .push(data, self.item.clone(), self.is_preview, cx);
2805    }
2806
2807    pub fn pop_backward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
2808        self.history.pop(NavigationMode::GoingBack, cx)
2809    }
2810
2811    pub fn pop_forward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
2812        self.history.pop(NavigationMode::GoingForward, cx)
2813    }
2814}
2815
2816impl NavHistory {
2817    pub fn for_each_entry(
2818        &self,
2819        cx: &AppContext,
2820        mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
2821    ) {
2822        let borrowed_history = self.0.lock();
2823        borrowed_history
2824            .forward_stack
2825            .iter()
2826            .chain(borrowed_history.backward_stack.iter())
2827            .chain(borrowed_history.closed_stack.iter())
2828            .for_each(|entry| {
2829                if let Some(project_and_abs_path) =
2830                    borrowed_history.paths_by_item.get(&entry.item.id())
2831                {
2832                    f(entry, project_and_abs_path.clone());
2833                } else if let Some(item) = entry.item.upgrade() {
2834                    if let Some(path) = item.project_path(cx) {
2835                        f(entry, (path, None));
2836                    }
2837                }
2838            })
2839    }
2840
2841    pub fn set_mode(&mut self, mode: NavigationMode) {
2842        self.0.lock().mode = mode;
2843    }
2844
2845    pub fn mode(&self) -> NavigationMode {
2846        self.0.lock().mode
2847    }
2848
2849    pub fn disable(&mut self) {
2850        self.0.lock().mode = NavigationMode::Disabled;
2851    }
2852
2853    pub fn enable(&mut self) {
2854        self.0.lock().mode = NavigationMode::Normal;
2855    }
2856
2857    pub fn pop(&mut self, mode: NavigationMode, cx: &mut WindowContext) -> Option<NavigationEntry> {
2858        let mut state = self.0.lock();
2859        let entry = match mode {
2860            NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
2861                return None
2862            }
2863            NavigationMode::GoingBack => &mut state.backward_stack,
2864            NavigationMode::GoingForward => &mut state.forward_stack,
2865            NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
2866        }
2867        .pop_back();
2868        if entry.is_some() {
2869            state.did_update(cx);
2870        }
2871        entry
2872    }
2873
2874    pub fn push<D: 'static + Send + Any>(
2875        &mut self,
2876        data: Option<D>,
2877        item: Arc<dyn WeakItemHandle>,
2878        is_preview: bool,
2879        cx: &mut WindowContext,
2880    ) {
2881        let state = &mut *self.0.lock();
2882        match state.mode {
2883            NavigationMode::Disabled => {}
2884            NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
2885                if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2886                    state.backward_stack.pop_front();
2887                }
2888                state.backward_stack.push_back(NavigationEntry {
2889                    item,
2890                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2891                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2892                    is_preview,
2893                });
2894                state.forward_stack.clear();
2895            }
2896            NavigationMode::GoingBack => {
2897                if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2898                    state.forward_stack.pop_front();
2899                }
2900                state.forward_stack.push_back(NavigationEntry {
2901                    item,
2902                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2903                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2904                    is_preview,
2905                });
2906            }
2907            NavigationMode::GoingForward => {
2908                if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2909                    state.backward_stack.pop_front();
2910                }
2911                state.backward_stack.push_back(NavigationEntry {
2912                    item,
2913                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2914                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2915                    is_preview,
2916                });
2917            }
2918            NavigationMode::ClosingItem => {
2919                if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2920                    state.closed_stack.pop_front();
2921                }
2922                state.closed_stack.push_back(NavigationEntry {
2923                    item,
2924                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2925                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2926                    is_preview,
2927                });
2928            }
2929        }
2930        state.did_update(cx);
2931    }
2932
2933    pub fn remove_item(&mut self, item_id: EntityId) {
2934        let mut state = self.0.lock();
2935        state.paths_by_item.remove(&item_id);
2936        state
2937            .backward_stack
2938            .retain(|entry| entry.item.id() != item_id);
2939        state
2940            .forward_stack
2941            .retain(|entry| entry.item.id() != item_id);
2942        state
2943            .closed_stack
2944            .retain(|entry| entry.item.id() != item_id);
2945    }
2946
2947    pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option<PathBuf>)> {
2948        self.0.lock().paths_by_item.get(&item_id).cloned()
2949    }
2950}
2951
2952impl NavHistoryState {
2953    pub fn did_update(&self, cx: &mut WindowContext) {
2954        if let Some(pane) = self.pane.upgrade() {
2955            cx.defer(move |cx| {
2956                pane.update(cx, |pane, cx| pane.history_updated(cx));
2957            });
2958        }
2959    }
2960}
2961
2962fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
2963    let path = buffer_path
2964        .as_ref()
2965        .and_then(|p| {
2966            p.path
2967                .to_str()
2968                .and_then(|s| if s.is_empty() { None } else { Some(s) })
2969        })
2970        .unwrap_or("This buffer");
2971    let path = truncate_and_remove_front(path, 80);
2972    format!("{path} contains unsaved edits. Do you want to save it?")
2973}
2974
2975pub fn tab_details(items: &[Box<dyn ItemHandle>], cx: &AppContext) -> Vec<usize> {
2976    let mut tab_details = items.iter().map(|_| 0).collect::<Vec<_>>();
2977    let mut tab_descriptions = HashMap::default();
2978    let mut done = false;
2979    while !done {
2980        done = true;
2981
2982        // Store item indices by their tab description.
2983        for (ix, (item, detail)) in items.iter().zip(&tab_details).enumerate() {
2984            if let Some(description) = item.tab_description(*detail, cx) {
2985                if *detail == 0
2986                    || Some(&description) != item.tab_description(detail - 1, cx).as_ref()
2987                {
2988                    tab_descriptions
2989                        .entry(description)
2990                        .or_insert(Vec::new())
2991                        .push(ix);
2992                }
2993            }
2994        }
2995
2996        // If two or more items have the same tab description, increase their level
2997        // of detail and try again.
2998        for (_, item_ixs) in tab_descriptions.drain() {
2999            if item_ixs.len() > 1 {
3000                done = false;
3001                for ix in item_ixs {
3002                    tab_details[ix] += 1;
3003                }
3004            }
3005        }
3006    }
3007
3008    tab_details
3009}
3010
3011pub fn render_item_indicator(item: Box<dyn ItemHandle>, cx: &WindowContext) -> Option<Indicator> {
3012    maybe!({
3013        let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
3014            (true, _) => Color::Warning,
3015            (_, true) => Color::Accent,
3016            (false, false) => return None,
3017        };
3018
3019        Some(Indicator::dot().color(indicator_color))
3020    })
3021}
3022
3023impl Render for DraggedTab {
3024    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3025        let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
3026        let label = self.item.tab_content(
3027            TabContentParams {
3028                detail: Some(self.detail),
3029                selected: false,
3030                preview: false,
3031            },
3032            cx,
3033        );
3034        Tab::new("")
3035            .selected(self.is_active)
3036            .child(label)
3037            .render(cx)
3038            .font(ui_font)
3039    }
3040}
3041
3042#[cfg(test)]
3043mod tests {
3044    use super::*;
3045    use crate::item::test::{TestItem, TestProjectItem};
3046    use gpui::{TestAppContext, VisualTestContext};
3047    use project::FakeFs;
3048    use settings::SettingsStore;
3049    use theme::LoadThemes;
3050
3051    #[gpui::test]
3052    async fn test_remove_active_empty(cx: &mut TestAppContext) {
3053        init_test(cx);
3054        let fs = FakeFs::new(cx.executor());
3055
3056        let project = Project::test(fs, None, cx).await;
3057        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3058        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3059
3060        pane.update(cx, |pane, cx| {
3061            assert!(pane
3062                .close_active_item(&CloseActiveItem { save_intent: None }, cx)
3063                .is_none())
3064        });
3065    }
3066
3067    #[gpui::test]
3068    async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
3069        init_test(cx);
3070        let fs = FakeFs::new(cx.executor());
3071
3072        let project = Project::test(fs, None, cx).await;
3073        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3074        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3075
3076        // 1. Add with a destination index
3077        //   a. Add before the active item
3078        set_labeled_items(&pane, ["A", "B*", "C"], cx);
3079        pane.update(cx, |pane, cx| {
3080            pane.add_item(
3081                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3082                false,
3083                false,
3084                Some(0),
3085                cx,
3086            );
3087        });
3088        assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
3089
3090        //   b. Add after the active item
3091        set_labeled_items(&pane, ["A", "B*", "C"], cx);
3092        pane.update(cx, |pane, cx| {
3093            pane.add_item(
3094                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3095                false,
3096                false,
3097                Some(2),
3098                cx,
3099            );
3100        });
3101        assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3102
3103        //   c. Add at the end of the item list (including off the length)
3104        set_labeled_items(&pane, ["A", "B*", "C"], cx);
3105        pane.update(cx, |pane, cx| {
3106            pane.add_item(
3107                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3108                false,
3109                false,
3110                Some(5),
3111                cx,
3112            );
3113        });
3114        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3115
3116        // 2. Add without a destination index
3117        //   a. Add with active item at the start of the item list
3118        set_labeled_items(&pane, ["A*", "B", "C"], cx);
3119        pane.update(cx, |pane, cx| {
3120            pane.add_item(
3121                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3122                false,
3123                false,
3124                None,
3125                cx,
3126            );
3127        });
3128        set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
3129
3130        //   b. Add with active item at the end of the item list
3131        set_labeled_items(&pane, ["A", "B", "C*"], cx);
3132        pane.update(cx, |pane, cx| {
3133            pane.add_item(
3134                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3135                false,
3136                false,
3137                None,
3138                cx,
3139            );
3140        });
3141        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3142    }
3143
3144    #[gpui::test]
3145    async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
3146        init_test(cx);
3147        let fs = FakeFs::new(cx.executor());
3148
3149        let project = Project::test(fs, None, cx).await;
3150        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3151        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3152
3153        // 1. Add with a destination index
3154        //   1a. Add before the active item
3155        let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3156        pane.update(cx, |pane, cx| {
3157            pane.add_item(d, false, false, Some(0), cx);
3158        });
3159        assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
3160
3161        //   1b. Add after the active item
3162        let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3163        pane.update(cx, |pane, cx| {
3164            pane.add_item(d, false, false, Some(2), cx);
3165        });
3166        assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3167
3168        //   1c. Add at the end of the item list (including off the length)
3169        let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3170        pane.update(cx, |pane, cx| {
3171            pane.add_item(a, false, false, Some(5), cx);
3172        });
3173        assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3174
3175        //   1d. Add same item to active index
3176        let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3177        pane.update(cx, |pane, cx| {
3178            pane.add_item(b, false, false, Some(1), cx);
3179        });
3180        assert_item_labels(&pane, ["A", "B*", "C"], cx);
3181
3182        //   1e. Add item to index after same item in last position
3183        let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3184        pane.update(cx, |pane, cx| {
3185            pane.add_item(c, false, false, Some(2), cx);
3186        });
3187        assert_item_labels(&pane, ["A", "B", "C*"], cx);
3188
3189        // 2. Add without a destination index
3190        //   2a. Add with active item at the start of the item list
3191        let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
3192        pane.update(cx, |pane, cx| {
3193            pane.add_item(d, false, false, None, cx);
3194        });
3195        assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
3196
3197        //   2b. Add with active item at the end of the item list
3198        let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
3199        pane.update(cx, |pane, cx| {
3200            pane.add_item(a, false, false, None, cx);
3201        });
3202        assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3203
3204        //   2c. Add active item to active item at end of list
3205        let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
3206        pane.update(cx, |pane, cx| {
3207            pane.add_item(c, false, false, None, cx);
3208        });
3209        assert_item_labels(&pane, ["A", "B", "C*"], cx);
3210
3211        //   2d. Add active item to active item at start of list
3212        let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
3213        pane.update(cx, |pane, cx| {
3214            pane.add_item(a, false, false, None, cx);
3215        });
3216        assert_item_labels(&pane, ["A*", "B", "C"], cx);
3217    }
3218
3219    #[gpui::test]
3220    async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
3221        init_test(cx);
3222        let fs = FakeFs::new(cx.executor());
3223
3224        let project = Project::test(fs, None, cx).await;
3225        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3226        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3227
3228        // singleton view
3229        pane.update(cx, |pane, cx| {
3230            pane.add_item(
3231                Box::new(cx.new_view(|cx| {
3232                    TestItem::new(cx)
3233                        .with_singleton(true)
3234                        .with_label("buffer 1")
3235                        .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
3236                })),
3237                false,
3238                false,
3239                None,
3240                cx,
3241            );
3242        });
3243        assert_item_labels(&pane, ["buffer 1*"], cx);
3244
3245        // new singleton view with the same project entry
3246        pane.update(cx, |pane, cx| {
3247            pane.add_item(
3248                Box::new(cx.new_view(|cx| {
3249                    TestItem::new(cx)
3250                        .with_singleton(true)
3251                        .with_label("buffer 1")
3252                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3253                })),
3254                false,
3255                false,
3256                None,
3257                cx,
3258            );
3259        });
3260        assert_item_labels(&pane, ["buffer 1*"], cx);
3261
3262        // new singleton view with different project entry
3263        pane.update(cx, |pane, cx| {
3264            pane.add_item(
3265                Box::new(cx.new_view(|cx| {
3266                    TestItem::new(cx)
3267                        .with_singleton(true)
3268                        .with_label("buffer 2")
3269                        .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
3270                })),
3271                false,
3272                false,
3273                None,
3274                cx,
3275            );
3276        });
3277        assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
3278
3279        // new multibuffer view with the same project entry
3280        pane.update(cx, |pane, cx| {
3281            pane.add_item(
3282                Box::new(cx.new_view(|cx| {
3283                    TestItem::new(cx)
3284                        .with_singleton(false)
3285                        .with_label("multibuffer 1")
3286                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3287                })),
3288                false,
3289                false,
3290                None,
3291                cx,
3292            );
3293        });
3294        assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
3295
3296        // another multibuffer view with the same project entry
3297        pane.update(cx, |pane, cx| {
3298            pane.add_item(
3299                Box::new(cx.new_view(|cx| {
3300                    TestItem::new(cx)
3301                        .with_singleton(false)
3302                        .with_label("multibuffer 1b")
3303                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3304                })),
3305                false,
3306                false,
3307                None,
3308                cx,
3309            );
3310        });
3311        assert_item_labels(
3312            &pane,
3313            ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
3314            cx,
3315        );
3316    }
3317
3318    #[gpui::test]
3319    async fn test_remove_item_ordering_history(cx: &mut TestAppContext) {
3320        init_test(cx);
3321        let fs = FakeFs::new(cx.executor());
3322
3323        let project = Project::test(fs, None, cx).await;
3324        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3325        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3326
3327        add_labeled_item(&pane, "A", false, cx);
3328        add_labeled_item(&pane, "B", false, cx);
3329        add_labeled_item(&pane, "C", false, cx);
3330        add_labeled_item(&pane, "D", false, cx);
3331        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3332
3333        pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
3334        add_labeled_item(&pane, "1", false, cx);
3335        assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
3336
3337        pane.update(cx, |pane, cx| {
3338            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3339        })
3340        .unwrap()
3341        .await
3342        .unwrap();
3343        assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
3344
3345        pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
3346        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3347
3348        pane.update(cx, |pane, cx| {
3349            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3350        })
3351        .unwrap()
3352        .await
3353        .unwrap();
3354        assert_item_labels(&pane, ["A", "B*", "C"], cx);
3355
3356        pane.update(cx, |pane, cx| {
3357            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3358        })
3359        .unwrap()
3360        .await
3361        .unwrap();
3362        assert_item_labels(&pane, ["A", "C*"], cx);
3363
3364        pane.update(cx, |pane, cx| {
3365            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3366        })
3367        .unwrap()
3368        .await
3369        .unwrap();
3370        assert_item_labels(&pane, ["A*"], cx);
3371    }
3372
3373    #[gpui::test]
3374    async fn test_remove_item_ordering_neighbour(cx: &mut TestAppContext) {
3375        init_test(cx);
3376        cx.update_global::<SettingsStore, ()>(|s, cx| {
3377            s.update_user_settings::<ItemSettings>(cx, |s| {
3378                s.activate_on_close = Some(ActivateOnClose::Neighbour);
3379            });
3380        });
3381        let fs = FakeFs::new(cx.executor());
3382
3383        let project = Project::test(fs, None, cx).await;
3384        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3385        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3386
3387        add_labeled_item(&pane, "A", false, cx);
3388        add_labeled_item(&pane, "B", false, cx);
3389        add_labeled_item(&pane, "C", false, cx);
3390        add_labeled_item(&pane, "D", false, cx);
3391        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3392
3393        pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
3394        add_labeled_item(&pane, "1", false, cx);
3395        assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
3396
3397        pane.update(cx, |pane, cx| {
3398            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3399        })
3400        .unwrap()
3401        .await
3402        .unwrap();
3403        assert_item_labels(&pane, ["A", "B", "C*", "D"], cx);
3404
3405        pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
3406        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3407
3408        pane.update(cx, |pane, cx| {
3409            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3410        })
3411        .unwrap()
3412        .await
3413        .unwrap();
3414        assert_item_labels(&pane, ["A", "B", "C*"], cx);
3415
3416        pane.update(cx, |pane, cx| {
3417            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3418        })
3419        .unwrap()
3420        .await
3421        .unwrap();
3422        assert_item_labels(&pane, ["A", "B*"], cx);
3423
3424        pane.update(cx, |pane, cx| {
3425            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3426        })
3427        .unwrap()
3428        .await
3429        .unwrap();
3430        assert_item_labels(&pane, ["A*"], cx);
3431    }
3432
3433    #[gpui::test]
3434    async fn test_close_inactive_items(cx: &mut TestAppContext) {
3435        init_test(cx);
3436        let fs = FakeFs::new(cx.executor());
3437
3438        let project = Project::test(fs, None, cx).await;
3439        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3440        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3441
3442        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
3443
3444        pane.update(cx, |pane, cx| {
3445            pane.close_inactive_items(&CloseInactiveItems { save_intent: None }, cx)
3446        })
3447        .unwrap()
3448        .await
3449        .unwrap();
3450        assert_item_labels(&pane, ["C*"], cx);
3451    }
3452
3453    #[gpui::test]
3454    async fn test_close_clean_items(cx: &mut TestAppContext) {
3455        init_test(cx);
3456        let fs = FakeFs::new(cx.executor());
3457
3458        let project = Project::test(fs, None, cx).await;
3459        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3460        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3461
3462        add_labeled_item(&pane, "A", true, cx);
3463        add_labeled_item(&pane, "B", false, cx);
3464        add_labeled_item(&pane, "C", true, cx);
3465        add_labeled_item(&pane, "D", false, cx);
3466        add_labeled_item(&pane, "E", false, cx);
3467        assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
3468
3469        pane.update(cx, |pane, cx| pane.close_clean_items(&CloseCleanItems, cx))
3470            .unwrap()
3471            .await
3472            .unwrap();
3473        assert_item_labels(&pane, ["A^", "C*^"], cx);
3474    }
3475
3476    #[gpui::test]
3477    async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
3478        init_test(cx);
3479        let fs = FakeFs::new(cx.executor());
3480
3481        let project = Project::test(fs, None, cx).await;
3482        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3483        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3484
3485        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
3486
3487        pane.update(cx, |pane, cx| {
3488            pane.close_items_to_the_left(&CloseItemsToTheLeft, cx)
3489        })
3490        .unwrap()
3491        .await
3492        .unwrap();
3493        assert_item_labels(&pane, ["C*", "D", "E"], cx);
3494    }
3495
3496    #[gpui::test]
3497    async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
3498        init_test(cx);
3499        let fs = FakeFs::new(cx.executor());
3500
3501        let project = Project::test(fs, None, cx).await;
3502        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3503        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3504
3505        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
3506
3507        pane.update(cx, |pane, cx| {
3508            pane.close_items_to_the_right(&CloseItemsToTheRight, cx)
3509        })
3510        .unwrap()
3511        .await
3512        .unwrap();
3513        assert_item_labels(&pane, ["A", "B", "C*"], cx);
3514    }
3515
3516    #[gpui::test]
3517    async fn test_close_all_items(cx: &mut TestAppContext) {
3518        init_test(cx);
3519        let fs = FakeFs::new(cx.executor());
3520
3521        let project = Project::test(fs, None, cx).await;
3522        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3523        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3524
3525        add_labeled_item(&pane, "A", false, cx);
3526        add_labeled_item(&pane, "B", false, cx);
3527        add_labeled_item(&pane, "C", false, cx);
3528        assert_item_labels(&pane, ["A", "B", "C*"], cx);
3529
3530        pane.update(cx, |pane, cx| {
3531            pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
3532        })
3533        .unwrap()
3534        .await
3535        .unwrap();
3536        assert_item_labels(&pane, [], cx);
3537
3538        add_labeled_item(&pane, "A", true, cx);
3539        add_labeled_item(&pane, "B", true, cx);
3540        add_labeled_item(&pane, "C", true, cx);
3541        assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
3542
3543        let save = pane
3544            .update(cx, |pane, cx| {
3545                pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
3546            })
3547            .unwrap();
3548
3549        cx.executor().run_until_parked();
3550        cx.simulate_prompt_answer(2);
3551        save.await.unwrap();
3552        assert_item_labels(&pane, [], cx);
3553    }
3554
3555    fn init_test(cx: &mut TestAppContext) {
3556        cx.update(|cx| {
3557            let settings_store = SettingsStore::test(cx);
3558            cx.set_global(settings_store);
3559            theme::init(LoadThemes::JustBase, cx);
3560            crate::init_settings(cx);
3561            Project::init_settings(cx);
3562        });
3563    }
3564
3565    fn add_labeled_item(
3566        pane: &View<Pane>,
3567        label: &str,
3568        is_dirty: bool,
3569        cx: &mut VisualTestContext,
3570    ) -> Box<View<TestItem>> {
3571        pane.update(cx, |pane, cx| {
3572            let labeled_item = Box::new(
3573                cx.new_view(|cx| TestItem::new(cx).with_label(label).with_dirty(is_dirty)),
3574            );
3575            pane.add_item(labeled_item.clone(), false, false, None, cx);
3576            labeled_item
3577        })
3578    }
3579
3580    fn set_labeled_items<const COUNT: usize>(
3581        pane: &View<Pane>,
3582        labels: [&str; COUNT],
3583        cx: &mut VisualTestContext,
3584    ) -> [Box<View<TestItem>>; COUNT] {
3585        pane.update(cx, |pane, cx| {
3586            pane.items.clear();
3587            let mut active_item_index = 0;
3588
3589            let mut index = 0;
3590            let items = labels.map(|mut label| {
3591                if label.ends_with('*') {
3592                    label = label.trim_end_matches('*');
3593                    active_item_index = index;
3594                }
3595
3596                let labeled_item = Box::new(cx.new_view(|cx| TestItem::new(cx).with_label(label)));
3597                pane.add_item(labeled_item.clone(), false, false, None, cx);
3598                index += 1;
3599                labeled_item
3600            });
3601
3602            pane.activate_item(active_item_index, false, false, cx);
3603
3604            items
3605        })
3606    }
3607
3608    // Assert the item label, with the active item label suffixed with a '*'
3609    fn assert_item_labels<const COUNT: usize>(
3610        pane: &View<Pane>,
3611        expected_states: [&str; COUNT],
3612        cx: &mut VisualTestContext,
3613    ) {
3614        pane.update(cx, |pane, cx| {
3615            let actual_states = pane
3616                .items
3617                .iter()
3618                .enumerate()
3619                .map(|(ix, item)| {
3620                    let mut state = item
3621                        .to_any()
3622                        .downcast::<TestItem>()
3623                        .unwrap()
3624                        .read(cx)
3625                        .label
3626                        .clone();
3627                    if ix == pane.active_item_index {
3628                        state.push('*');
3629                    }
3630                    if item.is_dirty(cx) {
3631                        state.push('^');
3632                    }
3633                    state
3634                })
3635                .collect::<Vec<_>>();
3636
3637            assert_eq!(
3638                actual_states, expected_states,
3639                "pane items do not match expectation"
3640            );
3641        })
3642    }
3643}