pane.rs

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