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