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