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