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