pane.rs

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