pane.rs

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