pane.rs

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