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        let active_item_id = self.active_item().map(|item| item.item_id());
1161
1162        items_to_close.sort_by_key(|item| {
1163            // Put the currently active item at the end, because if the currently active item is not closed last
1164            // closing the currently active item will cause the focus to switch to another item
1165            // This will cause Zed to expand the content of the currently active item
1166            active_item_id.filter(|&id| id == item.item_id()).is_some()
1167              // If a buffer is open both in a singleton editor and in a multibuffer, make sure
1168              // to focus the singleton buffer when prompting to save that buffer, as opposed
1169              // to focusing the multibuffer, because this gives the user a more clear idea
1170              // of what content they would be saving.
1171              || !item.is_singleton(cx)
1172        });
1173
1174        let workspace = self.workspace.clone();
1175        cx.spawn(|pane, mut cx| async move {
1176            if save_intent == SaveIntent::Close && dirty_items.len() > 1 {
1177                let answer = pane.update(&mut cx, |_, cx| {
1178                    let (prompt, detail) =
1179                        Self::file_names_for_prompt(&mut dirty_items.iter(), dirty_items.len(), cx);
1180                    cx.prompt(
1181                        PromptLevel::Warning,
1182                        &prompt,
1183                        Some(&detail),
1184                        &["Save all", "Discard all", "Cancel"],
1185                    )
1186                })?;
1187                match answer.await {
1188                    Ok(0) => save_intent = SaveIntent::SaveAll,
1189                    Ok(1) => save_intent = SaveIntent::Skip,
1190                    _ => {}
1191                }
1192            }
1193            let mut saved_project_items_ids = HashSet::default();
1194            for item in items_to_close.clone() {
1195                // Find the item's current index and its set of project item models. Avoid
1196                // storing these in advance, in case they have changed since this task
1197                // was started.
1198                let (item_ix, mut project_item_ids) = pane.update(&mut cx, |pane, cx| {
1199                    (pane.index_for_item(&*item), item.project_item_model_ids(cx))
1200                })?;
1201                let item_ix = if let Some(ix) = item_ix {
1202                    ix
1203                } else {
1204                    continue;
1205                };
1206
1207                // Check if this view has any project items that are not open anywhere else
1208                // in the workspace, AND that the user has not already been prompted to save.
1209                // If there are any such project entries, prompt the user to save this item.
1210                let project = workspace.update(&mut cx, |workspace, cx| {
1211                    for item in workspace.items(cx) {
1212                        if !items_to_close
1213                            .iter()
1214                            .any(|item_to_close| item_to_close.item_id() == item.item_id())
1215                        {
1216                            let other_project_item_ids = item.project_item_model_ids(cx);
1217                            project_item_ids.retain(|id| !other_project_item_ids.contains(id));
1218                        }
1219                    }
1220                    workspace.project().clone()
1221                })?;
1222                let should_save = project_item_ids
1223                    .iter()
1224                    .any(|id| saved_project_items_ids.insert(*id));
1225
1226                if should_save
1227                    && !Self::save_item(
1228                        project.clone(),
1229                        &pane,
1230                        item_ix,
1231                        &*item,
1232                        save_intent,
1233                        &mut cx,
1234                    )
1235                    .await?
1236                {
1237                    break;
1238                }
1239
1240                // Remove the item from the pane.
1241                pane.update(&mut cx, |pane, cx| {
1242                    if let Some(item_ix) = pane
1243                        .items
1244                        .iter()
1245                        .position(|i| i.item_id() == item.item_id())
1246                    {
1247                        pane.remove_item(item_ix, false, true, cx);
1248                    }
1249                })
1250                .ok();
1251            }
1252
1253            pane.update(&mut cx, |_, cx| cx.notify()).ok();
1254            Ok(())
1255        })
1256    }
1257
1258    pub fn remove_item(
1259        &mut self,
1260        item_index: usize,
1261        activate_pane: bool,
1262        close_pane_if_empty: bool,
1263        cx: &mut ViewContext<Self>,
1264    ) {
1265        self.activation_history
1266            .retain(|entry| entry.entity_id != self.items[item_index].item_id());
1267
1268        if item_index == self.active_item_index {
1269            let index_to_activate = self
1270                .activation_history
1271                .pop()
1272                .and_then(|last_activated_item| {
1273                    self.items.iter().enumerate().find_map(|(index, item)| {
1274                        (item.item_id() == last_activated_item.entity_id).then_some(index)
1275                    })
1276                })
1277                // We didn't have a valid activation history entry, so fallback
1278                // to activating the item to the left
1279                .unwrap_or_else(|| item_index.min(self.items.len()).saturating_sub(1));
1280
1281            let should_activate = activate_pane || self.has_focus(cx);
1282            if self.items.len() == 1 && should_activate {
1283                self.focus_handle.focus(cx);
1284            } else {
1285                self.activate_item(index_to_activate, should_activate, should_activate, cx);
1286            }
1287        }
1288
1289        let item = self.items.remove(item_index);
1290
1291        cx.emit(Event::RemoveItem {
1292            item_id: item.item_id(),
1293        });
1294        if self.items.is_empty() {
1295            item.deactivated(cx);
1296            if close_pane_if_empty {
1297                self.update_toolbar(cx);
1298                cx.emit(Event::Remove);
1299            }
1300        }
1301
1302        if item_index < self.active_item_index {
1303            self.active_item_index -= 1;
1304        }
1305
1306        let mode = self.nav_history.mode();
1307        self.nav_history.set_mode(NavigationMode::ClosingItem);
1308        item.deactivated(cx);
1309        self.nav_history.set_mode(mode);
1310
1311        if self.is_active_preview_item(item.item_id()) {
1312            self.set_preview_item_id(None, cx);
1313        }
1314
1315        if let Some(path) = item.project_path(cx) {
1316            let abs_path = self
1317                .nav_history
1318                .0
1319                .lock()
1320                .paths_by_item
1321                .get(&item.item_id())
1322                .and_then(|(_, abs_path)| abs_path.clone());
1323
1324            self.nav_history
1325                .0
1326                .lock()
1327                .paths_by_item
1328                .insert(item.item_id(), (path, abs_path));
1329        } else {
1330            self.nav_history
1331                .0
1332                .lock()
1333                .paths_by_item
1334                .remove(&item.item_id());
1335        }
1336
1337        if self.items.is_empty() && close_pane_if_empty && self.zoomed {
1338            cx.emit(Event::ZoomOut);
1339        }
1340
1341        cx.notify();
1342    }
1343
1344    pub async fn save_item(
1345        project: Model<Project>,
1346        pane: &WeakView<Pane>,
1347        item_ix: usize,
1348        item: &dyn ItemHandle,
1349        save_intent: SaveIntent,
1350        cx: &mut AsyncWindowContext,
1351    ) -> Result<bool> {
1352        const CONFLICT_MESSAGE: &str =
1353                "This file has changed on disk since you started editing it. Do you want to overwrite it?";
1354
1355        if save_intent == SaveIntent::Skip {
1356            return Ok(true);
1357        }
1358
1359        let (mut has_conflict, mut is_dirty, mut can_save, can_save_as) = cx.update(|cx| {
1360            (
1361                item.has_conflict(cx),
1362                item.is_dirty(cx),
1363                item.can_save(cx),
1364                item.is_singleton(cx),
1365            )
1366        })?;
1367
1368        // when saving a single buffer, we ignore whether or not it's dirty.
1369        if save_intent == SaveIntent::Save || save_intent == SaveIntent::SaveWithoutFormat {
1370            is_dirty = true;
1371        }
1372
1373        if save_intent == SaveIntent::SaveAs {
1374            is_dirty = true;
1375            has_conflict = false;
1376            can_save = false;
1377        }
1378
1379        if save_intent == SaveIntent::Overwrite {
1380            has_conflict = false;
1381        }
1382
1383        let should_format = save_intent != SaveIntent::SaveWithoutFormat;
1384
1385        if has_conflict && can_save {
1386            let answer = pane.update(cx, |pane, cx| {
1387                pane.activate_item(item_ix, true, true, cx);
1388                cx.prompt(
1389                    PromptLevel::Warning,
1390                    CONFLICT_MESSAGE,
1391                    None,
1392                    &["Overwrite", "Discard", "Cancel"],
1393                )
1394            })?;
1395            match answer.await {
1396                Ok(0) => {
1397                    pane.update(cx, |_, cx| item.save(should_format, project, cx))?
1398                        .await?
1399                }
1400                Ok(1) => pane.update(cx, |_, cx| item.reload(project, cx))?.await?,
1401                _ => return Ok(false),
1402            }
1403        } else if is_dirty && (can_save || can_save_as) {
1404            if save_intent == SaveIntent::Close {
1405                let will_autosave = cx.update(|cx| {
1406                    matches!(
1407                        WorkspaceSettings::get_global(cx).autosave,
1408                        AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange
1409                    ) && Self::can_autosave_item(item, cx)
1410                })?;
1411                if !will_autosave {
1412                    let item_id = item.item_id();
1413                    let answer_task = pane.update(cx, |pane, cx| {
1414                        if pane.save_modals_spawned.insert(item_id) {
1415                            pane.activate_item(item_ix, true, true, cx);
1416                            let prompt = dirty_message_for(item.project_path(cx));
1417                            Some(cx.prompt(
1418                                PromptLevel::Warning,
1419                                &prompt,
1420                                None,
1421                                &["Save", "Don't Save", "Cancel"],
1422                            ))
1423                        } else {
1424                            None
1425                        }
1426                    })?;
1427                    if let Some(answer_task) = answer_task {
1428                        let answer = answer_task.await;
1429                        pane.update(cx, |pane, _| {
1430                            if !pane.save_modals_spawned.remove(&item_id) {
1431                                debug_panic!(
1432                                    "save modal was not present in spawned modals after awaiting for its answer"
1433                                )
1434                            }
1435                        })?;
1436                        match answer {
1437                            Ok(0) => {}
1438                            Ok(1) => return Ok(true), // Don't save this file
1439                            _ => return Ok(false),    // Cancel
1440                        }
1441                    } else {
1442                        return Ok(false);
1443                    }
1444                }
1445            }
1446
1447            if can_save {
1448                pane.update(cx, |_, cx| item.save(should_format, project, cx))?
1449                    .await?;
1450            } else if can_save_as {
1451                let abs_path = pane.update(cx, |pane, cx| {
1452                    pane.workspace
1453                        .update(cx, |workspace, cx| workspace.prompt_for_new_path(cx))
1454                })??;
1455                if let Some(abs_path) = abs_path.await.ok().flatten() {
1456                    pane.update(cx, |_, cx| item.save_as(project, abs_path, cx))?
1457                        .await?;
1458                } else {
1459                    return Ok(false);
1460                }
1461            }
1462        }
1463        Ok(true)
1464    }
1465
1466    fn can_autosave_item(item: &dyn ItemHandle, cx: &AppContext) -> bool {
1467        let is_deleted = item.project_entry_ids(cx).is_empty();
1468        item.is_dirty(cx) && !item.has_conflict(cx) && item.can_save(cx) && !is_deleted
1469    }
1470
1471    pub fn autosave_item(
1472        item: &dyn ItemHandle,
1473        project: Model<Project>,
1474        cx: &mut WindowContext,
1475    ) -> Task<Result<()>> {
1476        let format = if let AutosaveSetting::AfterDelay { .. } =
1477            WorkspaceSettings::get_global(cx).autosave
1478        {
1479            false
1480        } else {
1481            true
1482        };
1483        if Self::can_autosave_item(item, cx) {
1484            item.save(format, project, cx)
1485        } else {
1486            Task::ready(Ok(()))
1487        }
1488    }
1489
1490    pub fn focus(&mut self, cx: &mut ViewContext<Pane>) {
1491        cx.focus(&self.focus_handle);
1492    }
1493
1494    pub fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
1495        if let Some(active_item) = self.active_item() {
1496            let focus_handle = active_item.focus_handle(cx);
1497            cx.focus(&focus_handle);
1498        }
1499    }
1500
1501    pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext<Self>) {
1502        cx.emit(Event::Split(direction));
1503    }
1504
1505    pub fn toolbar(&self) -> &View<Toolbar> {
1506        &self.toolbar
1507    }
1508
1509    pub fn handle_deleted_project_item(
1510        &mut self,
1511        entry_id: ProjectEntryId,
1512        cx: &mut ViewContext<Pane>,
1513    ) -> Option<()> {
1514        let (item_index_to_delete, item_id) = self.items().enumerate().find_map(|(i, item)| {
1515            if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
1516                Some((i, item.item_id()))
1517            } else {
1518                None
1519            }
1520        })?;
1521
1522        self.remove_item(item_index_to_delete, false, true, cx);
1523        self.nav_history.remove_item(item_id);
1524
1525        Some(())
1526    }
1527
1528    fn update_toolbar(&mut self, cx: &mut ViewContext<Self>) {
1529        let active_item = self
1530            .items
1531            .get(self.active_item_index)
1532            .map(|item| item.as_ref());
1533        self.toolbar.update(cx, |toolbar, cx| {
1534            toolbar.set_active_item(active_item, cx);
1535        });
1536    }
1537
1538    fn update_status_bar(&mut self, cx: &mut ViewContext<Self>) {
1539        let workspace = self.workspace.clone();
1540        let pane = cx.view().clone();
1541
1542        cx.window_context().defer(move |cx| {
1543            let Ok(status_bar) = workspace.update(cx, |workspace, _| workspace.status_bar.clone())
1544            else {
1545                return;
1546            };
1547
1548            status_bar.update(cx, move |status_bar, cx| {
1549                status_bar.set_active_pane(&pane, cx);
1550            });
1551        });
1552    }
1553
1554    fn render_tab(
1555        &self,
1556        ix: usize,
1557        item: &Box<dyn ItemHandle>,
1558        detail: usize,
1559        cx: &mut ViewContext<'_, Pane>,
1560    ) -> impl IntoElement {
1561        let is_active = ix == self.active_item_index;
1562        let is_preview = self
1563            .preview_item_id
1564            .map(|id| id == item.item_id())
1565            .unwrap_or(false);
1566
1567        let label = item.tab_content(
1568            TabContentParams {
1569                detail: Some(detail),
1570                selected: is_active,
1571                preview: is_preview,
1572            },
1573            cx,
1574        );
1575        let close_side = &ItemSettings::get_global(cx).close_position;
1576        let indicator = render_item_indicator(item.boxed_clone(), cx);
1577        let item_id = item.item_id();
1578        let is_first_item = ix == 0;
1579        let is_last_item = ix == self.items.len() - 1;
1580        let position_relative_to_active_item = ix.cmp(&self.active_item_index);
1581
1582        let tab = Tab::new(ix)
1583            .position(if is_first_item {
1584                TabPosition::First
1585            } else if is_last_item {
1586                TabPosition::Last
1587            } else {
1588                TabPosition::Middle(position_relative_to_active_item)
1589            })
1590            .close_side(match close_side {
1591                ClosePosition::Left => ui::TabCloseSide::Start,
1592                ClosePosition::Right => ui::TabCloseSide::End,
1593            })
1594            .selected(is_active)
1595            .on_click(
1596                cx.listener(move |pane: &mut Self, _, cx| pane.activate_item(ix, true, true, cx)),
1597            )
1598            // TODO: This should be a click listener with the middle mouse button instead of a mouse down listener.
1599            .on_mouse_down(
1600                MouseButton::Middle,
1601                cx.listener(move |pane, _event, cx| {
1602                    pane.close_item_by_id(item_id, SaveIntent::Close, cx)
1603                        .detach_and_log_err(cx);
1604                }),
1605            )
1606            .on_mouse_down(
1607                MouseButton::Left,
1608                cx.listener(move |pane, event: &MouseDownEvent, cx| {
1609                    if let Some(id) = pane.preview_item_id {
1610                        if id == item_id && event.click_count > 1 {
1611                            pane.set_preview_item_id(None, cx);
1612                        }
1613                    }
1614                }),
1615            )
1616            .on_drag(
1617                DraggedTab {
1618                    item: item.boxed_clone(),
1619                    pane: cx.view().clone(),
1620                    detail,
1621                    is_active,
1622                    ix,
1623                },
1624                |tab, cx| cx.new_view(|_| tab.clone()),
1625            )
1626            .drag_over::<DraggedTab>(|tab, _, cx| {
1627                tab.bg(cx.theme().colors().drop_target_background)
1628            })
1629            .drag_over::<DraggedSelection>(|tab, _, cx| {
1630                tab.bg(cx.theme().colors().drop_target_background)
1631            })
1632            .when_some(self.can_drop_predicate.clone(), |this, p| {
1633                this.can_drop(move |a, cx| p(a, cx))
1634            })
1635            .on_drop(cx.listener(move |this, dragged_tab: &DraggedTab, cx| {
1636                this.drag_split_direction = None;
1637                this.handle_tab_drop(dragged_tab, ix, cx)
1638            }))
1639            .on_drop(cx.listener(move |this, selection: &DraggedSelection, cx| {
1640                this.drag_split_direction = None;
1641                this.handle_project_entry_drop(&selection.active_selection.entry_id, cx)
1642            }))
1643            .on_drop(cx.listener(move |this, paths, cx| {
1644                this.drag_split_direction = None;
1645                this.handle_external_paths_drop(paths, cx)
1646            }))
1647            .when_some(item.tab_tooltip_text(cx), |tab, text| {
1648                tab.tooltip(move |cx| Tooltip::text(text.clone(), cx))
1649            })
1650            .start_slot::<Indicator>(indicator)
1651            .end_slot(
1652                IconButton::new("close tab", IconName::Close)
1653                    .shape(IconButtonShape::Square)
1654                    .icon_color(Color::Muted)
1655                    .size(ButtonSize::None)
1656                    .icon_size(IconSize::XSmall)
1657                    .on_click(cx.listener(move |pane, _, cx| {
1658                        pane.close_item_by_id(item_id, SaveIntent::Close, cx)
1659                            .detach_and_log_err(cx);
1660                    })),
1661            )
1662            .child(label);
1663
1664        let single_entry_to_resolve = {
1665            let item_entries = self.items[ix].project_entry_ids(cx);
1666            if item_entries.len() == 1 {
1667                Some(item_entries[0])
1668            } else {
1669                None
1670            }
1671        };
1672
1673        let pane = cx.view().downgrade();
1674        right_click_menu(ix).trigger(tab).menu(move |cx| {
1675            let pane = pane.clone();
1676            ContextMenu::build(cx, move |mut menu, cx| {
1677                if let Some(pane) = pane.upgrade() {
1678                    menu = menu
1679                        .entry(
1680                            "Close",
1681                            Some(Box::new(CloseActiveItem { save_intent: None })),
1682                            cx.handler_for(&pane, move |pane, cx| {
1683                                pane.close_item_by_id(item_id, SaveIntent::Close, cx)
1684                                    .detach_and_log_err(cx);
1685                            }),
1686                        )
1687                        .entry(
1688                            "Close Others",
1689                            Some(Box::new(CloseInactiveItems { save_intent: None })),
1690                            cx.handler_for(&pane, move |pane, cx| {
1691                                pane.close_items(cx, SaveIntent::Close, |id| id != item_id)
1692                                    .detach_and_log_err(cx);
1693                            }),
1694                        )
1695                        .separator()
1696                        .entry(
1697                            "Close Left",
1698                            Some(Box::new(CloseItemsToTheLeft)),
1699                            cx.handler_for(&pane, move |pane, cx| {
1700                                pane.close_items_to_the_left_by_id(item_id, cx)
1701                                    .detach_and_log_err(cx);
1702                            }),
1703                        )
1704                        .entry(
1705                            "Close Right",
1706                            Some(Box::new(CloseItemsToTheRight)),
1707                            cx.handler_for(&pane, move |pane, cx| {
1708                                pane.close_items_to_the_right_by_id(item_id, cx)
1709                                    .detach_and_log_err(cx);
1710                            }),
1711                        )
1712                        .separator()
1713                        .entry(
1714                            "Close Clean",
1715                            Some(Box::new(CloseCleanItems)),
1716                            cx.handler_for(&pane, move |pane, cx| {
1717                                if let Some(task) = pane.close_clean_items(&CloseCleanItems, cx) {
1718                                    task.detach_and_log_err(cx)
1719                                }
1720                            }),
1721                        )
1722                        .entry(
1723                            "Close All",
1724                            Some(Box::new(CloseAllItems { save_intent: None })),
1725                            cx.handler_for(&pane, |pane, cx| {
1726                                if let Some(task) =
1727                                    pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
1728                                {
1729                                    task.detach_and_log_err(cx)
1730                                }
1731                            }),
1732                        );
1733
1734                    if let Some(entry) = single_entry_to_resolve {
1735                        let parent_abs_path = pane
1736                            .update(cx, |pane, cx| {
1737                                pane.workspace.update(cx, |workspace, cx| {
1738                                    let project = workspace.project().read(cx);
1739                                    project.worktree_for_entry(entry, cx).and_then(|worktree| {
1740                                        let worktree = worktree.read(cx);
1741                                        let entry = worktree.entry_for_id(entry)?;
1742                                        let abs_path = worktree.absolutize(&entry.path).ok()?;
1743                                        let parent = if entry.is_symlink {
1744                                            abs_path.canonicalize().ok()?
1745                                        } else {
1746                                            abs_path
1747                                        }
1748                                        .parent()?
1749                                        .to_path_buf();
1750                                        Some(parent)
1751                                    })
1752                                })
1753                            })
1754                            .ok()
1755                            .flatten();
1756
1757                        let entry_id = entry.to_proto();
1758                        menu = menu
1759                            .separator()
1760                            .entry(
1761                                "Reveal In Project Panel",
1762                                Some(Box::new(RevealInProjectPanel {
1763                                    entry_id: Some(entry_id),
1764                                })),
1765                                cx.handler_for(&pane, move |pane, cx| {
1766                                    pane.project.update(cx, |_, cx| {
1767                                        cx.emit(project::Event::RevealInProjectPanel(
1768                                            ProjectEntryId::from_proto(entry_id),
1769                                        ))
1770                                    });
1771                                }),
1772                            )
1773                            .when_some(parent_abs_path, |menu, abs_path| {
1774                                menu.entry(
1775                                    "Open in Terminal",
1776                                    Some(Box::new(OpenInTerminal)),
1777                                    cx.handler_for(&pane, move |_, cx| {
1778                                        cx.dispatch_action(
1779                                            OpenTerminal {
1780                                                working_directory: abs_path.clone(),
1781                                            }
1782                                            .boxed_clone(),
1783                                        );
1784                                    }),
1785                                )
1786                            });
1787                    }
1788                }
1789
1790                menu
1791            })
1792        })
1793    }
1794
1795    fn render_tab_bar(&mut self, cx: &mut ViewContext<'_, Pane>) -> impl IntoElement {
1796        let navigate_backward = IconButton::new("navigate_backward", IconName::ArrowLeft)
1797            .shape(IconButtonShape::Square)
1798            .icon_size(IconSize::Small)
1799            .on_click({
1800                let view = cx.view().clone();
1801                move |_, cx| view.update(cx, Self::navigate_backward)
1802            })
1803            .disabled(!self.can_navigate_backward())
1804            .tooltip(|cx| Tooltip::for_action("Go Back", &GoBack, cx));
1805
1806        let navigate_forward = IconButton::new("navigate_forward", IconName::ArrowRight)
1807            .shape(IconButtonShape::Square)
1808            .icon_size(IconSize::Small)
1809            .on_click({
1810                let view = cx.view().clone();
1811                move |_, cx| view.update(cx, Self::navigate_forward)
1812            })
1813            .disabled(!self.can_navigate_forward())
1814            .tooltip(|cx| Tooltip::for_action("Go Forward", &GoForward, cx));
1815
1816        TabBar::new("tab_bar")
1817            .track_scroll(self.tab_bar_scroll_handle.clone())
1818            .when(
1819                self.display_nav_history_buttons.unwrap_or_default(),
1820                |tab_bar| tab_bar.start_children(vec![navigate_backward, navigate_forward]),
1821            )
1822            .when(self.has_focus(cx), |tab_bar| {
1823                tab_bar.end_child({
1824                    let render_tab_buttons = self.render_tab_bar_buttons.clone();
1825                    render_tab_buttons(self, cx)
1826                })
1827            })
1828            .children(
1829                self.items
1830                    .iter()
1831                    .enumerate()
1832                    .zip(tab_details(&self.items, cx))
1833                    .map(|((ix, item), detail)| self.render_tab(ix, item, detail, cx)),
1834            )
1835            .child(
1836                div()
1837                    .id("tab_bar_drop_target")
1838                    .min_w_6()
1839                    // HACK: This empty child is currently necessary to force the drop target to appear
1840                    // despite us setting a min width above.
1841                    .child("")
1842                    .h_full()
1843                    .flex_grow()
1844                    .drag_over::<DraggedTab>(|bar, _, cx| {
1845                        bar.bg(cx.theme().colors().drop_target_background)
1846                    })
1847                    .drag_over::<DraggedSelection>(|bar, _, cx| {
1848                        bar.bg(cx.theme().colors().drop_target_background)
1849                    })
1850                    .on_drop(cx.listener(move |this, dragged_tab: &DraggedTab, cx| {
1851                        this.drag_split_direction = None;
1852                        this.handle_tab_drop(dragged_tab, this.items.len(), cx)
1853                    }))
1854                    .on_drop(cx.listener(move |this, selection: &DraggedSelection, cx| {
1855                        this.drag_split_direction = None;
1856                        this.handle_project_entry_drop(&selection.active_selection.entry_id, cx)
1857                    }))
1858                    .on_drop(cx.listener(move |this, paths, cx| {
1859                        this.drag_split_direction = None;
1860                        this.handle_external_paths_drop(paths, cx)
1861                    }))
1862                    .on_click(cx.listener(move |this, event: &ClickEvent, cx| {
1863                        if event.up.click_count == 2 {
1864                            cx.dispatch_action(this.double_click_dispatch_action.boxed_clone())
1865                        }
1866                    })),
1867            )
1868    }
1869
1870    pub fn render_menu_overlay(menu: &View<ContextMenu>) -> Div {
1871        div().absolute().bottom_0().right_0().size_0().child(
1872            deferred(
1873                anchored()
1874                    .anchor(AnchorCorner::TopRight)
1875                    .child(menu.clone()),
1876            )
1877            .with_priority(1),
1878        )
1879    }
1880
1881    pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
1882        self.zoomed = zoomed;
1883        cx.notify();
1884    }
1885
1886    pub fn is_zoomed(&self) -> bool {
1887        self.zoomed
1888    }
1889
1890    fn handle_drag_move<T>(&mut self, event: &DragMoveEvent<T>, cx: &mut ViewContext<Self>) {
1891        if !self.can_split {
1892            return;
1893        }
1894
1895        let rect = event.bounds.size;
1896
1897        let size = event.bounds.size.width.min(event.bounds.size.height)
1898            * WorkspaceSettings::get_global(cx).drop_target_size;
1899
1900        let relative_cursor = Point::new(
1901            event.event.position.x - event.bounds.left(),
1902            event.event.position.y - event.bounds.top(),
1903        );
1904
1905        let direction = if relative_cursor.x < size
1906            || relative_cursor.x > rect.width - size
1907            || relative_cursor.y < size
1908            || relative_cursor.y > rect.height - size
1909        {
1910            [
1911                SplitDirection::Up,
1912                SplitDirection::Right,
1913                SplitDirection::Down,
1914                SplitDirection::Left,
1915            ]
1916            .iter()
1917            .min_by_key(|side| match side {
1918                SplitDirection::Up => relative_cursor.y,
1919                SplitDirection::Right => rect.width - relative_cursor.x,
1920                SplitDirection::Down => rect.height - relative_cursor.y,
1921                SplitDirection::Left => relative_cursor.x,
1922            })
1923            .cloned()
1924        } else {
1925            None
1926        };
1927
1928        if direction != self.drag_split_direction {
1929            self.drag_split_direction = direction;
1930        }
1931    }
1932
1933    fn handle_tab_drop(
1934        &mut self,
1935        dragged_tab: &DraggedTab,
1936        ix: usize,
1937        cx: &mut ViewContext<'_, Self>,
1938    ) {
1939        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
1940            if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_tab, cx) {
1941                return;
1942            }
1943        }
1944        let mut to_pane = cx.view().clone();
1945        let split_direction = self.drag_split_direction;
1946        let item_id = dragged_tab.item.item_id();
1947        if let Some(preview_item_id) = self.preview_item_id {
1948            if item_id == preview_item_id {
1949                self.set_preview_item_id(None, cx);
1950            }
1951        }
1952
1953        let from_pane = dragged_tab.pane.clone();
1954        self.workspace
1955            .update(cx, |_, cx| {
1956                cx.defer(move |workspace, cx| {
1957                    if let Some(split_direction) = split_direction {
1958                        to_pane = workspace.split_pane(to_pane, split_direction, cx);
1959                    }
1960                    workspace.move_item(from_pane, to_pane, item_id, ix, cx);
1961                });
1962            })
1963            .log_err();
1964    }
1965
1966    fn handle_project_entry_drop(
1967        &mut self,
1968        project_entry_id: &ProjectEntryId,
1969        cx: &mut ViewContext<'_, Self>,
1970    ) {
1971        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
1972            if let ControlFlow::Break(()) = custom_drop_handle(self, project_entry_id, cx) {
1973                return;
1974            }
1975        }
1976        let mut to_pane = cx.view().clone();
1977        let split_direction = self.drag_split_direction;
1978        let project_entry_id = *project_entry_id;
1979        self.workspace
1980            .update(cx, |_, cx| {
1981                cx.defer(move |workspace, cx| {
1982                    if let Some(path) = workspace
1983                        .project()
1984                        .read(cx)
1985                        .path_for_entry(project_entry_id, cx)
1986                    {
1987                        if let Some(split_direction) = split_direction {
1988                            to_pane = workspace.split_pane(to_pane, split_direction, cx);
1989                        }
1990                        workspace
1991                            .open_path(path, Some(to_pane.downgrade()), true, cx)
1992                            .detach_and_log_err(cx);
1993                    }
1994                });
1995            })
1996            .log_err();
1997    }
1998
1999    fn handle_external_paths_drop(
2000        &mut self,
2001        paths: &ExternalPaths,
2002        cx: &mut ViewContext<'_, Self>,
2003    ) {
2004        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2005            if let ControlFlow::Break(()) = custom_drop_handle(self, paths, cx) {
2006                return;
2007            }
2008        }
2009        let mut to_pane = cx.view().clone();
2010        let mut split_direction = self.drag_split_direction;
2011        let paths = paths.paths().to_vec();
2012        let is_remote = self
2013            .workspace
2014            .update(cx, |workspace, cx| {
2015                if workspace.project().read(cx).is_remote() {
2016                    workspace.show_error(
2017                        &anyhow::anyhow!("Cannot drop files on a remote project"),
2018                        cx,
2019                    );
2020                    true
2021                } else {
2022                    false
2023                }
2024            })
2025            .unwrap_or(true);
2026        if is_remote {
2027            return;
2028        }
2029
2030        self.workspace
2031            .update(cx, |workspace, cx| {
2032                let fs = Arc::clone(workspace.project().read(cx).fs());
2033                cx.spawn(|workspace, mut cx| async move {
2034                    let mut is_file_checks = FuturesUnordered::new();
2035                    for path in &paths {
2036                        is_file_checks.push(fs.is_file(path))
2037                    }
2038                    let mut has_files_to_open = false;
2039                    while let Some(is_file) = is_file_checks.next().await {
2040                        if is_file {
2041                            has_files_to_open = true;
2042                            break;
2043                        }
2044                    }
2045                    drop(is_file_checks);
2046                    if !has_files_to_open {
2047                        split_direction = None;
2048                    }
2049
2050                    if let Some(open_task) = workspace
2051                        .update(&mut cx, |workspace, cx| {
2052                            if let Some(split_direction) = split_direction {
2053                                to_pane = workspace.split_pane(to_pane, split_direction, cx);
2054                            }
2055                            workspace.open_paths(
2056                                paths,
2057                                OpenVisible::OnlyDirectories,
2058                                Some(to_pane.downgrade()),
2059                                cx,
2060                            )
2061                        })
2062                        .ok()
2063                    {
2064                        let _opened_items: Vec<_> = open_task.await;
2065                    }
2066                })
2067                .detach();
2068            })
2069            .log_err();
2070    }
2071
2072    pub fn display_nav_history_buttons(&mut self, display: Option<bool>) {
2073        self.display_nav_history_buttons = display;
2074    }
2075}
2076
2077impl FocusableView for Pane {
2078    fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
2079        self.focus_handle.clone()
2080    }
2081}
2082
2083impl Render for Pane {
2084    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2085        let mut key_context = KeyContext::new_with_defaults();
2086        key_context.add("Pane");
2087        if self.active_item().is_none() {
2088            key_context.add("EmptyPane");
2089        }
2090
2091        let should_display_tab_bar = self.should_display_tab_bar.clone();
2092        let display_tab_bar = should_display_tab_bar(cx);
2093
2094        v_flex()
2095            .key_context(key_context)
2096            .track_focus(&self.focus_handle)
2097            .size_full()
2098            .flex_none()
2099            .overflow_hidden()
2100            .on_action(cx.listener(|pane, _: &AlternateFile, cx| {
2101                pane.alternate_file(cx);
2102            }))
2103            .on_action(cx.listener(|pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx)))
2104            .on_action(cx.listener(|pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx)))
2105            .on_action(
2106                cx.listener(|pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx)),
2107            )
2108            .on_action(cx.listener(|pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx)))
2109            .on_action(cx.listener(|pane, _: &GoBack, cx| pane.navigate_backward(cx)))
2110            .on_action(cx.listener(|pane, _: &GoForward, cx| pane.navigate_forward(cx)))
2111            .on_action(cx.listener(Pane::toggle_zoom))
2112            .on_action(cx.listener(|pane: &mut Pane, action: &ActivateItem, cx| {
2113                pane.activate_item(action.0, true, true, cx);
2114            }))
2115            .on_action(cx.listener(|pane: &mut Pane, _: &ActivateLastItem, cx| {
2116                pane.activate_item(pane.items.len() - 1, true, true, cx);
2117            }))
2118            .on_action(cx.listener(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
2119                pane.activate_prev_item(true, cx);
2120            }))
2121            .on_action(cx.listener(|pane: &mut Pane, _: &ActivateNextItem, cx| {
2122                pane.activate_next_item(true, cx);
2123            }))
2124            .when(PreviewTabsSettings::get_global(cx).enabled, |this| {
2125                this.on_action(cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, cx| {
2126                    if let Some(active_item_id) = pane.active_item().map(|i| i.item_id()) {
2127                        if pane.is_active_preview_item(active_item_id) {
2128                            pane.set_preview_item_id(None, cx);
2129                        } else {
2130                            pane.set_preview_item_id(Some(active_item_id), cx);
2131                        }
2132                    }
2133                }))
2134            })
2135            .on_action(
2136                cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
2137                    if let Some(task) = pane.close_active_item(action, cx) {
2138                        task.detach_and_log_err(cx)
2139                    }
2140                }),
2141            )
2142            .on_action(
2143                cx.listener(|pane: &mut Self, action: &CloseInactiveItems, cx| {
2144                    if let Some(task) = pane.close_inactive_items(action, cx) {
2145                        task.detach_and_log_err(cx)
2146                    }
2147                }),
2148            )
2149            .on_action(
2150                cx.listener(|pane: &mut Self, action: &CloseCleanItems, cx| {
2151                    if let Some(task) = pane.close_clean_items(action, cx) {
2152                        task.detach_and_log_err(cx)
2153                    }
2154                }),
2155            )
2156            .on_action(
2157                cx.listener(|pane: &mut Self, action: &CloseItemsToTheLeft, cx| {
2158                    if let Some(task) = pane.close_items_to_the_left(action, cx) {
2159                        task.detach_and_log_err(cx)
2160                    }
2161                }),
2162            )
2163            .on_action(
2164                cx.listener(|pane: &mut Self, action: &CloseItemsToTheRight, cx| {
2165                    if let Some(task) = pane.close_items_to_the_right(action, cx) {
2166                        task.detach_and_log_err(cx)
2167                    }
2168                }),
2169            )
2170            .on_action(cx.listener(|pane: &mut Self, action: &CloseAllItems, cx| {
2171                if let Some(task) = pane.close_all_items(action, cx) {
2172                    task.detach_and_log_err(cx)
2173                }
2174            }))
2175            .on_action(
2176                cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
2177                    if let Some(task) = pane.close_active_item(action, cx) {
2178                        task.detach_and_log_err(cx)
2179                    }
2180                }),
2181            )
2182            .on_action(
2183                cx.listener(|pane: &mut Self, action: &RevealInProjectPanel, cx| {
2184                    let entry_id = action
2185                        .entry_id
2186                        .map(ProjectEntryId::from_proto)
2187                        .or_else(|| pane.active_item()?.project_entry_ids(cx).first().copied());
2188                    if let Some(entry_id) = entry_id {
2189                        pane.project.update(cx, |_, cx| {
2190                            cx.emit(project::Event::RevealInProjectPanel(entry_id))
2191                        });
2192                    }
2193                }),
2194            )
2195            .when(self.active_item().is_some() && display_tab_bar, |pane| {
2196                pane.child(self.render_tab_bar(cx))
2197            })
2198            .child({
2199                let has_worktrees = self.project.read(cx).worktrees().next().is_some();
2200                // main content
2201                div()
2202                    .flex_1()
2203                    .relative()
2204                    .group("")
2205                    .on_drag_move::<DraggedTab>(cx.listener(Self::handle_drag_move))
2206                    .on_drag_move::<DraggedSelection>(cx.listener(Self::handle_drag_move))
2207                    .on_drag_move::<ExternalPaths>(cx.listener(Self::handle_drag_move))
2208                    .map(|div| {
2209                        if let Some(item) = self.active_item() {
2210                            div.v_flex()
2211                                .child(self.toolbar.clone())
2212                                .child(item.to_any())
2213                        } else {
2214                            let placeholder = div.h_flex().size_full().justify_center();
2215                            if has_worktrees {
2216                                placeholder
2217                            } else {
2218                                placeholder.child(
2219                                    Label::new("Open a file or project to get started.")
2220                                        .color(Color::Muted),
2221                                )
2222                            }
2223                        }
2224                    })
2225                    .child(
2226                        // drag target
2227                        div()
2228                            .invisible()
2229                            .absolute()
2230                            .bg(cx.theme().colors().drop_target_background)
2231                            .group_drag_over::<DraggedTab>("", |style| style.visible())
2232                            .group_drag_over::<DraggedSelection>("", |style| style.visible())
2233                            .group_drag_over::<ExternalPaths>("", |style| style.visible())
2234                            .when_some(self.can_drop_predicate.clone(), |this, p| {
2235                                this.can_drop(move |a, cx| p(a, cx))
2236                            })
2237                            .on_drop(cx.listener(move |this, dragged_tab, cx| {
2238                                this.handle_tab_drop(dragged_tab, this.active_item_index(), cx)
2239                            }))
2240                            .on_drop(cx.listener(move |this, selection: &DraggedSelection, cx| {
2241                                this.handle_project_entry_drop(
2242                                    &selection.active_selection.entry_id,
2243                                    cx,
2244                                )
2245                            }))
2246                            .on_drop(cx.listener(move |this, paths, cx| {
2247                                this.handle_external_paths_drop(paths, cx)
2248                            }))
2249                            .map(|div| {
2250                                let size = DefiniteLength::Fraction(0.5);
2251                                match self.drag_split_direction {
2252                                    None => div.top_0().right_0().bottom_0().left_0(),
2253                                    Some(SplitDirection::Up) => {
2254                                        div.top_0().left_0().right_0().h(size)
2255                                    }
2256                                    Some(SplitDirection::Down) => {
2257                                        div.left_0().bottom_0().right_0().h(size)
2258                                    }
2259                                    Some(SplitDirection::Left) => {
2260                                        div.top_0().left_0().bottom_0().w(size)
2261                                    }
2262                                    Some(SplitDirection::Right) => {
2263                                        div.top_0().bottom_0().right_0().w(size)
2264                                    }
2265                                }
2266                            }),
2267                    )
2268            })
2269            .on_mouse_down(
2270                MouseButton::Navigate(NavigationDirection::Back),
2271                cx.listener(|pane, _, cx| {
2272                    if let Some(workspace) = pane.workspace.upgrade() {
2273                        let pane = cx.view().downgrade();
2274                        cx.window_context().defer(move |cx| {
2275                            workspace.update(cx, |workspace, cx| {
2276                                workspace.go_back(pane, cx).detach_and_log_err(cx)
2277                            })
2278                        })
2279                    }
2280                }),
2281            )
2282            .on_mouse_down(
2283                MouseButton::Navigate(NavigationDirection::Forward),
2284                cx.listener(|pane, _, cx| {
2285                    if let Some(workspace) = pane.workspace.upgrade() {
2286                        let pane = cx.view().downgrade();
2287                        cx.window_context().defer(move |cx| {
2288                            workspace.update(cx, |workspace, cx| {
2289                                workspace.go_forward(pane, cx).detach_and_log_err(cx)
2290                            })
2291                        })
2292                    }
2293                }),
2294            )
2295    }
2296}
2297
2298impl ItemNavHistory {
2299    pub fn push<D: 'static + Send + Any>(&mut self, data: Option<D>, cx: &mut WindowContext) {
2300        self.history
2301            .push(data, self.item.clone(), self.is_preview, cx);
2302    }
2303
2304    pub fn pop_backward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
2305        self.history.pop(NavigationMode::GoingBack, cx)
2306    }
2307
2308    pub fn pop_forward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
2309        self.history.pop(NavigationMode::GoingForward, cx)
2310    }
2311}
2312
2313impl NavHistory {
2314    pub fn for_each_entry(
2315        &self,
2316        cx: &AppContext,
2317        mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
2318    ) {
2319        let borrowed_history = self.0.lock();
2320        borrowed_history
2321            .forward_stack
2322            .iter()
2323            .chain(borrowed_history.backward_stack.iter())
2324            .chain(borrowed_history.closed_stack.iter())
2325            .for_each(|entry| {
2326                if let Some(project_and_abs_path) =
2327                    borrowed_history.paths_by_item.get(&entry.item.id())
2328                {
2329                    f(entry, project_and_abs_path.clone());
2330                } else if let Some(item) = entry.item.upgrade() {
2331                    if let Some(path) = item.project_path(cx) {
2332                        f(entry, (path, None));
2333                    }
2334                }
2335            })
2336    }
2337
2338    pub fn set_mode(&mut self, mode: NavigationMode) {
2339        self.0.lock().mode = mode;
2340    }
2341
2342    pub fn mode(&self) -> NavigationMode {
2343        self.0.lock().mode
2344    }
2345
2346    pub fn disable(&mut self) {
2347        self.0.lock().mode = NavigationMode::Disabled;
2348    }
2349
2350    pub fn enable(&mut self) {
2351        self.0.lock().mode = NavigationMode::Normal;
2352    }
2353
2354    pub fn pop(&mut self, mode: NavigationMode, cx: &mut WindowContext) -> Option<NavigationEntry> {
2355        let mut state = self.0.lock();
2356        let entry = match mode {
2357            NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
2358                return None
2359            }
2360            NavigationMode::GoingBack => &mut state.backward_stack,
2361            NavigationMode::GoingForward => &mut state.forward_stack,
2362            NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
2363        }
2364        .pop_back();
2365        if entry.is_some() {
2366            state.did_update(cx);
2367        }
2368        entry
2369    }
2370
2371    pub fn push<D: 'static + Send + Any>(
2372        &mut self,
2373        data: Option<D>,
2374        item: Arc<dyn WeakItemHandle>,
2375        is_preview: bool,
2376        cx: &mut WindowContext,
2377    ) {
2378        let state = &mut *self.0.lock();
2379        match state.mode {
2380            NavigationMode::Disabled => {}
2381            NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
2382                if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2383                    state.backward_stack.pop_front();
2384                }
2385                state.backward_stack.push_back(NavigationEntry {
2386                    item,
2387                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2388                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2389                    is_preview,
2390                });
2391                state.forward_stack.clear();
2392            }
2393            NavigationMode::GoingBack => {
2394                if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2395                    state.forward_stack.pop_front();
2396                }
2397                state.forward_stack.push_back(NavigationEntry {
2398                    item,
2399                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2400                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2401                    is_preview,
2402                });
2403            }
2404            NavigationMode::GoingForward => {
2405                if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2406                    state.backward_stack.pop_front();
2407                }
2408                state.backward_stack.push_back(NavigationEntry {
2409                    item,
2410                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2411                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2412                    is_preview,
2413                });
2414            }
2415            NavigationMode::ClosingItem => {
2416                if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2417                    state.closed_stack.pop_front();
2418                }
2419                state.closed_stack.push_back(NavigationEntry {
2420                    item,
2421                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2422                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2423                    is_preview,
2424                });
2425            }
2426        }
2427        state.did_update(cx);
2428    }
2429
2430    pub fn remove_item(&mut self, item_id: EntityId) {
2431        let mut state = self.0.lock();
2432        state.paths_by_item.remove(&item_id);
2433        state
2434            .backward_stack
2435            .retain(|entry| entry.item.id() != item_id);
2436        state
2437            .forward_stack
2438            .retain(|entry| entry.item.id() != item_id);
2439        state
2440            .closed_stack
2441            .retain(|entry| entry.item.id() != item_id);
2442    }
2443
2444    pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option<PathBuf>)> {
2445        self.0.lock().paths_by_item.get(&item_id).cloned()
2446    }
2447}
2448
2449impl NavHistoryState {
2450    pub fn did_update(&self, cx: &mut WindowContext) {
2451        if let Some(pane) = self.pane.upgrade() {
2452            cx.defer(move |cx| {
2453                pane.update(cx, |pane, cx| pane.history_updated(cx));
2454            });
2455        }
2456    }
2457}
2458
2459fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
2460    let path = buffer_path
2461        .as_ref()
2462        .and_then(|p| {
2463            p.path
2464                .to_str()
2465                .and_then(|s| if s == "" { None } else { Some(s) })
2466        })
2467        .unwrap_or("This buffer");
2468    let path = truncate_and_remove_front(path, 80);
2469    format!("{path} contains unsaved edits. Do you want to save it?")
2470}
2471
2472pub fn tab_details(items: &Vec<Box<dyn ItemHandle>>, cx: &AppContext) -> Vec<usize> {
2473    let mut tab_details = items.iter().map(|_| 0).collect::<Vec<_>>();
2474    let mut tab_descriptions = HashMap::default();
2475    let mut done = false;
2476    while !done {
2477        done = true;
2478
2479        // Store item indices by their tab description.
2480        for (ix, (item, detail)) in items.iter().zip(&tab_details).enumerate() {
2481            if let Some(description) = item.tab_description(*detail, cx) {
2482                if *detail == 0
2483                    || Some(&description) != item.tab_description(detail - 1, cx).as_ref()
2484                {
2485                    tab_descriptions
2486                        .entry(description)
2487                        .or_insert(Vec::new())
2488                        .push(ix);
2489                }
2490            }
2491        }
2492
2493        // If two or more items have the same tab description, increase their level
2494        // of detail and try again.
2495        for (_, item_ixs) in tab_descriptions.drain() {
2496            if item_ixs.len() > 1 {
2497                done = false;
2498                for ix in item_ixs {
2499                    tab_details[ix] += 1;
2500                }
2501            }
2502        }
2503    }
2504
2505    tab_details
2506}
2507
2508pub fn render_item_indicator(item: Box<dyn ItemHandle>, cx: &WindowContext) -> Option<Indicator> {
2509    maybe!({
2510        let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
2511            (true, _) => Color::Warning,
2512            (_, true) => Color::Accent,
2513            (false, false) => return None,
2514        };
2515
2516        Some(Indicator::dot().color(indicator_color))
2517    })
2518}
2519
2520#[cfg(test)]
2521mod tests {
2522    use super::*;
2523    use crate::item::test::{TestItem, TestProjectItem};
2524    use gpui::{TestAppContext, VisualTestContext};
2525    use project::FakeFs;
2526    use settings::SettingsStore;
2527    use theme::LoadThemes;
2528
2529    #[gpui::test]
2530    async fn test_remove_active_empty(cx: &mut TestAppContext) {
2531        init_test(cx);
2532        let fs = FakeFs::new(cx.executor());
2533
2534        let project = Project::test(fs, None, cx).await;
2535        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2536        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2537
2538        pane.update(cx, |pane, cx| {
2539            assert!(pane
2540                .close_active_item(&CloseActiveItem { save_intent: None }, cx)
2541                .is_none())
2542        });
2543    }
2544
2545    #[gpui::test]
2546    async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
2547        init_test(cx);
2548        let fs = FakeFs::new(cx.executor());
2549
2550        let project = Project::test(fs, None, cx).await;
2551        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2552        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2553
2554        // 1. Add with a destination index
2555        //   a. Add before the active item
2556        set_labeled_items(&pane, ["A", "B*", "C"], cx);
2557        pane.update(cx, |pane, cx| {
2558            pane.add_item(
2559                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2560                false,
2561                false,
2562                Some(0),
2563                cx,
2564            );
2565        });
2566        assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
2567
2568        //   b. Add after the active item
2569        set_labeled_items(&pane, ["A", "B*", "C"], cx);
2570        pane.update(cx, |pane, cx| {
2571            pane.add_item(
2572                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2573                false,
2574                false,
2575                Some(2),
2576                cx,
2577            );
2578        });
2579        assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
2580
2581        //   c. Add at the end of the item list (including off the length)
2582        set_labeled_items(&pane, ["A", "B*", "C"], cx);
2583        pane.update(cx, |pane, cx| {
2584            pane.add_item(
2585                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2586                false,
2587                false,
2588                Some(5),
2589                cx,
2590            );
2591        });
2592        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2593
2594        // 2. Add without a destination index
2595        //   a. Add with active item at the start of the item list
2596        set_labeled_items(&pane, ["A*", "B", "C"], cx);
2597        pane.update(cx, |pane, cx| {
2598            pane.add_item(
2599                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2600                false,
2601                false,
2602                None,
2603                cx,
2604            );
2605        });
2606        set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
2607
2608        //   b. Add with active item at the end of the item list
2609        set_labeled_items(&pane, ["A", "B", "C*"], cx);
2610        pane.update(cx, |pane, cx| {
2611            pane.add_item(
2612                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2613                false,
2614                false,
2615                None,
2616                cx,
2617            );
2618        });
2619        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2620    }
2621
2622    #[gpui::test]
2623    async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
2624        init_test(cx);
2625        let fs = FakeFs::new(cx.executor());
2626
2627        let project = Project::test(fs, None, cx).await;
2628        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2629        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2630
2631        // 1. Add with a destination index
2632        //   1a. Add before the active item
2633        let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
2634        pane.update(cx, |pane, cx| {
2635            pane.add_item(d, false, false, Some(0), cx);
2636        });
2637        assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
2638
2639        //   1b. Add after the active item
2640        let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
2641        pane.update(cx, |pane, cx| {
2642            pane.add_item(d, false, false, Some(2), cx);
2643        });
2644        assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
2645
2646        //   1c. Add at the end of the item list (including off the length)
2647        let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
2648        pane.update(cx, |pane, cx| {
2649            pane.add_item(a, false, false, Some(5), cx);
2650        });
2651        assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
2652
2653        //   1d. Add same item to active index
2654        let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
2655        pane.update(cx, |pane, cx| {
2656            pane.add_item(b, false, false, Some(1), cx);
2657        });
2658        assert_item_labels(&pane, ["A", "B*", "C"], cx);
2659
2660        //   1e. Add item to index after same item in last position
2661        let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
2662        pane.update(cx, |pane, cx| {
2663            pane.add_item(c, false, false, Some(2), cx);
2664        });
2665        assert_item_labels(&pane, ["A", "B", "C*"], cx);
2666
2667        // 2. Add without a destination index
2668        //   2a. Add with active item at the start of the item list
2669        let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
2670        pane.update(cx, |pane, cx| {
2671            pane.add_item(d, false, false, None, cx);
2672        });
2673        assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
2674
2675        //   2b. Add with active item at the end of the item list
2676        let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
2677        pane.update(cx, |pane, cx| {
2678            pane.add_item(a, false, false, None, cx);
2679        });
2680        assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
2681
2682        //   2c. Add active item to active item at end of list
2683        let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
2684        pane.update(cx, |pane, cx| {
2685            pane.add_item(c, false, false, None, cx);
2686        });
2687        assert_item_labels(&pane, ["A", "B", "C*"], cx);
2688
2689        //   2d. Add active item to active item at start of list
2690        let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
2691        pane.update(cx, |pane, cx| {
2692            pane.add_item(a, false, false, None, cx);
2693        });
2694        assert_item_labels(&pane, ["A*", "B", "C"], cx);
2695    }
2696
2697    #[gpui::test]
2698    async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
2699        init_test(cx);
2700        let fs = FakeFs::new(cx.executor());
2701
2702        let project = Project::test(fs, None, cx).await;
2703        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2704        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2705
2706        // singleton view
2707        pane.update(cx, |pane, cx| {
2708            pane.add_item(
2709                Box::new(cx.new_view(|cx| {
2710                    TestItem::new(cx)
2711                        .with_singleton(true)
2712                        .with_label("buffer 1")
2713                        .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
2714                })),
2715                false,
2716                false,
2717                None,
2718                cx,
2719            );
2720        });
2721        assert_item_labels(&pane, ["buffer 1*"], cx);
2722
2723        // new singleton view with the same project entry
2724        pane.update(cx, |pane, cx| {
2725            pane.add_item(
2726                Box::new(cx.new_view(|cx| {
2727                    TestItem::new(cx)
2728                        .with_singleton(true)
2729                        .with_label("buffer 1")
2730                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
2731                })),
2732                false,
2733                false,
2734                None,
2735                cx,
2736            );
2737        });
2738        assert_item_labels(&pane, ["buffer 1*"], cx);
2739
2740        // new singleton view with different project entry
2741        pane.update(cx, |pane, cx| {
2742            pane.add_item(
2743                Box::new(cx.new_view(|cx| {
2744                    TestItem::new(cx)
2745                        .with_singleton(true)
2746                        .with_label("buffer 2")
2747                        .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
2748                })),
2749                false,
2750                false,
2751                None,
2752                cx,
2753            );
2754        });
2755        assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
2756
2757        // new multibuffer view with the same project entry
2758        pane.update(cx, |pane, cx| {
2759            pane.add_item(
2760                Box::new(cx.new_view(|cx| {
2761                    TestItem::new(cx)
2762                        .with_singleton(false)
2763                        .with_label("multibuffer 1")
2764                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
2765                })),
2766                false,
2767                false,
2768                None,
2769                cx,
2770            );
2771        });
2772        assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
2773
2774        // another multibuffer view with the same project entry
2775        pane.update(cx, |pane, cx| {
2776            pane.add_item(
2777                Box::new(cx.new_view(|cx| {
2778                    TestItem::new(cx)
2779                        .with_singleton(false)
2780                        .with_label("multibuffer 1b")
2781                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
2782                })),
2783                false,
2784                false,
2785                None,
2786                cx,
2787            );
2788        });
2789        assert_item_labels(
2790            &pane,
2791            ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
2792            cx,
2793        );
2794    }
2795
2796    #[gpui::test]
2797    async fn test_remove_item_ordering(cx: &mut TestAppContext) {
2798        init_test(cx);
2799        let fs = FakeFs::new(cx.executor());
2800
2801        let project = Project::test(fs, None, cx).await;
2802        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2803        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2804
2805        add_labeled_item(&pane, "A", false, cx);
2806        add_labeled_item(&pane, "B", false, cx);
2807        add_labeled_item(&pane, "C", false, cx);
2808        add_labeled_item(&pane, "D", false, cx);
2809        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2810
2811        pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
2812        add_labeled_item(&pane, "1", false, cx);
2813        assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
2814
2815        pane.update(cx, |pane, cx| {
2816            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
2817        })
2818        .unwrap()
2819        .await
2820        .unwrap();
2821        assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
2822
2823        pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
2824        assert_item_labels(&pane, ["A", "B", "C", "D*"], 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", "B*", "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", "C*"], cx);
2841
2842        pane.update(cx, |pane, cx| {
2843            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
2844        })
2845        .unwrap()
2846        .await
2847        .unwrap();
2848        assert_item_labels(&pane, ["A*"], cx);
2849    }
2850
2851    #[gpui::test]
2852    async fn test_close_inactive_items(cx: &mut TestAppContext) {
2853        init_test(cx);
2854        let fs = FakeFs::new(cx.executor());
2855
2856        let project = Project::test(fs, None, cx).await;
2857        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2858        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2859
2860        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
2861
2862        pane.update(cx, |pane, cx| {
2863            pane.close_inactive_items(&CloseInactiveItems { save_intent: None }, cx)
2864        })
2865        .unwrap()
2866        .await
2867        .unwrap();
2868        assert_item_labels(&pane, ["C*"], cx);
2869    }
2870
2871    #[gpui::test]
2872    async fn test_close_clean_items(cx: &mut TestAppContext) {
2873        init_test(cx);
2874        let fs = FakeFs::new(cx.executor());
2875
2876        let project = Project::test(fs, None, cx).await;
2877        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2878        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2879
2880        add_labeled_item(&pane, "A", true, cx);
2881        add_labeled_item(&pane, "B", false, cx);
2882        add_labeled_item(&pane, "C", true, cx);
2883        add_labeled_item(&pane, "D", false, cx);
2884        add_labeled_item(&pane, "E", false, cx);
2885        assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
2886
2887        pane.update(cx, |pane, cx| pane.close_clean_items(&CloseCleanItems, cx))
2888            .unwrap()
2889            .await
2890            .unwrap();
2891        assert_item_labels(&pane, ["A^", "C*^"], cx);
2892    }
2893
2894    #[gpui::test]
2895    async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
2896        init_test(cx);
2897        let fs = FakeFs::new(cx.executor());
2898
2899        let project = Project::test(fs, None, cx).await;
2900        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2901        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2902
2903        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
2904
2905        pane.update(cx, |pane, cx| {
2906            pane.close_items_to_the_left(&CloseItemsToTheLeft, cx)
2907        })
2908        .unwrap()
2909        .await
2910        .unwrap();
2911        assert_item_labels(&pane, ["C*", "D", "E"], cx);
2912    }
2913
2914    #[gpui::test]
2915    async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
2916        init_test(cx);
2917        let fs = FakeFs::new(cx.executor());
2918
2919        let project = Project::test(fs, None, cx).await;
2920        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2921        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2922
2923        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
2924
2925        pane.update(cx, |pane, cx| {
2926            pane.close_items_to_the_right(&CloseItemsToTheRight, cx)
2927        })
2928        .unwrap()
2929        .await
2930        .unwrap();
2931        assert_item_labels(&pane, ["A", "B", "C*"], cx);
2932    }
2933
2934    #[gpui::test]
2935    async fn test_close_all_items(cx: &mut TestAppContext) {
2936        init_test(cx);
2937        let fs = FakeFs::new(cx.executor());
2938
2939        let project = Project::test(fs, None, cx).await;
2940        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2941        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2942
2943        add_labeled_item(&pane, "A", false, cx);
2944        add_labeled_item(&pane, "B", false, cx);
2945        add_labeled_item(&pane, "C", false, cx);
2946        assert_item_labels(&pane, ["A", "B", "C*"], cx);
2947
2948        pane.update(cx, |pane, cx| {
2949            pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
2950        })
2951        .unwrap()
2952        .await
2953        .unwrap();
2954        assert_item_labels(&pane, [], cx);
2955
2956        add_labeled_item(&pane, "A", true, cx);
2957        add_labeled_item(&pane, "B", true, cx);
2958        add_labeled_item(&pane, "C", true, cx);
2959        assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
2960
2961        let save = pane
2962            .update(cx, |pane, cx| {
2963                pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
2964            })
2965            .unwrap();
2966
2967        cx.executor().run_until_parked();
2968        cx.simulate_prompt_answer(2);
2969        save.await.unwrap();
2970        assert_item_labels(&pane, [], cx);
2971    }
2972
2973    fn init_test(cx: &mut TestAppContext) {
2974        cx.update(|cx| {
2975            let settings_store = SettingsStore::test(cx);
2976            cx.set_global(settings_store);
2977            theme::init(LoadThemes::JustBase, cx);
2978            crate::init_settings(cx);
2979            Project::init_settings(cx);
2980        });
2981    }
2982
2983    fn add_labeled_item(
2984        pane: &View<Pane>,
2985        label: &str,
2986        is_dirty: bool,
2987        cx: &mut VisualTestContext,
2988    ) -> Box<View<TestItem>> {
2989        pane.update(cx, |pane, cx| {
2990            let labeled_item = Box::new(
2991                cx.new_view(|cx| TestItem::new(cx).with_label(label).with_dirty(is_dirty)),
2992            );
2993            pane.add_item(labeled_item.clone(), false, false, None, cx);
2994            labeled_item
2995        })
2996    }
2997
2998    fn set_labeled_items<const COUNT: usize>(
2999        pane: &View<Pane>,
3000        labels: [&str; COUNT],
3001        cx: &mut VisualTestContext,
3002    ) -> [Box<View<TestItem>>; COUNT] {
3003        pane.update(cx, |pane, cx| {
3004            pane.items.clear();
3005            let mut active_item_index = 0;
3006
3007            let mut index = 0;
3008            let items = labels.map(|mut label| {
3009                if label.ends_with('*') {
3010                    label = label.trim_end_matches('*');
3011                    active_item_index = index;
3012                }
3013
3014                let labeled_item = Box::new(cx.new_view(|cx| TestItem::new(cx).with_label(label)));
3015                pane.add_item(labeled_item.clone(), false, false, None, cx);
3016                index += 1;
3017                labeled_item
3018            });
3019
3020            pane.activate_item(active_item_index, false, false, cx);
3021
3022            items
3023        })
3024    }
3025
3026    // Assert the item label, with the active item label suffixed with a '*'
3027    fn assert_item_labels<const COUNT: usize>(
3028        pane: &View<Pane>,
3029        expected_states: [&str; COUNT],
3030        cx: &mut VisualTestContext,
3031    ) {
3032        pane.update(cx, |pane, cx| {
3033            let actual_states = pane
3034                .items
3035                .iter()
3036                .enumerate()
3037                .map(|(ix, item)| {
3038                    let mut state = item
3039                        .to_any()
3040                        .downcast::<TestItem>()
3041                        .unwrap()
3042                        .read(cx)
3043                        .label
3044                        .clone();
3045                    if ix == pane.active_item_index {
3046                        state.push('*');
3047                    }
3048                    if item.is_dirty(cx) {
3049                        state.push('^');
3050                    }
3051                    state
3052                })
3053                .collect::<Vec<_>>();
3054
3055            assert_eq!(
3056                actual_states, expected_states,
3057                "pane items do not match expectation"
3058            );
3059        })
3060    }
3061}
3062
3063impl Render for DraggedTab {
3064    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3065        let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
3066        let label = self.item.tab_content(
3067            TabContentParams {
3068                detail: Some(self.detail),
3069                selected: false,
3070                preview: false,
3071            },
3072            cx,
3073        );
3074        Tab::new("")
3075            .selected(self.is_active)
3076            .child(label)
3077            .render(cx)
3078            .font(ui_font)
3079    }
3080}