pane.rs

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