pane.rs

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