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