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