pane.rs

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