pane.rs

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