pane.rs

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