pane.rs

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