pane.rs

   1use crate::{
   2    item::{ClosePosition, Item, ItemHandle, ItemSettings, WeakItemHandle},
   3    toolbar::Toolbar,
   4    workspace_settings::{AutosaveSetting, WorkspaceSettings},
   5    NewCenterTerminal, NewFile, NewSearch, 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(paths, true, Some(to_pane.downgrade()), cx)
1903                        .detach();
1904                });
1905            })
1906            .log_err();
1907    }
1908
1909    pub fn display_nav_history_buttons(&mut self, display: bool) {
1910        self.display_nav_history_buttons = display;
1911    }
1912}
1913
1914impl FocusableView for Pane {
1915    fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
1916        self.focus_handle.clone()
1917    }
1918}
1919
1920impl Render for Pane {
1921    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
1922        v_stack()
1923            .key_context("Pane")
1924            .track_focus(&self.focus_handle)
1925            .size_full()
1926            .flex_none()
1927            .overflow_hidden()
1928            .on_action(cx.listener(|pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx)))
1929            .on_action(cx.listener(|pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx)))
1930            .on_action(
1931                cx.listener(|pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx)),
1932            )
1933            .on_action(cx.listener(|pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx)))
1934            .on_action(cx.listener(|pane, _: &GoBack, cx| pane.navigate_backward(cx)))
1935            .on_action(cx.listener(|pane, _: &GoForward, cx| pane.navigate_forward(cx)))
1936            .on_action(cx.listener(Pane::toggle_zoom))
1937            .on_action(cx.listener(|pane: &mut Pane, action: &ActivateItem, cx| {
1938                pane.activate_item(action.0, true, true, cx);
1939            }))
1940            .on_action(cx.listener(|pane: &mut Pane, _: &ActivateLastItem, cx| {
1941                pane.activate_item(pane.items.len() - 1, true, true, cx);
1942            }))
1943            .on_action(cx.listener(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
1944                pane.activate_prev_item(true, cx);
1945            }))
1946            .on_action(cx.listener(|pane: &mut Pane, _: &ActivateNextItem, cx| {
1947                pane.activate_next_item(true, cx);
1948            }))
1949            .on_action(
1950                cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
1951                    pane.close_active_item(action, cx)
1952                        .map(|task| task.detach_and_log_err(cx));
1953                }),
1954            )
1955            .on_action(
1956                cx.listener(|pane: &mut Self, action: &CloseInactiveItems, cx| {
1957                    pane.close_inactive_items(action, cx)
1958                        .map(|task| task.detach_and_log_err(cx));
1959                }),
1960            )
1961            .on_action(
1962                cx.listener(|pane: &mut Self, action: &CloseCleanItems, cx| {
1963                    pane.close_clean_items(action, cx)
1964                        .map(|task| task.detach_and_log_err(cx));
1965                }),
1966            )
1967            .on_action(
1968                cx.listener(|pane: &mut Self, action: &CloseItemsToTheLeft, cx| {
1969                    pane.close_items_to_the_left(action, cx)
1970                        .map(|task| task.detach_and_log_err(cx));
1971                }),
1972            )
1973            .on_action(
1974                cx.listener(|pane: &mut Self, action: &CloseItemsToTheRight, cx| {
1975                    pane.close_items_to_the_right(action, cx)
1976                        .map(|task| task.detach_and_log_err(cx));
1977                }),
1978            )
1979            .on_action(cx.listener(|pane: &mut Self, action: &CloseAllItems, cx| {
1980                pane.close_all_items(action, cx)
1981                    .map(|task| task.detach_and_log_err(cx));
1982            }))
1983            .on_action(
1984                cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
1985                    pane.close_active_item(action, cx)
1986                        .map(|task| task.detach_and_log_err(cx));
1987                }),
1988            )
1989            .on_action(
1990                cx.listener(|pane: &mut Self, action: &RevealInProjectPanel, cx| {
1991                    pane.project.update(cx, |_, cx| {
1992                        cx.emit(project::Event::RevealInProjectPanel(
1993                            ProjectEntryId::from_proto(action.entry_id),
1994                        ))
1995                    })
1996                }),
1997            )
1998            .when(self.active_item().is_some(), |pane| {
1999                pane.child(self.render_tab_bar(cx))
2000            })
2001            .child({
2002                let has_worktrees = self.project.read(cx).worktrees().next().is_some();
2003                // main content
2004                div()
2005                    .flex_1()
2006                    .relative()
2007                    .group("")
2008                    .on_drag_move::<DraggedTab>(cx.listener(Self::handle_drag_move))
2009                    .on_drag_move::<ProjectEntryId>(cx.listener(Self::handle_drag_move))
2010                    .on_drag_move::<ExternalPaths>(cx.listener(Self::handle_drag_move))
2011                    .map(|div| {
2012                        if let Some(item) = self.active_item() {
2013                            div.v_flex()
2014                                .child(self.toolbar.clone())
2015                                .child(item.to_any())
2016                        } else {
2017                            let placeholder = div.h_flex().size_full().justify_center();
2018                            if has_worktrees {
2019                                placeholder
2020                            } else {
2021                                placeholder.child(
2022                                    Label::new("Open a file or project to get started.")
2023                                        .color(Color::Muted),
2024                                )
2025                            }
2026                        }
2027                    })
2028                    .child(
2029                        // drag target
2030                        div()
2031                            .z_index(1)
2032                            .invisible()
2033                            .absolute()
2034                            .bg(theme::color_alpha(
2035                                cx.theme().colors().drop_target_background,
2036                                0.75,
2037                            ))
2038                            .group_drag_over::<DraggedTab>("", |style| style.visible())
2039                            .group_drag_over::<ProjectEntryId>("", |style| style.visible())
2040                            .group_drag_over::<ExternalPaths>("", |style| style.visible())
2041                            .when_some(self.can_drop_predicate.clone(), |this, p| {
2042                                this.can_drop(move |a, cx| p(a, cx))
2043                            })
2044                            .on_drop(cx.listener(move |this, dragged_tab, cx| {
2045                                this.handle_tab_drop(dragged_tab, this.active_item_index(), cx)
2046                            }))
2047                            .on_drop(cx.listener(move |this, entry_id, cx| {
2048                                this.handle_project_entry_drop(entry_id, cx)
2049                            }))
2050                            .on_drop(cx.listener(move |this, paths, cx| {
2051                                this.handle_external_paths_drop(paths, cx)
2052                            }))
2053                            .map(|div| match self.drag_split_direction {
2054                                None => div.top_0().left_0().right_0().bottom_0(),
2055                                Some(SplitDirection::Up) => div.top_0().left_0().right_0().h_32(),
2056                                Some(SplitDirection::Down) => {
2057                                    div.left_0().bottom_0().right_0().h_32()
2058                                }
2059                                Some(SplitDirection::Left) => {
2060                                    div.top_0().left_0().bottom_0().w_32()
2061                                }
2062                                Some(SplitDirection::Right) => {
2063                                    div.top_0().bottom_0().right_0().w_32()
2064                                }
2065                            }),
2066                    )
2067            })
2068            .on_mouse_down(
2069                MouseButton::Navigate(NavigationDirection::Back),
2070                cx.listener(|pane, _, cx| {
2071                    if let Some(workspace) = pane.workspace.upgrade() {
2072                        let pane = cx.view().downgrade();
2073                        cx.window_context().defer(move |cx| {
2074                            workspace.update(cx, |workspace, cx| {
2075                                workspace.go_back(pane, cx).detach_and_log_err(cx)
2076                            })
2077                        })
2078                    }
2079                }),
2080            )
2081            .on_mouse_down(
2082                MouseButton::Navigate(NavigationDirection::Forward),
2083                cx.listener(|pane, _, cx| {
2084                    if let Some(workspace) = pane.workspace.upgrade() {
2085                        let pane = cx.view().downgrade();
2086                        cx.window_context().defer(move |cx| {
2087                            workspace.update(cx, |workspace, cx| {
2088                                workspace.go_forward(pane, cx).detach_and_log_err(cx)
2089                            })
2090                        })
2091                    }
2092                }),
2093            )
2094    }
2095}
2096
2097impl ItemNavHistory {
2098    pub fn push<D: 'static + Send + Any>(&mut self, data: Option<D>, cx: &mut WindowContext) {
2099        self.history.push(data, self.item.clone(), cx);
2100    }
2101
2102    pub fn pop_backward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
2103        self.history.pop(NavigationMode::GoingBack, cx)
2104    }
2105
2106    pub fn pop_forward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
2107        self.history.pop(NavigationMode::GoingForward, cx)
2108    }
2109}
2110
2111impl NavHistory {
2112    pub fn for_each_entry(
2113        &self,
2114        cx: &AppContext,
2115        mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
2116    ) {
2117        let borrowed_history = self.0.lock();
2118        borrowed_history
2119            .forward_stack
2120            .iter()
2121            .chain(borrowed_history.backward_stack.iter())
2122            .chain(borrowed_history.closed_stack.iter())
2123            .for_each(|entry| {
2124                if let Some(project_and_abs_path) =
2125                    borrowed_history.paths_by_item.get(&entry.item.id())
2126                {
2127                    f(entry, project_and_abs_path.clone());
2128                } else if let Some(item) = entry.item.upgrade() {
2129                    if let Some(path) = item.project_path(cx) {
2130                        f(entry, (path, None));
2131                    }
2132                }
2133            })
2134    }
2135
2136    pub fn set_mode(&mut self, mode: NavigationMode) {
2137        self.0.lock().mode = mode;
2138    }
2139
2140    pub fn mode(&self) -> NavigationMode {
2141        self.0.lock().mode
2142    }
2143
2144    pub fn disable(&mut self) {
2145        self.0.lock().mode = NavigationMode::Disabled;
2146    }
2147
2148    pub fn enable(&mut self) {
2149        self.0.lock().mode = NavigationMode::Normal;
2150    }
2151
2152    pub fn pop(&mut self, mode: NavigationMode, cx: &mut WindowContext) -> Option<NavigationEntry> {
2153        let mut state = self.0.lock();
2154        let entry = match mode {
2155            NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
2156                return None
2157            }
2158            NavigationMode::GoingBack => &mut state.backward_stack,
2159            NavigationMode::GoingForward => &mut state.forward_stack,
2160            NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
2161        }
2162        .pop_back();
2163        if entry.is_some() {
2164            state.did_update(cx);
2165        }
2166        entry
2167    }
2168
2169    pub fn push<D: 'static + Send + Any>(
2170        &mut self,
2171        data: Option<D>,
2172        item: Arc<dyn WeakItemHandle>,
2173        cx: &mut WindowContext,
2174    ) {
2175        let state = &mut *self.0.lock();
2176        match state.mode {
2177            NavigationMode::Disabled => {}
2178            NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
2179                if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2180                    state.backward_stack.pop_front();
2181                }
2182                state.backward_stack.push_back(NavigationEntry {
2183                    item,
2184                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2185                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2186                });
2187                state.forward_stack.clear();
2188            }
2189            NavigationMode::GoingBack => {
2190                if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2191                    state.forward_stack.pop_front();
2192                }
2193                state.forward_stack.push_back(NavigationEntry {
2194                    item,
2195                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2196                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2197                });
2198            }
2199            NavigationMode::GoingForward => {
2200                if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2201                    state.backward_stack.pop_front();
2202                }
2203                state.backward_stack.push_back(NavigationEntry {
2204                    item,
2205                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2206                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2207                });
2208            }
2209            NavigationMode::ClosingItem => {
2210                if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2211                    state.closed_stack.pop_front();
2212                }
2213                state.closed_stack.push_back(NavigationEntry {
2214                    item,
2215                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2216                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2217                });
2218            }
2219        }
2220        state.did_update(cx);
2221    }
2222
2223    pub fn remove_item(&mut self, item_id: EntityId) {
2224        let mut state = self.0.lock();
2225        state.paths_by_item.remove(&item_id);
2226        state
2227            .backward_stack
2228            .retain(|entry| entry.item.id() != item_id);
2229        state
2230            .forward_stack
2231            .retain(|entry| entry.item.id() != item_id);
2232        state
2233            .closed_stack
2234            .retain(|entry| entry.item.id() != item_id);
2235    }
2236
2237    pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option<PathBuf>)> {
2238        self.0.lock().paths_by_item.get(&item_id).cloned()
2239    }
2240}
2241
2242impl NavHistoryState {
2243    pub fn did_update(&self, cx: &mut WindowContext) {
2244        if let Some(pane) = self.pane.upgrade() {
2245            cx.defer(move |cx| {
2246                pane.update(cx, |pane, cx| pane.history_updated(cx));
2247            });
2248        }
2249    }
2250}
2251
2252fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
2253    let path = buffer_path
2254        .as_ref()
2255        .and_then(|p| p.path.to_str())
2256        .unwrap_or(&"This buffer");
2257    let path = truncate_and_remove_front(path, 80);
2258    format!("{path} contains unsaved edits. Do you want to save it?")
2259}
2260
2261#[cfg(test)]
2262mod tests {
2263    use super::*;
2264    use crate::item::test::{TestItem, TestProjectItem};
2265    use gpui::{TestAppContext, VisualTestContext};
2266    use project::FakeFs;
2267    use settings::SettingsStore;
2268    use theme::LoadThemes;
2269
2270    #[gpui::test]
2271    async fn test_remove_active_empty(cx: &mut TestAppContext) {
2272        init_test(cx);
2273        let fs = FakeFs::new(cx.executor());
2274
2275        let project = Project::test(fs, None, cx).await;
2276        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2277        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2278
2279        pane.update(cx, |pane, cx| {
2280            assert!(pane
2281                .close_active_item(&CloseActiveItem { save_intent: None }, cx)
2282                .is_none())
2283        });
2284    }
2285
2286    #[gpui::test]
2287    async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
2288        init_test(cx);
2289        let fs = FakeFs::new(cx.executor());
2290
2291        let project = Project::test(fs, None, cx).await;
2292        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2293        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2294
2295        // 1. Add with a destination index
2296        //   a. Add before the active item
2297        set_labeled_items(&pane, ["A", "B*", "C"], cx);
2298        pane.update(cx, |pane, cx| {
2299            pane.add_item(
2300                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2301                false,
2302                false,
2303                Some(0),
2304                cx,
2305            );
2306        });
2307        assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
2308
2309        //   b. Add after the active item
2310        set_labeled_items(&pane, ["A", "B*", "C"], cx);
2311        pane.update(cx, |pane, cx| {
2312            pane.add_item(
2313                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2314                false,
2315                false,
2316                Some(2),
2317                cx,
2318            );
2319        });
2320        assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
2321
2322        //   c. Add at the end of the item list (including off the length)
2323        set_labeled_items(&pane, ["A", "B*", "C"], cx);
2324        pane.update(cx, |pane, cx| {
2325            pane.add_item(
2326                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2327                false,
2328                false,
2329                Some(5),
2330                cx,
2331            );
2332        });
2333        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2334
2335        // 2. Add without a destination index
2336        //   a. Add with active item at the start of the item list
2337        set_labeled_items(&pane, ["A*", "B", "C"], cx);
2338        pane.update(cx, |pane, cx| {
2339            pane.add_item(
2340                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2341                false,
2342                false,
2343                None,
2344                cx,
2345            );
2346        });
2347        set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
2348
2349        //   b. Add with active item at the end of the item list
2350        set_labeled_items(&pane, ["A", "B", "C*"], cx);
2351        pane.update(cx, |pane, cx| {
2352            pane.add_item(
2353                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2354                false,
2355                false,
2356                None,
2357                cx,
2358            );
2359        });
2360        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2361    }
2362
2363    #[gpui::test]
2364    async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
2365        init_test(cx);
2366        let fs = FakeFs::new(cx.executor());
2367
2368        let project = Project::test(fs, None, cx).await;
2369        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2370        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2371
2372        // 1. Add with a destination index
2373        //   1a. Add before the active item
2374        let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
2375        pane.update(cx, |pane, cx| {
2376            pane.add_item(d, false, false, Some(0), cx);
2377        });
2378        assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
2379
2380        //   1b. Add after the active item
2381        let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
2382        pane.update(cx, |pane, cx| {
2383            pane.add_item(d, false, false, Some(2), cx);
2384        });
2385        assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
2386
2387        //   1c. Add at the end of the item list (including off the length)
2388        let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
2389        pane.update(cx, |pane, cx| {
2390            pane.add_item(a, false, false, Some(5), cx);
2391        });
2392        assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
2393
2394        //   1d. Add same item to active index
2395        let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
2396        pane.update(cx, |pane, cx| {
2397            pane.add_item(b, false, false, Some(1), cx);
2398        });
2399        assert_item_labels(&pane, ["A", "B*", "C"], cx);
2400
2401        //   1e. Add item to index after same item in last position
2402        let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
2403        pane.update(cx, |pane, cx| {
2404            pane.add_item(c, false, false, Some(2), cx);
2405        });
2406        assert_item_labels(&pane, ["A", "B", "C*"], cx);
2407
2408        // 2. Add without a destination index
2409        //   2a. Add with active item at the start of the item list
2410        let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
2411        pane.update(cx, |pane, cx| {
2412            pane.add_item(d, false, false, None, cx);
2413        });
2414        assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
2415
2416        //   2b. Add with active item at the end of the item list
2417        let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
2418        pane.update(cx, |pane, cx| {
2419            pane.add_item(a, false, false, None, cx);
2420        });
2421        assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
2422
2423        //   2c. Add active item to active item at end of list
2424        let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
2425        pane.update(cx, |pane, cx| {
2426            pane.add_item(c, false, false, None, cx);
2427        });
2428        assert_item_labels(&pane, ["A", "B", "C*"], cx);
2429
2430        //   2d. Add active item to active item at start of list
2431        let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
2432        pane.update(cx, |pane, cx| {
2433            pane.add_item(a, false, false, None, cx);
2434        });
2435        assert_item_labels(&pane, ["A*", "B", "C"], cx);
2436    }
2437
2438    #[gpui::test]
2439    async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
2440        init_test(cx);
2441        let fs = FakeFs::new(cx.executor());
2442
2443        let project = Project::test(fs, None, cx).await;
2444        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2445        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2446
2447        // singleton view
2448        pane.update(cx, |pane, cx| {
2449            pane.add_item(
2450                Box::new(cx.new_view(|cx| {
2451                    TestItem::new(cx)
2452                        .with_singleton(true)
2453                        .with_label("buffer 1")
2454                        .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
2455                })),
2456                false,
2457                false,
2458                None,
2459                cx,
2460            );
2461        });
2462        assert_item_labels(&pane, ["buffer 1*"], cx);
2463
2464        // new singleton view with the same project entry
2465        pane.update(cx, |pane, cx| {
2466            pane.add_item(
2467                Box::new(cx.new_view(|cx| {
2468                    TestItem::new(cx)
2469                        .with_singleton(true)
2470                        .with_label("buffer 1")
2471                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
2472                })),
2473                false,
2474                false,
2475                None,
2476                cx,
2477            );
2478        });
2479        assert_item_labels(&pane, ["buffer 1*"], cx);
2480
2481        // new singleton view with different project entry
2482        pane.update(cx, |pane, cx| {
2483            pane.add_item(
2484                Box::new(cx.new_view(|cx| {
2485                    TestItem::new(cx)
2486                        .with_singleton(true)
2487                        .with_label("buffer 2")
2488                        .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
2489                })),
2490                false,
2491                false,
2492                None,
2493                cx,
2494            );
2495        });
2496        assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
2497
2498        // new multibuffer view with the same project entry
2499        pane.update(cx, |pane, cx| {
2500            pane.add_item(
2501                Box::new(cx.new_view(|cx| {
2502                    TestItem::new(cx)
2503                        .with_singleton(false)
2504                        .with_label("multibuffer 1")
2505                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
2506                })),
2507                false,
2508                false,
2509                None,
2510                cx,
2511            );
2512        });
2513        assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
2514
2515        // another multibuffer view with the same project entry
2516        pane.update(cx, |pane, cx| {
2517            pane.add_item(
2518                Box::new(cx.new_view(|cx| {
2519                    TestItem::new(cx)
2520                        .with_singleton(false)
2521                        .with_label("multibuffer 1b")
2522                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
2523                })),
2524                false,
2525                false,
2526                None,
2527                cx,
2528            );
2529        });
2530        assert_item_labels(
2531            &pane,
2532            ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
2533            cx,
2534        );
2535    }
2536
2537    #[gpui::test]
2538    async fn test_remove_item_ordering(cx: &mut TestAppContext) {
2539        init_test(cx);
2540        let fs = FakeFs::new(cx.executor());
2541
2542        let project = Project::test(fs, None, cx).await;
2543        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2544        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2545
2546        add_labeled_item(&pane, "A", false, cx);
2547        add_labeled_item(&pane, "B", false, cx);
2548        add_labeled_item(&pane, "C", false, cx);
2549        add_labeled_item(&pane, "D", false, cx);
2550        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2551
2552        pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
2553        add_labeled_item(&pane, "1", false, cx);
2554        assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
2555
2556        pane.update(cx, |pane, cx| {
2557            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
2558        })
2559        .unwrap()
2560        .await
2561        .unwrap();
2562        assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
2563
2564        pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
2565        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2566
2567        pane.update(cx, |pane, cx| {
2568            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
2569        })
2570        .unwrap()
2571        .await
2572        .unwrap();
2573        assert_item_labels(&pane, ["A", "B*", "C"], cx);
2574
2575        pane.update(cx, |pane, cx| {
2576            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
2577        })
2578        .unwrap()
2579        .await
2580        .unwrap();
2581        assert_item_labels(&pane, ["A", "C*"], cx);
2582
2583        pane.update(cx, |pane, cx| {
2584            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
2585        })
2586        .unwrap()
2587        .await
2588        .unwrap();
2589        assert_item_labels(&pane, ["A*"], cx);
2590    }
2591
2592    #[gpui::test]
2593    async fn test_close_inactive_items(cx: &mut TestAppContext) {
2594        init_test(cx);
2595        let fs = FakeFs::new(cx.executor());
2596
2597        let project = Project::test(fs, None, cx).await;
2598        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2599        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2600
2601        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
2602
2603        pane.update(cx, |pane, cx| {
2604            pane.close_inactive_items(&CloseInactiveItems, cx)
2605        })
2606        .unwrap()
2607        .await
2608        .unwrap();
2609        assert_item_labels(&pane, ["C*"], cx);
2610    }
2611
2612    #[gpui::test]
2613    async fn test_close_clean_items(cx: &mut TestAppContext) {
2614        init_test(cx);
2615        let fs = FakeFs::new(cx.executor());
2616
2617        let project = Project::test(fs, None, cx).await;
2618        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2619        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2620
2621        add_labeled_item(&pane, "A", true, cx);
2622        add_labeled_item(&pane, "B", false, cx);
2623        add_labeled_item(&pane, "C", true, cx);
2624        add_labeled_item(&pane, "D", false, cx);
2625        add_labeled_item(&pane, "E", false, cx);
2626        assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
2627
2628        pane.update(cx, |pane, cx| pane.close_clean_items(&CloseCleanItems, cx))
2629            .unwrap()
2630            .await
2631            .unwrap();
2632        assert_item_labels(&pane, ["A^", "C*^"], cx);
2633    }
2634
2635    #[gpui::test]
2636    async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
2637        init_test(cx);
2638        let fs = FakeFs::new(cx.executor());
2639
2640        let project = Project::test(fs, None, cx).await;
2641        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2642        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2643
2644        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
2645
2646        pane.update(cx, |pane, cx| {
2647            pane.close_items_to_the_left(&CloseItemsToTheLeft, cx)
2648        })
2649        .unwrap()
2650        .await
2651        .unwrap();
2652        assert_item_labels(&pane, ["C*", "D", "E"], cx);
2653    }
2654
2655    #[gpui::test]
2656    async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
2657        init_test(cx);
2658        let fs = FakeFs::new(cx.executor());
2659
2660        let project = Project::test(fs, None, cx).await;
2661        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2662        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2663
2664        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
2665
2666        pane.update(cx, |pane, cx| {
2667            pane.close_items_to_the_right(&CloseItemsToTheRight, cx)
2668        })
2669        .unwrap()
2670        .await
2671        .unwrap();
2672        assert_item_labels(&pane, ["A", "B", "C*"], cx);
2673    }
2674
2675    #[gpui::test]
2676    async fn test_close_all_items(cx: &mut TestAppContext) {
2677        init_test(cx);
2678        let fs = FakeFs::new(cx.executor());
2679
2680        let project = Project::test(fs, None, cx).await;
2681        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2682        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2683
2684        add_labeled_item(&pane, "A", false, cx);
2685        add_labeled_item(&pane, "B", false, cx);
2686        add_labeled_item(&pane, "C", false, cx);
2687        assert_item_labels(&pane, ["A", "B", "C*"], cx);
2688
2689        pane.update(cx, |pane, cx| {
2690            pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
2691        })
2692        .unwrap()
2693        .await
2694        .unwrap();
2695        assert_item_labels(&pane, [], cx);
2696
2697        add_labeled_item(&pane, "A", true, cx);
2698        add_labeled_item(&pane, "B", true, cx);
2699        add_labeled_item(&pane, "C", true, cx);
2700        assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
2701
2702        let save = pane
2703            .update(cx, |pane, cx| {
2704                pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
2705            })
2706            .unwrap();
2707
2708        cx.executor().run_until_parked();
2709        cx.simulate_prompt_answer(2);
2710        save.await.unwrap();
2711        assert_item_labels(&pane, [], cx);
2712    }
2713
2714    fn init_test(cx: &mut TestAppContext) {
2715        cx.update(|cx| {
2716            let settings_store = SettingsStore::test(cx);
2717            cx.set_global(settings_store);
2718            theme::init(LoadThemes::JustBase, cx);
2719            crate::init_settings(cx);
2720            Project::init_settings(cx);
2721        });
2722    }
2723
2724    fn add_labeled_item(
2725        pane: &View<Pane>,
2726        label: &str,
2727        is_dirty: bool,
2728        cx: &mut VisualTestContext,
2729    ) -> Box<View<TestItem>> {
2730        pane.update(cx, |pane, cx| {
2731            let labeled_item = Box::new(
2732                cx.new_view(|cx| TestItem::new(cx).with_label(label).with_dirty(is_dirty)),
2733            );
2734            pane.add_item(labeled_item.clone(), false, false, None, cx);
2735            labeled_item
2736        })
2737    }
2738
2739    fn set_labeled_items<const COUNT: usize>(
2740        pane: &View<Pane>,
2741        labels: [&str; COUNT],
2742        cx: &mut VisualTestContext,
2743    ) -> [Box<View<TestItem>>; COUNT] {
2744        pane.update(cx, |pane, cx| {
2745            pane.items.clear();
2746            let mut active_item_index = 0;
2747
2748            let mut index = 0;
2749            let items = labels.map(|mut label| {
2750                if label.ends_with("*") {
2751                    label = label.trim_end_matches("*");
2752                    active_item_index = index;
2753                }
2754
2755                let labeled_item = Box::new(cx.new_view(|cx| TestItem::new(cx).with_label(label)));
2756                pane.add_item(labeled_item.clone(), false, false, None, cx);
2757                index += 1;
2758                labeled_item
2759            });
2760
2761            pane.activate_item(active_item_index, false, false, cx);
2762
2763            items
2764        })
2765    }
2766
2767    // Assert the item label, with the active item label suffixed with a '*'
2768    fn assert_item_labels<const COUNT: usize>(
2769        pane: &View<Pane>,
2770        expected_states: [&str; COUNT],
2771        cx: &mut VisualTestContext,
2772    ) {
2773        pane.update(cx, |pane, cx| {
2774            let actual_states = pane
2775                .items
2776                .iter()
2777                .enumerate()
2778                .map(|(ix, item)| {
2779                    let mut state = item
2780                        .to_any()
2781                        .downcast::<TestItem>()
2782                        .unwrap()
2783                        .read(cx)
2784                        .label
2785                        .clone();
2786                    if ix == pane.active_item_index {
2787                        state.push('*');
2788                    }
2789                    if item.is_dirty(cx) {
2790                        state.push('^');
2791                    }
2792                    state
2793                })
2794                .collect::<Vec<_>>();
2795
2796            assert_eq!(
2797                actual_states, expected_states,
2798                "pane items do not match expectation"
2799            );
2800        })
2801    }
2802}
2803
2804impl Render for DraggedTab {
2805    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2806        let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
2807        let item = &self.pane.read(cx).items[self.ix];
2808        let label = item.tab_content(Some(self.detail), false, cx);
2809        Tab::new("")
2810            .selected(self.is_active)
2811            .child(label)
2812            .render(cx)
2813            .font(ui_font)
2814    }
2815}