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