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