pane.rs

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