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