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