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