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