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