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