pane.rs

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