pane.rs

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