pane.rs

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