pane.rs

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