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