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