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