pane.rs

   1mod dragged_item_receiver;
   2
   3use super::{ItemHandle, SplitDirection};
   4pub use crate::toolbar::Toolbar;
   5use crate::{
   6    item::WeakItemHandle, notify_of_new_dock, AutosaveSetting, Item, NewCenterTerminal, NewFile,
   7    NewSearch, ToggleZoom, Workspace, WorkspaceSettings,
   8};
   9use anyhow::Result;
  10use collections::{HashMap, HashSet, VecDeque};
  11use context_menu::{ContextMenu, ContextMenuItem};
  12use drag_and_drop::{DragAndDrop, Draggable};
  13use dragged_item_receiver::dragged_item_receiver;
  14use futures::StreamExt;
  15use gpui::{
  16    actions,
  17    elements::*,
  18    geometry::{
  19        rect::RectF,
  20        vector::{vec2f, Vector2F},
  21    },
  22    impl_actions,
  23    keymap_matcher::KeymapContext,
  24    platform::{CursorStyle, MouseButton, NavigationDirection, PromptLevel},
  25    Action, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, EventContext,
  26    LayoutContext, ModelHandle, MouseRegion, Quad, Task, View, ViewContext, ViewHandle,
  27    WeakViewHandle, WindowContext,
  28};
  29use project::{Project, ProjectEntryId, ProjectPath};
  30use serde::Deserialize;
  31use std::{
  32    any::Any,
  33    cell::RefCell,
  34    cmp, mem,
  35    path::{Path, PathBuf},
  36    rc::Rc,
  37    sync::{
  38        atomic::{AtomicUsize, Ordering},
  39        Arc,
  40    },
  41};
  42use theme::{Theme, ThemeSettings};
  43
  44#[derive(Clone, Deserialize, PartialEq)]
  45pub struct ActivateItem(pub usize);
  46
  47#[derive(Clone, PartialEq)]
  48pub struct CloseItemById {
  49    pub item_id: usize,
  50    pub pane: WeakViewHandle<Pane>,
  51}
  52
  53#[derive(Clone, PartialEq)]
  54pub struct CloseItemsToTheLeftById {
  55    pub item_id: usize,
  56    pub pane: WeakViewHandle<Pane>,
  57}
  58
  59#[derive(Clone, PartialEq)]
  60pub struct CloseItemsToTheRightById {
  61    pub item_id: usize,
  62    pub pane: WeakViewHandle<Pane>,
  63}
  64
  65actions!(
  66    pane,
  67    [
  68        ActivatePrevItem,
  69        ActivateNextItem,
  70        ActivateLastItem,
  71        CloseActiveItem,
  72        CloseInactiveItems,
  73        CloseCleanItems,
  74        CloseItemsToTheLeft,
  75        CloseItemsToTheRight,
  76        CloseAllItems,
  77        GoBack,
  78        GoForward,
  79        ReopenClosedItem,
  80        SplitLeft,
  81        SplitUp,
  82        SplitRight,
  83        SplitDown,
  84    ]
  85);
  86
  87impl_actions!(pane, [ActivateItem]);
  88
  89const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
  90
  91pub type BackgroundActions = fn() -> &'static [(&'static str, &'static dyn Action)];
  92
  93pub fn init(cx: &mut AppContext) {
  94    cx.add_action(Pane::toggle_zoom);
  95    cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| {
  96        pane.activate_item(action.0, true, true, cx);
  97    });
  98    cx.add_action(|pane: &mut Pane, _: &ActivateLastItem, cx| {
  99        pane.activate_item(pane.items.len() - 1, true, true, cx);
 100    });
 101    cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
 102        pane.activate_prev_item(true, cx);
 103    });
 104    cx.add_action(|pane: &mut Pane, _: &ActivateNextItem, cx| {
 105        pane.activate_next_item(true, cx);
 106    });
 107    cx.add_async_action(Pane::close_active_item);
 108    cx.add_async_action(Pane::close_inactive_items);
 109    cx.add_async_action(Pane::close_clean_items);
 110    cx.add_async_action(Pane::close_items_to_the_left);
 111    cx.add_async_action(Pane::close_items_to_the_right);
 112    cx.add_async_action(Pane::close_all_items);
 113    cx.add_action(|pane: &mut Pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx));
 114    cx.add_action(|pane: &mut Pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx));
 115    cx.add_action(|pane: &mut Pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx));
 116    cx.add_action(|pane: &mut Pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx));
 117}
 118
 119#[derive(Debug)]
 120pub enum Event {
 121    AddItem { item: Box<dyn ItemHandle> },
 122    ActivateItem { local: bool },
 123    Remove,
 124    RemoveItem { item_id: usize },
 125    Split(SplitDirection),
 126    ChangeItemTitle,
 127    Focus,
 128    ZoomIn,
 129    ZoomOut,
 130}
 131
 132pub struct Pane {
 133    items: Vec<Box<dyn ItemHandle>>,
 134    activation_history: Vec<usize>,
 135    zoomed: bool,
 136    active_item_index: usize,
 137    last_focused_view_by_item: HashMap<usize, AnyWeakViewHandle>,
 138    autoscroll: bool,
 139    nav_history: NavHistory,
 140    toolbar: ViewHandle<Toolbar>,
 141    tab_bar_context_menu: TabBarContextMenu,
 142    tab_context_menu: ViewHandle<ContextMenu>,
 143    _background_actions: BackgroundActions,
 144    workspace: WeakViewHandle<Workspace>,
 145    project: ModelHandle<Project>,
 146    has_focus: bool,
 147    can_drop: Rc<dyn Fn(&DragAndDrop<Workspace>, &WindowContext) -> bool>,
 148    can_split: bool,
 149    render_tab_bar_buttons: Rc<dyn Fn(&mut Pane, &mut ViewContext<Pane>) -> AnyElement<Pane>>,
 150}
 151
 152pub struct ItemNavHistory {
 153    history: NavHistory,
 154    item: Rc<dyn WeakItemHandle>,
 155}
 156
 157#[derive(Clone)]
 158pub struct NavHistory(Rc<RefCell<NavHistoryState>>);
 159
 160struct NavHistoryState {
 161    mode: NavigationMode,
 162    backward_stack: VecDeque<NavigationEntry>,
 163    forward_stack: VecDeque<NavigationEntry>,
 164    closed_stack: VecDeque<NavigationEntry>,
 165    paths_by_item: HashMap<usize, (ProjectPath, Option<PathBuf>)>,
 166    pane: WeakViewHandle<Pane>,
 167    next_timestamp: Arc<AtomicUsize>,
 168}
 169
 170#[derive(Copy, Clone)]
 171pub enum NavigationMode {
 172    Normal,
 173    GoingBack,
 174    GoingForward,
 175    ClosingItem,
 176    ReopeningClosedItem,
 177    Disabled,
 178}
 179
 180impl Default for NavigationMode {
 181    fn default() -> Self {
 182        Self::Normal
 183    }
 184}
 185
 186pub struct NavigationEntry {
 187    pub item: Rc<dyn WeakItemHandle>,
 188    pub data: Option<Box<dyn Any>>,
 189    pub timestamp: usize,
 190}
 191
 192pub struct DraggedItem {
 193    pub handle: Box<dyn ItemHandle>,
 194    pub pane: WeakViewHandle<Pane>,
 195}
 196
 197pub enum ReorderBehavior {
 198    None,
 199    MoveAfterActive,
 200    MoveToIndex(usize),
 201}
 202
 203#[derive(Debug, Clone, Copy, PartialEq, Eq)]
 204enum TabBarContextMenuKind {
 205    New,
 206    Split,
 207}
 208
 209struct TabBarContextMenu {
 210    kind: TabBarContextMenuKind,
 211    handle: ViewHandle<ContextMenu>,
 212}
 213
 214impl TabBarContextMenu {
 215    fn handle_if_kind(&self, kind: TabBarContextMenuKind) -> Option<ViewHandle<ContextMenu>> {
 216        if self.kind == kind {
 217            return Some(self.handle.clone());
 218        }
 219        None
 220    }
 221}
 222
 223impl Pane {
 224    pub fn new(
 225        workspace: WeakViewHandle<Workspace>,
 226        project: ModelHandle<Project>,
 227        background_actions: BackgroundActions,
 228        next_timestamp: Arc<AtomicUsize>,
 229        cx: &mut ViewContext<Self>,
 230    ) -> Self {
 231        let pane_view_id = cx.view_id();
 232        let handle = cx.weak_handle();
 233        let context_menu = cx.add_view(|cx| ContextMenu::new(pane_view_id, cx));
 234        context_menu.update(cx, |menu, _| {
 235            menu.set_position_mode(OverlayPositionMode::Local)
 236        });
 237
 238        Self {
 239            items: Vec::new(),
 240            activation_history: Vec::new(),
 241            zoomed: false,
 242            active_item_index: 0,
 243            last_focused_view_by_item: Default::default(),
 244            autoscroll: false,
 245            nav_history: NavHistory(Rc::new(RefCell::new(NavHistoryState {
 246                mode: NavigationMode::Normal,
 247                backward_stack: Default::default(),
 248                forward_stack: Default::default(),
 249                closed_stack: Default::default(),
 250                paths_by_item: Default::default(),
 251                pane: handle.clone(),
 252                next_timestamp,
 253            }))),
 254            toolbar: cx.add_view(|_| Toolbar::new(Some(handle))),
 255            tab_bar_context_menu: TabBarContextMenu {
 256                kind: TabBarContextMenuKind::New,
 257                handle: context_menu,
 258            },
 259            tab_context_menu: cx.add_view(|cx| ContextMenu::new(pane_view_id, cx)),
 260            _background_actions: background_actions,
 261            workspace,
 262            project,
 263            has_focus: false,
 264            can_drop: Rc::new(|_, _| true),
 265            can_split: true,
 266            render_tab_bar_buttons: Rc::new(|pane, cx| {
 267                Flex::row()
 268                    // New menu
 269                    .with_child(Self::render_tab_bar_button(
 270                        0,
 271                        "icons/plus_12.svg",
 272                        false,
 273                        Some(("New...".into(), None)),
 274                        cx,
 275                        |pane, cx| pane.deploy_new_menu(cx),
 276                        pane.tab_bar_context_menu
 277                            .handle_if_kind(TabBarContextMenuKind::New),
 278                    ))
 279                    .with_child(Self::render_tab_bar_button(
 280                        1,
 281                        "icons/split_12.svg",
 282                        false,
 283                        Some(("Split Pane".into(), None)),
 284                        cx,
 285                        |pane, cx| pane.deploy_split_menu(cx),
 286                        pane.tab_bar_context_menu
 287                            .handle_if_kind(TabBarContextMenuKind::Split),
 288                    ))
 289                    .with_child(Pane::render_tab_bar_button(
 290                        2,
 291                        if pane.is_zoomed() {
 292                            "icons/minimize_8.svg"
 293                        } else {
 294                            "icons/maximize_8.svg"
 295                        },
 296                        pane.is_zoomed(),
 297                        Some(("Toggle Zoom".into(), Some(Box::new(ToggleZoom)))),
 298                        cx,
 299                        move |pane, cx| pane.toggle_zoom(&Default::default(), cx),
 300                        None,
 301                    ))
 302                    .into_any()
 303            }),
 304        }
 305    }
 306
 307    pub(crate) fn workspace(&self) -> &WeakViewHandle<Workspace> {
 308        &self.workspace
 309    }
 310
 311    pub fn has_focus(&self) -> bool {
 312        self.has_focus
 313    }
 314
 315    pub fn active_item_index(&self) -> usize {
 316        self.active_item_index
 317    }
 318
 319    pub fn on_can_drop<F>(&mut self, can_drop: F)
 320    where
 321        F: 'static + Fn(&DragAndDrop<Workspace>, &WindowContext) -> bool,
 322    {
 323        self.can_drop = Rc::new(can_drop);
 324    }
 325
 326    pub fn set_can_split(&mut self, can_split: bool, cx: &mut ViewContext<Self>) {
 327        self.can_split = can_split;
 328        cx.notify();
 329    }
 330
 331    pub fn set_can_navigate(&mut self, can_navigate: bool, cx: &mut ViewContext<Self>) {
 332        self.toolbar.update(cx, |toolbar, cx| {
 333            toolbar.set_can_navigate(can_navigate, cx);
 334        });
 335        cx.notify();
 336    }
 337
 338    pub fn set_render_tab_bar_buttons<F>(&mut self, cx: &mut ViewContext<Self>, render: F)
 339    where
 340        F: 'static + Fn(&mut Pane, &mut ViewContext<Pane>) -> AnyElement<Pane>,
 341    {
 342        self.render_tab_bar_buttons = Rc::new(render);
 343        cx.notify();
 344    }
 345
 346    pub fn nav_history_for_item<T: Item>(&self, item: &ViewHandle<T>) -> ItemNavHistory {
 347        ItemNavHistory {
 348            history: self.nav_history.clone(),
 349            item: Rc::new(item.downgrade()),
 350        }
 351    }
 352
 353    pub fn nav_history(&self) -> &NavHistory {
 354        &self.nav_history
 355    }
 356
 357    pub fn nav_history_mut(&mut self) -> &mut NavHistory {
 358        &mut self.nav_history
 359    }
 360
 361    pub fn disable_history(&mut self) {
 362        self.nav_history.disable();
 363    }
 364
 365    pub fn enable_history(&mut self) {
 366        self.nav_history.enable();
 367    }
 368
 369    pub fn can_navigate_backward(&self) -> bool {
 370        !self.nav_history.0.borrow().backward_stack.is_empty()
 371    }
 372
 373    pub fn can_navigate_forward(&self) -> bool {
 374        !self.nav_history.0.borrow().forward_stack.is_empty()
 375    }
 376
 377    fn history_updated(&mut self, cx: &mut ViewContext<Self>) {
 378        self.toolbar.update(cx, |_, cx| cx.notify());
 379    }
 380
 381    pub(crate) fn open_item(
 382        &mut self,
 383        project_entry_id: ProjectEntryId,
 384        focus_item: bool,
 385        cx: &mut ViewContext<Self>,
 386        build_item: impl FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
 387    ) -> Box<dyn ItemHandle> {
 388        let mut existing_item = None;
 389        for (index, item) in self.items.iter().enumerate() {
 390            if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [project_entry_id]
 391            {
 392                let item = item.boxed_clone();
 393                existing_item = Some((index, item));
 394                break;
 395            }
 396        }
 397
 398        if let Some((index, existing_item)) = existing_item {
 399            self.activate_item(index, focus_item, focus_item, cx);
 400            existing_item
 401        } else {
 402            let new_item = build_item(cx);
 403            self.add_item(new_item.clone(), true, focus_item, None, cx);
 404            new_item
 405        }
 406    }
 407
 408    pub fn add_item(
 409        &mut self,
 410        item: Box<dyn ItemHandle>,
 411        activate_pane: bool,
 412        focus_item: bool,
 413        destination_index: Option<usize>,
 414        cx: &mut ViewContext<Self>,
 415    ) {
 416        if item.is_singleton(cx) {
 417            if let Some(&entry_id) = item.project_entry_ids(cx).get(0) {
 418                let project = self.project.read(cx);
 419                if let Some(project_path) = project.path_for_entry(entry_id, cx) {
 420                    let abs_path = project.absolute_path(&project_path, cx);
 421                    self.nav_history
 422                        .0
 423                        .borrow_mut()
 424                        .paths_by_item
 425                        .insert(item.id(), (project_path, abs_path));
 426                }
 427            }
 428        }
 429        // If no destination index is specified, add or move the item after the active item.
 430        let mut insertion_index = {
 431            cmp::min(
 432                if let Some(destination_index) = destination_index {
 433                    destination_index
 434                } else {
 435                    self.active_item_index + 1
 436                },
 437                self.items.len(),
 438            )
 439        };
 440
 441        // Does the item already exist?
 442        let project_entry_id = if item.is_singleton(cx) {
 443            item.project_entry_ids(cx).get(0).copied()
 444        } else {
 445            None
 446        };
 447
 448        let existing_item_index = self.items.iter().position(|existing_item| {
 449            if existing_item.id() == item.id() {
 450                true
 451            } else if existing_item.is_singleton(cx) {
 452                existing_item
 453                    .project_entry_ids(cx)
 454                    .get(0)
 455                    .map_or(false, |existing_entry_id| {
 456                        Some(existing_entry_id) == project_entry_id.as_ref()
 457                    })
 458            } else {
 459                false
 460            }
 461        });
 462
 463        if let Some(existing_item_index) = existing_item_index {
 464            // If the item already exists, move it to the desired destination and activate it
 465
 466            if existing_item_index != insertion_index {
 467                let existing_item_is_active = existing_item_index == self.active_item_index;
 468
 469                // If the caller didn't specify a destination and the added item is already
 470                // the active one, don't move it
 471                if existing_item_is_active && destination_index.is_none() {
 472                    insertion_index = existing_item_index;
 473                } else {
 474                    self.items.remove(existing_item_index);
 475                    if existing_item_index < self.active_item_index {
 476                        self.active_item_index -= 1;
 477                    }
 478                    insertion_index = insertion_index.min(self.items.len());
 479
 480                    self.items.insert(insertion_index, item.clone());
 481
 482                    if existing_item_is_active {
 483                        self.active_item_index = insertion_index;
 484                    } else if insertion_index <= self.active_item_index {
 485                        self.active_item_index += 1;
 486                    }
 487                }
 488
 489                cx.notify();
 490            }
 491
 492            self.activate_item(insertion_index, activate_pane, focus_item, cx);
 493        } else {
 494            self.items.insert(insertion_index, item.clone());
 495            if insertion_index <= self.active_item_index {
 496                self.active_item_index += 1;
 497            }
 498
 499            self.activate_item(insertion_index, activate_pane, focus_item, cx);
 500            cx.notify();
 501        }
 502
 503        cx.emit(Event::AddItem { item });
 504    }
 505
 506    pub fn items_len(&self) -> usize {
 507        self.items.len()
 508    }
 509
 510    pub fn items(&self) -> impl Iterator<Item = &Box<dyn ItemHandle>> + DoubleEndedIterator {
 511        self.items.iter()
 512    }
 513
 514    pub fn items_of_type<T: View>(&self) -> impl '_ + Iterator<Item = ViewHandle<T>> {
 515        self.items
 516            .iter()
 517            .filter_map(|item| item.as_any().clone().downcast())
 518    }
 519
 520    pub fn active_item(&self) -> Option<Box<dyn ItemHandle>> {
 521        self.items.get(self.active_item_index).cloned()
 522    }
 523
 524    pub fn item_for_entry(
 525        &self,
 526        entry_id: ProjectEntryId,
 527        cx: &AppContext,
 528    ) -> Option<Box<dyn ItemHandle>> {
 529        self.items.iter().find_map(|item| {
 530            if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
 531                Some(item.boxed_clone())
 532            } else {
 533                None
 534            }
 535        })
 536    }
 537
 538    pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option<usize> {
 539        self.items.iter().position(|i| i.id() == item.id())
 540    }
 541
 542    pub fn toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext<Self>) {
 543        // Potentially warn the user of the new keybinding
 544        let workspace_handle = self.workspace().clone();
 545        cx.spawn(|_, mut cx| async move { notify_of_new_dock(&workspace_handle, &mut cx) })
 546            .detach();
 547
 548        if self.zoomed {
 549            cx.emit(Event::ZoomOut);
 550        } else if !self.items.is_empty() {
 551            if !self.has_focus {
 552                cx.focus_self();
 553            }
 554            cx.emit(Event::ZoomIn);
 555        }
 556    }
 557
 558    pub fn activate_item(
 559        &mut self,
 560        index: usize,
 561        activate_pane: bool,
 562        focus_item: bool,
 563        cx: &mut ViewContext<Self>,
 564    ) {
 565        use NavigationMode::{GoingBack, GoingForward};
 566
 567        if index < self.items.len() {
 568            let prev_active_item_ix = mem::replace(&mut self.active_item_index, index);
 569            if prev_active_item_ix != self.active_item_index
 570                || matches!(self.nav_history.mode(), GoingBack | GoingForward)
 571            {
 572                if let Some(prev_item) = self.items.get(prev_active_item_ix) {
 573                    prev_item.deactivated(cx);
 574                }
 575
 576                cx.emit(Event::ActivateItem {
 577                    local: activate_pane,
 578                });
 579            }
 580
 581            if let Some(newly_active_item) = self.items.get(index) {
 582                self.activation_history
 583                    .retain(|&previously_active_item_id| {
 584                        previously_active_item_id != newly_active_item.id()
 585                    });
 586                self.activation_history.push(newly_active_item.id());
 587            }
 588
 589            self.update_toolbar(cx);
 590
 591            if focus_item {
 592                self.focus_active_item(cx);
 593            }
 594
 595            self.autoscroll = true;
 596            cx.notify();
 597        }
 598    }
 599
 600    pub fn activate_prev_item(&mut self, activate_pane: bool, cx: &mut ViewContext<Self>) {
 601        let mut index = self.active_item_index;
 602        if index > 0 {
 603            index -= 1;
 604        } else if !self.items.is_empty() {
 605            index = self.items.len() - 1;
 606        }
 607        self.activate_item(index, activate_pane, activate_pane, cx);
 608    }
 609
 610    pub fn activate_next_item(&mut self, activate_pane: bool, cx: &mut ViewContext<Self>) {
 611        let mut index = self.active_item_index;
 612        if index + 1 < self.items.len() {
 613            index += 1;
 614        } else {
 615            index = 0;
 616        }
 617        self.activate_item(index, activate_pane, activate_pane, cx);
 618    }
 619
 620    pub fn close_active_item(
 621        &mut self,
 622        _: &CloseActiveItem,
 623        cx: &mut ViewContext<Self>,
 624    ) -> Option<Task<Result<()>>> {
 625        if self.items.is_empty() {
 626            return None;
 627        }
 628        let active_item_id = self.items[self.active_item_index].id();
 629        Some(self.close_item_by_id(active_item_id, cx))
 630    }
 631
 632    pub fn close_item_by_id(
 633        &mut self,
 634        item_id_to_close: usize,
 635        cx: &mut ViewContext<Self>,
 636    ) -> Task<Result<()>> {
 637        self.close_items(cx, move |view_id| view_id == item_id_to_close)
 638    }
 639
 640    pub fn close_inactive_items(
 641        &mut self,
 642        _: &CloseInactiveItems,
 643        cx: &mut ViewContext<Self>,
 644    ) -> Option<Task<Result<()>>> {
 645        if self.items.is_empty() {
 646            return None;
 647        }
 648
 649        let active_item_id = self.items[self.active_item_index].id();
 650        Some(self.close_items(cx, move |item_id| item_id != active_item_id))
 651    }
 652
 653    pub fn close_clean_items(
 654        &mut self,
 655        _: &CloseCleanItems,
 656        cx: &mut ViewContext<Self>,
 657    ) -> Option<Task<Result<()>>> {
 658        let item_ids: Vec<_> = self
 659            .items()
 660            .filter(|item| !item.is_dirty(cx))
 661            .map(|item| item.id())
 662            .collect();
 663        Some(self.close_items(cx, move |item_id| item_ids.contains(&item_id)))
 664    }
 665
 666    pub fn close_items_to_the_left(
 667        &mut self,
 668        _: &CloseItemsToTheLeft,
 669        cx: &mut ViewContext<Self>,
 670    ) -> Option<Task<Result<()>>> {
 671        if self.items.is_empty() {
 672            return None;
 673        }
 674        let active_item_id = self.items[self.active_item_index].id();
 675        Some(self.close_items_to_the_left_by_id(active_item_id, cx))
 676    }
 677
 678    pub fn close_items_to_the_left_by_id(
 679        &mut self,
 680        item_id: usize,
 681        cx: &mut ViewContext<Self>,
 682    ) -> Task<Result<()>> {
 683        let item_ids: Vec<_> = self
 684            .items()
 685            .take_while(|item| item.id() != item_id)
 686            .map(|item| item.id())
 687            .collect();
 688        self.close_items(cx, move |item_id| item_ids.contains(&item_id))
 689    }
 690
 691    pub fn close_items_to_the_right(
 692        &mut self,
 693        _: &CloseItemsToTheRight,
 694        cx: &mut ViewContext<Self>,
 695    ) -> Option<Task<Result<()>>> {
 696        if self.items.is_empty() {
 697            return None;
 698        }
 699        let active_item_id = self.items[self.active_item_index].id();
 700        Some(self.close_items_to_the_right_by_id(active_item_id, cx))
 701    }
 702
 703    pub fn close_items_to_the_right_by_id(
 704        &mut self,
 705        item_id: usize,
 706        cx: &mut ViewContext<Self>,
 707    ) -> Task<Result<()>> {
 708        let item_ids: Vec<_> = self
 709            .items()
 710            .rev()
 711            .take_while(|item| item.id() != item_id)
 712            .map(|item| item.id())
 713            .collect();
 714        self.close_items(cx, move |item_id| item_ids.contains(&item_id))
 715    }
 716
 717    pub fn close_all_items(
 718        &mut self,
 719        _: &CloseAllItems,
 720        cx: &mut ViewContext<Self>,
 721    ) -> Option<Task<Result<()>>> {
 722        Some(self.close_items(cx, move |_| true))
 723    }
 724
 725    pub fn close_items(
 726        &mut self,
 727        cx: &mut ViewContext<Pane>,
 728        should_close: impl 'static + Fn(usize) -> bool,
 729    ) -> Task<Result<()>> {
 730        // Find the items to close.
 731        let mut items_to_close = Vec::new();
 732        for item in &self.items {
 733            if should_close(item.id()) {
 734                items_to_close.push(item.boxed_clone());
 735            }
 736        }
 737
 738        // If a buffer is open both in a singleton editor and in a multibuffer, make sure
 739        // to focus the singleton buffer when prompting to save that buffer, as opposed
 740        // to focusing the multibuffer, because this gives the user a more clear idea
 741        // of what content they would be saving.
 742        items_to_close.sort_by_key(|item| !item.is_singleton(cx));
 743
 744        let workspace = self.workspace.clone();
 745        cx.spawn(|pane, mut cx| async move {
 746            let mut saved_project_items_ids = HashSet::default();
 747            for item in items_to_close.clone() {
 748                // Find the item's current index and its set of project item models. Avoid
 749                // storing these in advance, in case they have changed since this task
 750                // was started.
 751                let (item_ix, mut project_item_ids) = pane.read_with(&cx, |pane, cx| {
 752                    (pane.index_for_item(&*item), item.project_item_model_ids(cx))
 753                })?;
 754                let item_ix = if let Some(ix) = item_ix {
 755                    ix
 756                } else {
 757                    continue;
 758                };
 759
 760                // Check if this view has any project items that are not open anywhere else
 761                // in the workspace, AND that the user has not already been prompted to save.
 762                // If there are any such project entries, prompt the user to save this item.
 763                let project = workspace.read_with(&cx, |workspace, cx| {
 764                    for item in workspace.items(cx) {
 765                        if !items_to_close
 766                            .iter()
 767                            .any(|item_to_close| item_to_close.id() == item.id())
 768                        {
 769                            let other_project_item_ids = item.project_item_model_ids(cx);
 770                            project_item_ids.retain(|id| !other_project_item_ids.contains(id));
 771                        }
 772                    }
 773                    workspace.project().clone()
 774                })?;
 775                let should_save = project_item_ids
 776                    .iter()
 777                    .any(|id| saved_project_items_ids.insert(*id));
 778
 779                if should_save
 780                    && !Self::save_item(project.clone(), &pane, item_ix, &*item, true, &mut cx)
 781                        .await?
 782                {
 783                    break;
 784                }
 785
 786                // Remove the item from the pane.
 787                pane.update(&mut cx, |pane, cx| {
 788                    if let Some(item_ix) = pane.items.iter().position(|i| i.id() == item.id()) {
 789                        pane.remove_item(item_ix, false, cx);
 790                    }
 791                })?;
 792            }
 793
 794            pane.update(&mut cx, |_, cx| cx.notify())?;
 795            Ok(())
 796        })
 797    }
 798
 799    pub fn remove_item(
 800        &mut self,
 801        item_index: usize,
 802        activate_pane: bool,
 803        cx: &mut ViewContext<Self>,
 804    ) {
 805        self.activation_history
 806            .retain(|&history_entry| history_entry != self.items[item_index].id());
 807
 808        if item_index == self.active_item_index {
 809            let index_to_activate = self
 810                .activation_history
 811                .pop()
 812                .and_then(|last_activated_item| {
 813                    self.items.iter().enumerate().find_map(|(index, item)| {
 814                        (item.id() == last_activated_item).then_some(index)
 815                    })
 816                })
 817                // We didn't have a valid activation history entry, so fallback
 818                // to activating the item to the left
 819                .unwrap_or_else(|| item_index.min(self.items.len()).saturating_sub(1));
 820
 821            let should_activate = activate_pane || self.has_focus;
 822            self.activate_item(index_to_activate, should_activate, should_activate, cx);
 823        }
 824
 825        let item = self.items.remove(item_index);
 826
 827        cx.emit(Event::RemoveItem { item_id: item.id() });
 828        if self.items.is_empty() {
 829            item.deactivated(cx);
 830            self.update_toolbar(cx);
 831            cx.emit(Event::Remove);
 832        }
 833
 834        if item_index < self.active_item_index {
 835            self.active_item_index -= 1;
 836        }
 837
 838        self.nav_history.set_mode(NavigationMode::ClosingItem);
 839        item.deactivated(cx);
 840        self.nav_history.set_mode(NavigationMode::Normal);
 841
 842        if let Some(path) = item.project_path(cx) {
 843            let abs_path = self
 844                .nav_history
 845                .0
 846                .borrow()
 847                .paths_by_item
 848                .get(&item.id())
 849                .and_then(|(_, abs_path)| abs_path.clone());
 850            self.nav_history
 851                .0
 852                .borrow_mut()
 853                .paths_by_item
 854                .insert(item.id(), (path, abs_path));
 855        } else {
 856            self.nav_history
 857                .0
 858                .borrow_mut()
 859                .paths_by_item
 860                .remove(&item.id());
 861        }
 862
 863        if self.items.is_empty() && self.zoomed {
 864            cx.emit(Event::ZoomOut);
 865        }
 866
 867        cx.notify();
 868    }
 869
 870    pub async fn save_item(
 871        project: ModelHandle<Project>,
 872        pane: &WeakViewHandle<Pane>,
 873        item_ix: usize,
 874        item: &dyn ItemHandle,
 875        should_prompt_for_save: bool,
 876        cx: &mut AsyncAppContext,
 877    ) -> Result<bool> {
 878        const CONFLICT_MESSAGE: &str =
 879            "This file has changed on disk since you started editing it. Do you want to overwrite it?";
 880        const DIRTY_MESSAGE: &str = "This file contains unsaved edits. Do you want to save it?";
 881
 882        let (has_conflict, is_dirty, can_save, is_singleton) = cx.read(|cx| {
 883            (
 884                item.has_conflict(cx),
 885                item.is_dirty(cx),
 886                item.can_save(cx),
 887                item.is_singleton(cx),
 888            )
 889        });
 890
 891        if has_conflict && can_save {
 892            let mut answer = pane.update(cx, |pane, cx| {
 893                pane.activate_item(item_ix, true, true, cx);
 894                cx.prompt(
 895                    PromptLevel::Warning,
 896                    CONFLICT_MESSAGE,
 897                    &["Overwrite", "Discard", "Cancel"],
 898                )
 899            })?;
 900            match answer.next().await {
 901                Some(0) => pane.update(cx, |_, cx| item.save(project, cx))?.await?,
 902                Some(1) => pane.update(cx, |_, cx| item.reload(project, cx))?.await?,
 903                _ => return Ok(false),
 904            }
 905        } else if is_dirty && (can_save || is_singleton) {
 906            let will_autosave = cx.read(|cx| {
 907                matches!(
 908                    settings::get::<WorkspaceSettings>(cx).autosave,
 909                    AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange
 910                ) && Self::can_autosave_item(&*item, cx)
 911            });
 912            let should_save = if should_prompt_for_save && !will_autosave {
 913                let mut answer = pane.update(cx, |pane, cx| {
 914                    pane.activate_item(item_ix, true, true, cx);
 915                    cx.prompt(
 916                        PromptLevel::Warning,
 917                        DIRTY_MESSAGE,
 918                        &["Save", "Don't Save", "Cancel"],
 919                    )
 920                })?;
 921                match answer.next().await {
 922                    Some(0) => true,
 923                    Some(1) => false,
 924                    _ => return Ok(false),
 925                }
 926            } else {
 927                true
 928            };
 929
 930            if should_save {
 931                if can_save {
 932                    pane.update(cx, |_, cx| item.save(project, cx))?.await?;
 933                } else if is_singleton {
 934                    let start_abs_path = project
 935                        .read_with(cx, |project, cx| {
 936                            let worktree = project.visible_worktrees(cx).next()?;
 937                            Some(worktree.read(cx).as_local()?.abs_path().to_path_buf())
 938                        })
 939                        .unwrap_or_else(|| Path::new("").into());
 940
 941                    let mut abs_path = cx.update(|cx| cx.prompt_for_new_path(&start_abs_path));
 942                    if let Some(abs_path) = abs_path.next().await.flatten() {
 943                        pane.update(cx, |_, cx| item.save_as(project, abs_path, cx))?
 944                            .await?;
 945                    } else {
 946                        return Ok(false);
 947                    }
 948                }
 949            }
 950        }
 951        Ok(true)
 952    }
 953
 954    fn can_autosave_item(item: &dyn ItemHandle, cx: &AppContext) -> bool {
 955        let is_deleted = item.project_entry_ids(cx).is_empty();
 956        item.is_dirty(cx) && !item.has_conflict(cx) && item.can_save(cx) && !is_deleted
 957    }
 958
 959    pub fn autosave_item(
 960        item: &dyn ItemHandle,
 961        project: ModelHandle<Project>,
 962        cx: &mut WindowContext,
 963    ) -> Task<Result<()>> {
 964        if Self::can_autosave_item(item, cx) {
 965            item.save(project, cx)
 966        } else {
 967            Task::ready(Ok(()))
 968        }
 969    }
 970
 971    pub fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
 972        if let Some(active_item) = self.active_item() {
 973            cx.focus(active_item.as_any());
 974        }
 975    }
 976
 977    pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext<Self>) {
 978        cx.emit(Event::Split(direction));
 979    }
 980
 981    fn deploy_split_menu(&mut self, cx: &mut ViewContext<Self>) {
 982        self.tab_bar_context_menu.handle.update(cx, |menu, cx| {
 983            menu.show(
 984                Default::default(),
 985                AnchorCorner::TopRight,
 986                vec![
 987                    ContextMenuItem::action("Split Right", SplitRight),
 988                    ContextMenuItem::action("Split Left", SplitLeft),
 989                    ContextMenuItem::action("Split Up", SplitUp),
 990                    ContextMenuItem::action("Split Down", SplitDown),
 991                ],
 992                cx,
 993            );
 994        });
 995
 996        self.tab_bar_context_menu.kind = TabBarContextMenuKind::Split;
 997    }
 998
 999    fn deploy_new_menu(&mut self, cx: &mut ViewContext<Self>) {
1000        self.tab_bar_context_menu.handle.update(cx, |menu, cx| {
1001            menu.show(
1002                Default::default(),
1003                AnchorCorner::TopRight,
1004                vec![
1005                    ContextMenuItem::action("New File", NewFile),
1006                    ContextMenuItem::action("New Terminal", NewCenterTerminal),
1007                    ContextMenuItem::action("New Search", NewSearch),
1008                ],
1009                cx,
1010            );
1011        });
1012
1013        self.tab_bar_context_menu.kind = TabBarContextMenuKind::New;
1014    }
1015
1016    fn deploy_tab_context_menu(
1017        &mut self,
1018        position: Vector2F,
1019        target_item_id: usize,
1020        cx: &mut ViewContext<Self>,
1021    ) {
1022        let active_item_id = self.items[self.active_item_index].id();
1023        let is_active_item = target_item_id == active_item_id;
1024        let target_pane = cx.weak_handle();
1025
1026        // 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
1027
1028        self.tab_context_menu.update(cx, |menu, cx| {
1029            menu.show(
1030                position,
1031                AnchorCorner::TopLeft,
1032                if is_active_item {
1033                    vec![
1034                        ContextMenuItem::action("Close Active Item", CloseActiveItem),
1035                        ContextMenuItem::action("Close Inactive Items", CloseInactiveItems),
1036                        ContextMenuItem::action("Close Clean Items", CloseCleanItems),
1037                        ContextMenuItem::action("Close Items To The Left", CloseItemsToTheLeft),
1038                        ContextMenuItem::action("Close Items To The Right", CloseItemsToTheRight),
1039                        ContextMenuItem::action("Close All Items", CloseAllItems),
1040                    ]
1041                } else {
1042                    // 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.
1043                    vec![
1044                        ContextMenuItem::handler("Close Inactive Item", {
1045                            let pane = target_pane.clone();
1046                            move |cx| {
1047                                if let Some(pane) = pane.upgrade(cx) {
1048                                    pane.update(cx, |pane, cx| {
1049                                        pane.close_item_by_id(target_item_id, cx)
1050                                            .detach_and_log_err(cx);
1051                                    })
1052                                }
1053                            }
1054                        }),
1055                        ContextMenuItem::action("Close Inactive Items", CloseInactiveItems),
1056                        ContextMenuItem::action("Close Clean Items", CloseCleanItems),
1057                        ContextMenuItem::handler("Close Items To The Left", {
1058                            let pane = target_pane.clone();
1059                            move |cx| {
1060                                if let Some(pane) = pane.upgrade(cx) {
1061                                    pane.update(cx, |pane, cx| {
1062                                        pane.close_items_to_the_left_by_id(target_item_id, cx)
1063                                            .detach_and_log_err(cx);
1064                                    })
1065                                }
1066                            }
1067                        }),
1068                        ContextMenuItem::handler("Close Items To The Right", {
1069                            let pane = target_pane.clone();
1070                            move |cx| {
1071                                if let Some(pane) = pane.upgrade(cx) {
1072                                    pane.update(cx, |pane, cx| {
1073                                        pane.close_items_to_the_right_by_id(target_item_id, cx)
1074                                            .detach_and_log_err(cx);
1075                                    })
1076                                }
1077                            }
1078                        }),
1079                        ContextMenuItem::action("Close All Items", CloseAllItems),
1080                    ]
1081                },
1082                cx,
1083            );
1084        });
1085    }
1086
1087    pub fn toolbar(&self) -> &ViewHandle<Toolbar> {
1088        &self.toolbar
1089    }
1090
1091    pub fn handle_deleted_project_item(
1092        &mut self,
1093        entry_id: ProjectEntryId,
1094        cx: &mut ViewContext<Pane>,
1095    ) -> Option<()> {
1096        let (item_index_to_delete, item_id) = self.items().enumerate().find_map(|(i, item)| {
1097            if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
1098                Some((i, item.id()))
1099            } else {
1100                None
1101            }
1102        })?;
1103
1104        self.remove_item(item_index_to_delete, false, cx);
1105        self.nav_history.remove_item(item_id);
1106
1107        Some(())
1108    }
1109
1110    fn update_toolbar(&mut self, cx: &mut ViewContext<Self>) {
1111        let active_item = self
1112            .items
1113            .get(self.active_item_index)
1114            .map(|item| item.as_ref());
1115        self.toolbar.update(cx, |toolbar, cx| {
1116            toolbar.set_active_item(active_item, cx);
1117        });
1118    }
1119
1120    fn render_tabs(&mut self, cx: &mut ViewContext<Self>) -> impl Element<Self> {
1121        let theme = theme::current(cx).clone();
1122
1123        let pane = cx.handle().downgrade();
1124        let autoscroll = if mem::take(&mut self.autoscroll) {
1125            Some(self.active_item_index)
1126        } else {
1127            None
1128        };
1129
1130        let pane_active = self.has_focus;
1131
1132        enum Tabs {}
1133        let mut row = Flex::row().scrollable::<Tabs>(1, autoscroll, cx);
1134        for (ix, (item, detail)) in self
1135            .items
1136            .iter()
1137            .cloned()
1138            .zip(self.tab_details(cx))
1139            .enumerate()
1140        {
1141            let detail = if detail == 0 { None } else { Some(detail) };
1142            let tab_active = ix == self.active_item_index;
1143
1144            row.add_child({
1145                enum TabDragReceiver {}
1146                let mut receiver =
1147                    dragged_item_receiver::<TabDragReceiver, _, _>(self, ix, ix, true, None, cx, {
1148                        let item = item.clone();
1149                        let pane = pane.clone();
1150                        let detail = detail.clone();
1151
1152                        let theme = theme::current(cx).clone();
1153                        let mut tooltip_theme = theme.tooltip.clone();
1154                        tooltip_theme.max_text_width = None;
1155                        let tab_tooltip_text =
1156                            item.tab_tooltip_text(cx).map(|text| text.into_owned());
1157
1158                        move |mouse_state, cx| {
1159                            let tab_style =
1160                                theme.workspace.tab_bar.tab_style(pane_active, tab_active);
1161                            let hovered = mouse_state.hovered();
1162
1163                            enum Tab {}
1164                            let mouse_event_handler =
1165                                MouseEventHandler::<Tab, Pane>::new(ix, cx, |_, cx| {
1166                                    Self::render_tab(
1167                                        &item,
1168                                        pane.clone(),
1169                                        ix == 0,
1170                                        detail,
1171                                        hovered,
1172                                        tab_style,
1173                                        cx,
1174                                    )
1175                                })
1176                                .on_down(MouseButton::Left, move |_, this, cx| {
1177                                    this.activate_item(ix, true, true, cx);
1178                                })
1179                                .on_click(MouseButton::Middle, {
1180                                    let item_id = item.id();
1181                                    move |_, pane, cx| {
1182                                        pane.close_item_by_id(item_id, cx).detach_and_log_err(cx);
1183                                    }
1184                                })
1185                                .on_down(
1186                                    MouseButton::Right,
1187                                    move |event, pane, cx| {
1188                                        pane.deploy_tab_context_menu(event.position, item.id(), cx);
1189                                    },
1190                                );
1191
1192                            if let Some(tab_tooltip_text) = tab_tooltip_text {
1193                                mouse_event_handler
1194                                    .with_tooltip::<Self>(
1195                                        ix,
1196                                        tab_tooltip_text,
1197                                        None,
1198                                        tooltip_theme,
1199                                        cx,
1200                                    )
1201                                    .into_any()
1202                            } else {
1203                                mouse_event_handler.into_any()
1204                            }
1205                        }
1206                    });
1207
1208                if !pane_active || !tab_active {
1209                    receiver = receiver.with_cursor_style(CursorStyle::PointingHand);
1210                }
1211
1212                receiver.as_draggable(
1213                    DraggedItem {
1214                        handle: item,
1215                        pane: pane.clone(),
1216                    },
1217                    {
1218                        let theme = theme::current(cx).clone();
1219
1220                        let detail = detail.clone();
1221                        move |dragged_item: &DraggedItem, cx: &mut ViewContext<Workspace>| {
1222                            let tab_style = &theme.workspace.tab_bar.dragged_tab;
1223                            Self::render_dragged_tab(
1224                                &dragged_item.handle,
1225                                dragged_item.pane.clone(),
1226                                false,
1227                                detail,
1228                                false,
1229                                &tab_style,
1230                                cx,
1231                            )
1232                        }
1233                    },
1234                )
1235            })
1236        }
1237
1238        // Use the inactive tab style along with the current pane's active status to decide how to render
1239        // the filler
1240        let filler_index = self.items.len();
1241        let filler_style = theme.workspace.tab_bar.tab_style(pane_active, false);
1242        enum Filler {}
1243        row.add_child(
1244            dragged_item_receiver::<Filler, _, _>(self, 0, filler_index, true, None, cx, |_, _| {
1245                Empty::new()
1246                    .contained()
1247                    .with_style(filler_style.container)
1248                    .with_border(filler_style.container.border)
1249            })
1250            .flex(1., true)
1251            .into_any_named("filler"),
1252        );
1253
1254        row
1255    }
1256
1257    fn tab_details(&self, cx: &AppContext) -> Vec<usize> {
1258        let mut tab_details = (0..self.items.len()).map(|_| 0).collect::<Vec<_>>();
1259
1260        let mut tab_descriptions = HashMap::default();
1261        let mut done = false;
1262        while !done {
1263            done = true;
1264
1265            // Store item indices by their tab description.
1266            for (ix, (item, detail)) in self.items.iter().zip(&tab_details).enumerate() {
1267                if let Some(description) = item.tab_description(*detail, cx) {
1268                    if *detail == 0
1269                        || Some(&description) != item.tab_description(detail - 1, cx).as_ref()
1270                    {
1271                        tab_descriptions
1272                            .entry(description)
1273                            .or_insert(Vec::new())
1274                            .push(ix);
1275                    }
1276                }
1277            }
1278
1279            // If two or more items have the same tab description, increase their level
1280            // of detail and try again.
1281            for (_, item_ixs) in tab_descriptions.drain() {
1282                if item_ixs.len() > 1 {
1283                    done = false;
1284                    for ix in item_ixs {
1285                        tab_details[ix] += 1;
1286                    }
1287                }
1288            }
1289        }
1290
1291        tab_details
1292    }
1293
1294    fn render_tab(
1295        item: &Box<dyn ItemHandle>,
1296        pane: WeakViewHandle<Pane>,
1297        first: bool,
1298        detail: Option<usize>,
1299        hovered: bool,
1300        tab_style: &theme::Tab,
1301        cx: &mut ViewContext<Self>,
1302    ) -> AnyElement<Self> {
1303        let title = item.tab_content(detail, &tab_style, cx);
1304        Self::render_tab_with_title(title, item, pane, first, hovered, tab_style, cx)
1305    }
1306
1307    fn render_dragged_tab(
1308        item: &Box<dyn ItemHandle>,
1309        pane: WeakViewHandle<Pane>,
1310        first: bool,
1311        detail: Option<usize>,
1312        hovered: bool,
1313        tab_style: &theme::Tab,
1314        cx: &mut ViewContext<Workspace>,
1315    ) -> AnyElement<Workspace> {
1316        let title = item.dragged_tab_content(detail, &tab_style, cx);
1317        Self::render_tab_with_title(title, item, pane, first, hovered, tab_style, cx)
1318    }
1319
1320    fn render_tab_with_title<T: View>(
1321        title: AnyElement<T>,
1322        item: &Box<dyn ItemHandle>,
1323        pane: WeakViewHandle<Pane>,
1324        first: bool,
1325        hovered: bool,
1326        tab_style: &theme::Tab,
1327        cx: &mut ViewContext<T>,
1328    ) -> AnyElement<T> {
1329        let mut container = tab_style.container.clone();
1330        if first {
1331            container.border.left = false;
1332        }
1333
1334        Flex::row()
1335            .with_child({
1336                let diameter = 7.0;
1337                let icon_color = if item.has_conflict(cx) {
1338                    Some(tab_style.icon_conflict)
1339                } else if item.is_dirty(cx) {
1340                    Some(tab_style.icon_dirty)
1341                } else {
1342                    None
1343                };
1344
1345                Canvas::new(move |scene, bounds, _, _, _| {
1346                    if let Some(color) = icon_color {
1347                        let square = RectF::new(bounds.origin(), vec2f(diameter, diameter));
1348                        scene.push_quad(Quad {
1349                            bounds: square,
1350                            background: Some(color),
1351                            border: Default::default(),
1352                            corner_radius: diameter / 2.,
1353                        });
1354                    }
1355                })
1356                .constrained()
1357                .with_width(diameter)
1358                .with_height(diameter)
1359                .aligned()
1360            })
1361            .with_child(title.aligned().contained().with_style(ContainerStyle {
1362                margin: Margin {
1363                    left: tab_style.spacing,
1364                    right: tab_style.spacing,
1365                    ..Default::default()
1366                },
1367                ..Default::default()
1368            }))
1369            .with_child(
1370                if hovered {
1371                    let item_id = item.id();
1372                    enum TabCloseButton {}
1373                    let icon = Svg::new("icons/x_mark_8.svg");
1374                    MouseEventHandler::<TabCloseButton, _>::new(item_id, cx, |mouse_state, _| {
1375                        if mouse_state.hovered() {
1376                            icon.with_color(tab_style.icon_close_active)
1377                        } else {
1378                            icon.with_color(tab_style.icon_close)
1379                        }
1380                    })
1381                    .with_padding(Padding::uniform(4.))
1382                    .with_cursor_style(CursorStyle::PointingHand)
1383                    .on_click(MouseButton::Left, {
1384                        let pane = pane.clone();
1385                        move |_, _, cx| {
1386                            let pane = pane.clone();
1387                            cx.window_context().defer(move |cx| {
1388                                if let Some(pane) = pane.upgrade(cx) {
1389                                    pane.update(cx, |pane, cx| {
1390                                        pane.close_item_by_id(item_id, cx).detach_and_log_err(cx);
1391                                    });
1392                                }
1393                            });
1394                        }
1395                    })
1396                    .into_any_named("close-tab-icon")
1397                    .constrained()
1398                } else {
1399                    Empty::new().constrained()
1400                }
1401                .with_width(tab_style.close_icon_width)
1402                .aligned(),
1403            )
1404            .contained()
1405            .with_style(container)
1406            .constrained()
1407            .with_height(tab_style.height)
1408            .into_any()
1409    }
1410
1411    pub fn render_tab_bar_button<F: 'static + Fn(&mut Pane, &mut EventContext<Pane>)>(
1412        index: usize,
1413        icon: &'static str,
1414        is_active: bool,
1415        tooltip: Option<(String, Option<Box<dyn Action>>)>,
1416        cx: &mut ViewContext<Pane>,
1417        on_click: F,
1418        context_menu: Option<ViewHandle<ContextMenu>>,
1419    ) -> AnyElement<Pane> {
1420        enum TabBarButton {}
1421
1422        let mut button = MouseEventHandler::<TabBarButton, _>::new(index, cx, |mouse_state, cx| {
1423            let theme = &settings::get::<ThemeSettings>(cx).theme.workspace.tab_bar;
1424            let style = theme.pane_button.in_state(is_active).style_for(mouse_state);
1425            Svg::new(icon)
1426                .with_color(style.color)
1427                .constrained()
1428                .with_width(style.icon_width)
1429                .aligned()
1430                .constrained()
1431                .with_width(style.button_width)
1432                .with_height(style.button_width)
1433        })
1434        .with_cursor_style(CursorStyle::PointingHand)
1435        .on_click(MouseButton::Left, move |_, pane, cx| on_click(pane, cx))
1436        .into_any();
1437        if let Some((tooltip, action)) = tooltip {
1438            let tooltip_style = settings::get::<ThemeSettings>(cx).theme.tooltip.clone();
1439            button = button
1440                .with_tooltip::<TabBarButton>(index, tooltip, action, tooltip_style, cx)
1441                .into_any();
1442        }
1443
1444        Stack::new()
1445            .with_child(button)
1446            .with_children(
1447                context_menu.map(|menu| ChildView::new(&menu, cx).aligned().bottom().right()),
1448            )
1449            .flex(1., false)
1450            .into_any_named("tab bar button")
1451    }
1452
1453    fn render_blank_pane(&self, theme: &Theme, _cx: &mut ViewContext<Self>) -> AnyElement<Self> {
1454        let background = theme.workspace.background;
1455        Empty::new()
1456            .contained()
1457            .with_background_color(background)
1458            .into_any()
1459    }
1460
1461    pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
1462        self.zoomed = zoomed;
1463        cx.notify();
1464    }
1465
1466    pub fn is_zoomed(&self) -> bool {
1467        self.zoomed
1468    }
1469}
1470
1471impl Entity for Pane {
1472    type Event = Event;
1473}
1474
1475impl View for Pane {
1476    fn ui_name() -> &'static str {
1477        "Pane"
1478    }
1479
1480    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
1481        enum MouseNavigationHandler {}
1482
1483        MouseEventHandler::<MouseNavigationHandler, _>::new(0, cx, |_, cx| {
1484            let active_item_index = self.active_item_index;
1485
1486            if let Some(active_item) = self.active_item() {
1487                Flex::column()
1488                    .with_child({
1489                        let theme = theme::current(cx).clone();
1490
1491                        let mut stack = Stack::new();
1492
1493                        enum TabBarEventHandler {}
1494                        stack.add_child(
1495                            MouseEventHandler::<TabBarEventHandler, _>::new(0, cx, |_, _| {
1496                                Empty::new()
1497                                    .contained()
1498                                    .with_style(theme.workspace.tab_bar.container)
1499                            })
1500                            .on_down(
1501                                MouseButton::Left,
1502                                move |_, this, cx| {
1503                                    this.activate_item(active_item_index, true, true, cx);
1504                                },
1505                            ),
1506                        );
1507
1508                        let mut tab_row = Flex::row()
1509                            .with_child(self.render_tabs(cx).flex(1., true).into_any_named("tabs"));
1510
1511                        if self.has_focus {
1512                            let render_tab_bar_buttons = self.render_tab_bar_buttons.clone();
1513                            tab_row.add_child(
1514                                (render_tab_bar_buttons)(self, cx)
1515                                    .contained()
1516                                    .with_style(theme.workspace.tab_bar.pane_button_container)
1517                                    .flex(1., false)
1518                                    .into_any(),
1519                            )
1520                        }
1521
1522                        stack.add_child(tab_row);
1523                        stack
1524                            .constrained()
1525                            .with_height(theme.workspace.tab_bar.height)
1526                            .flex(1., false)
1527                            .into_any_named("tab bar")
1528                    })
1529                    .with_child({
1530                        enum PaneContentTabDropTarget {}
1531                        dragged_item_receiver::<PaneContentTabDropTarget, _, _>(
1532                            self,
1533                            0,
1534                            self.active_item_index + 1,
1535                            !self.can_split,
1536                            if self.can_split { Some(100.) } else { None },
1537                            cx,
1538                            {
1539                                let toolbar = self.toolbar.clone();
1540                                let toolbar_hidden = toolbar.read(cx).hidden();
1541                                move |_, cx| {
1542                                    Flex::column()
1543                                        .with_children(
1544                                            (!toolbar_hidden)
1545                                                .then(|| ChildView::new(&toolbar, cx).expanded()),
1546                                        )
1547                                        .with_child(
1548                                            ChildView::new(active_item.as_any(), cx).flex(1., true),
1549                                        )
1550                                }
1551                            },
1552                        )
1553                        .flex(1., true)
1554                    })
1555                    .with_child(ChildView::new(&self.tab_context_menu, cx))
1556                    .into_any()
1557            } else {
1558                enum EmptyPane {}
1559                let theme = theme::current(cx).clone();
1560
1561                dragged_item_receiver::<EmptyPane, _, _>(self, 0, 0, false, None, cx, |_, cx| {
1562                    self.render_blank_pane(&theme, cx)
1563                })
1564                .on_down(MouseButton::Left, |_, _, cx| {
1565                    cx.focus_parent();
1566                })
1567                .into_any()
1568            }
1569        })
1570        .on_down(
1571            MouseButton::Navigate(NavigationDirection::Back),
1572            move |_, pane, cx| {
1573                if let Some(workspace) = pane.workspace.upgrade(cx) {
1574                    let pane = cx.weak_handle();
1575                    cx.window_context().defer(move |cx| {
1576                        workspace.update(cx, |workspace, cx| {
1577                            workspace.go_back(pane, cx).detach_and_log_err(cx)
1578                        })
1579                    })
1580                }
1581            },
1582        )
1583        .on_down(MouseButton::Navigate(NavigationDirection::Forward), {
1584            move |_, pane, cx| {
1585                if let Some(workspace) = pane.workspace.upgrade(cx) {
1586                    let pane = cx.weak_handle();
1587                    cx.window_context().defer(move |cx| {
1588                        workspace.update(cx, |workspace, cx| {
1589                            workspace.go_forward(pane, cx).detach_and_log_err(cx)
1590                        })
1591                    })
1592                }
1593            }
1594        })
1595        .into_any_named("pane")
1596    }
1597
1598    fn focus_in(&mut self, focused: AnyViewHandle, cx: &mut ViewContext<Self>) {
1599        if !self.has_focus {
1600            self.has_focus = true;
1601            cx.emit(Event::Focus);
1602            cx.notify();
1603        }
1604
1605        self.toolbar.update(cx, |toolbar, cx| {
1606            toolbar.focus_changed(true, cx);
1607        });
1608
1609        if let Some(active_item) = self.active_item() {
1610            if cx.is_self_focused() {
1611                // Pane was focused directly. We need to either focus a view inside the active item,
1612                // or focus the active item itself
1613                if let Some(weak_last_focused_view) =
1614                    self.last_focused_view_by_item.get(&active_item.id())
1615                {
1616                    if let Some(last_focused_view) = weak_last_focused_view.upgrade(cx) {
1617                        cx.focus(&last_focused_view);
1618                        return;
1619                    } else {
1620                        self.last_focused_view_by_item.remove(&active_item.id());
1621                    }
1622                }
1623
1624                cx.focus(active_item.as_any());
1625            } else if focused != self.tab_bar_context_menu.handle {
1626                self.last_focused_view_by_item
1627                    .insert(active_item.id(), focused.downgrade());
1628            }
1629        }
1630    }
1631
1632    fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
1633        self.has_focus = false;
1634        self.toolbar.update(cx, |toolbar, cx| {
1635            toolbar.focus_changed(false, cx);
1636        });
1637        cx.notify();
1638    }
1639
1640    fn update_keymap_context(&self, keymap: &mut KeymapContext, _: &AppContext) {
1641        Self::reset_to_default_keymap_context(keymap);
1642    }
1643}
1644
1645impl ItemNavHistory {
1646    pub fn push<D: 'static + Any>(&mut self, data: Option<D>, cx: &mut WindowContext) {
1647        self.history.push(data, self.item.clone(), cx);
1648    }
1649
1650    pub fn pop_backward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
1651        self.history.pop(NavigationMode::GoingBack, cx)
1652    }
1653
1654    pub fn pop_forward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
1655        self.history.pop(NavigationMode::GoingForward, cx)
1656    }
1657}
1658
1659impl NavHistory {
1660    pub fn for_each_entry(
1661        &self,
1662        cx: &AppContext,
1663        mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
1664    ) {
1665        let borrowed_history = self.0.borrow();
1666        borrowed_history
1667            .forward_stack
1668            .iter()
1669            .chain(borrowed_history.backward_stack.iter())
1670            .chain(borrowed_history.closed_stack.iter())
1671            .for_each(|entry| {
1672                if let Some(project_and_abs_path) =
1673                    borrowed_history.paths_by_item.get(&entry.item.id())
1674                {
1675                    f(entry, project_and_abs_path.clone());
1676                } else if let Some(item) = entry.item.upgrade(cx) {
1677                    if let Some(path) = item.project_path(cx) {
1678                        f(entry, (path, None));
1679                    }
1680                }
1681            })
1682    }
1683
1684    pub fn set_mode(&mut self, mode: NavigationMode) {
1685        self.0.borrow_mut().mode = mode;
1686    }
1687
1688    pub fn mode(&self) -> NavigationMode {
1689        self.0.borrow().mode
1690    }
1691
1692    pub fn disable(&mut self) {
1693        self.0.borrow_mut().mode = NavigationMode::Disabled;
1694    }
1695
1696    pub fn enable(&mut self) {
1697        self.0.borrow_mut().mode = NavigationMode::Normal;
1698    }
1699
1700    pub fn pop(&mut self, mode: NavigationMode, cx: &mut WindowContext) -> Option<NavigationEntry> {
1701        let mut state = self.0.borrow_mut();
1702        let entry = match mode {
1703            NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
1704                return None
1705            }
1706            NavigationMode::GoingBack => &mut state.backward_stack,
1707            NavigationMode::GoingForward => &mut state.forward_stack,
1708            NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
1709        }
1710        .pop_back();
1711        if entry.is_some() {
1712            state.did_update(cx);
1713        }
1714        entry
1715    }
1716
1717    pub fn push<D: 'static + Any>(
1718        &mut self,
1719        data: Option<D>,
1720        item: Rc<dyn WeakItemHandle>,
1721        cx: &mut WindowContext,
1722    ) {
1723        let state = &mut *self.0.borrow_mut();
1724        match state.mode {
1725            NavigationMode::Disabled => {}
1726            NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
1727                if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
1728                    state.backward_stack.pop_front();
1729                }
1730                state.backward_stack.push_back(NavigationEntry {
1731                    item,
1732                    data: data.map(|data| Box::new(data) as Box<dyn Any>),
1733                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
1734                });
1735                state.forward_stack.clear();
1736            }
1737            NavigationMode::GoingBack => {
1738                if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
1739                    state.forward_stack.pop_front();
1740                }
1741                state.forward_stack.push_back(NavigationEntry {
1742                    item,
1743                    data: data.map(|data| Box::new(data) as Box<dyn Any>),
1744                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
1745                });
1746            }
1747            NavigationMode::GoingForward => {
1748                if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
1749                    state.backward_stack.pop_front();
1750                }
1751                state.backward_stack.push_back(NavigationEntry {
1752                    item,
1753                    data: data.map(|data| Box::new(data) as Box<dyn Any>),
1754                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
1755                });
1756            }
1757            NavigationMode::ClosingItem => {
1758                if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
1759                    state.closed_stack.pop_front();
1760                }
1761                state.closed_stack.push_back(NavigationEntry {
1762                    item,
1763                    data: data.map(|data| Box::new(data) as Box<dyn Any>),
1764                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
1765                });
1766            }
1767        }
1768        state.did_update(cx);
1769    }
1770
1771    pub fn remove_item(&mut self, item_id: usize) {
1772        let mut state = self.0.borrow_mut();
1773        state.paths_by_item.remove(&item_id);
1774        state
1775            .backward_stack
1776            .retain(|entry| entry.item.id() != item_id);
1777        state
1778            .forward_stack
1779            .retain(|entry| entry.item.id() != item_id);
1780        state
1781            .closed_stack
1782            .retain(|entry| entry.item.id() != item_id);
1783    }
1784
1785    pub fn path_for_item(&self, item_id: usize) -> Option<(ProjectPath, Option<PathBuf>)> {
1786        self.0.borrow().paths_by_item.get(&item_id).cloned()
1787    }
1788}
1789
1790impl NavHistoryState {
1791    pub fn did_update(&self, cx: &mut WindowContext) {
1792        if let Some(pane) = self.pane.upgrade(cx) {
1793            cx.defer(move |cx| {
1794                pane.update(cx, |pane, cx| pane.history_updated(cx));
1795            });
1796        }
1797    }
1798}
1799
1800pub struct PaneBackdrop<V: View> {
1801    child_view: usize,
1802    child: AnyElement<V>,
1803}
1804
1805impl<V: View> PaneBackdrop<V> {
1806    pub fn new(pane_item_view: usize, child: AnyElement<V>) -> Self {
1807        PaneBackdrop {
1808            child,
1809            child_view: pane_item_view,
1810        }
1811    }
1812}
1813
1814impl<V: View> Element<V> for PaneBackdrop<V> {
1815    type LayoutState = ();
1816
1817    type PaintState = ();
1818
1819    fn layout(
1820        &mut self,
1821        constraint: gpui::SizeConstraint,
1822        view: &mut V,
1823        cx: &mut LayoutContext<V>,
1824    ) -> (Vector2F, Self::LayoutState) {
1825        let size = self.child.layout(constraint, view, cx);
1826        (size, ())
1827    }
1828
1829    fn paint(
1830        &mut self,
1831        scene: &mut gpui::SceneBuilder,
1832        bounds: RectF,
1833        visible_bounds: RectF,
1834        _: &mut Self::LayoutState,
1835        view: &mut V,
1836        cx: &mut ViewContext<V>,
1837    ) -> Self::PaintState {
1838        let background = theme::current(cx).editor.background;
1839
1840        let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
1841
1842        scene.push_quad(gpui::Quad {
1843            bounds: RectF::new(bounds.origin(), bounds.size()),
1844            background: Some(background),
1845            ..Default::default()
1846        });
1847
1848        let child_view_id = self.child_view;
1849        scene.push_mouse_region(
1850            MouseRegion::new::<Self>(child_view_id, 0, visible_bounds).on_down(
1851                gpui::platform::MouseButton::Left,
1852                move |_, _: &mut V, cx| {
1853                    let window_id = cx.window_id();
1854                    cx.app_context().focus(window_id, Some(child_view_id))
1855                },
1856            ),
1857        );
1858
1859        scene.paint_layer(Some(bounds), |scene| {
1860            self.child
1861                .paint(scene, bounds.origin(), visible_bounds, view, cx)
1862        })
1863    }
1864
1865    fn rect_for_text_range(
1866        &self,
1867        range_utf16: std::ops::Range<usize>,
1868        _bounds: RectF,
1869        _visible_bounds: RectF,
1870        _layout: &Self::LayoutState,
1871        _paint: &Self::PaintState,
1872        view: &V,
1873        cx: &gpui::ViewContext<V>,
1874    ) -> Option<RectF> {
1875        self.child.rect_for_text_range(range_utf16, view, cx)
1876    }
1877
1878    fn debug(
1879        &self,
1880        _bounds: RectF,
1881        _layout: &Self::LayoutState,
1882        _paint: &Self::PaintState,
1883        view: &V,
1884        cx: &gpui::ViewContext<V>,
1885    ) -> serde_json::Value {
1886        gpui::json::json!({
1887            "type": "Pane Back Drop",
1888            "view": self.child_view,
1889            "child": self.child.debug(view, cx),
1890        })
1891    }
1892}
1893
1894#[cfg(test)]
1895mod tests {
1896    use super::*;
1897    use crate::item::test::{TestItem, TestProjectItem};
1898    use gpui::TestAppContext;
1899    use project::FakeFs;
1900    use settings::SettingsStore;
1901
1902    #[gpui::test]
1903    async fn test_remove_active_empty(cx: &mut TestAppContext) {
1904        init_test(cx);
1905        let fs = FakeFs::new(cx.background());
1906
1907        let project = Project::test(fs, None, cx).await;
1908        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
1909        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
1910
1911        pane.update(cx, |pane, cx| {
1912            assert!(pane.close_active_item(&CloseActiveItem, cx).is_none())
1913        });
1914    }
1915
1916    #[gpui::test]
1917    async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
1918        cx.foreground().forbid_parking();
1919        init_test(cx);
1920        let fs = FakeFs::new(cx.background());
1921
1922        let project = Project::test(fs, None, cx).await;
1923        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
1924        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
1925
1926        // 1. Add with a destination index
1927        //   a. Add before the active item
1928        set_labeled_items(&pane, ["A", "B*", "C"], cx);
1929        pane.update(cx, |pane, cx| {
1930            pane.add_item(
1931                Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
1932                false,
1933                false,
1934                Some(0),
1935                cx,
1936            );
1937        });
1938        assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
1939
1940        //   b. Add after the active item
1941        set_labeled_items(&pane, ["A", "B*", "C"], cx);
1942        pane.update(cx, |pane, cx| {
1943            pane.add_item(
1944                Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
1945                false,
1946                false,
1947                Some(2),
1948                cx,
1949            );
1950        });
1951        assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
1952
1953        //   c. Add at the end of the item list (including off the length)
1954        set_labeled_items(&pane, ["A", "B*", "C"], cx);
1955        pane.update(cx, |pane, cx| {
1956            pane.add_item(
1957                Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
1958                false,
1959                false,
1960                Some(5),
1961                cx,
1962            );
1963        });
1964        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
1965
1966        // 2. Add without a destination index
1967        //   a. Add with active item at the start of the item list
1968        set_labeled_items(&pane, ["A*", "B", "C"], cx);
1969        pane.update(cx, |pane, cx| {
1970            pane.add_item(
1971                Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
1972                false,
1973                false,
1974                None,
1975                cx,
1976            );
1977        });
1978        set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
1979
1980        //   b. Add with active item at the end of the item list
1981        set_labeled_items(&pane, ["A", "B", "C*"], cx);
1982        pane.update(cx, |pane, cx| {
1983            pane.add_item(
1984                Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
1985                false,
1986                false,
1987                None,
1988                cx,
1989            );
1990        });
1991        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
1992    }
1993
1994    #[gpui::test]
1995    async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
1996        cx.foreground().forbid_parking();
1997        init_test(cx);
1998        let fs = FakeFs::new(cx.background());
1999
2000        let project = Project::test(fs, None, cx).await;
2001        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2002        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
2003
2004        // 1. Add with a destination index
2005        //   1a. Add before the active item
2006        let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
2007        pane.update(cx, |pane, cx| {
2008            pane.add_item(d, false, false, Some(0), cx);
2009        });
2010        assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
2011
2012        //   1b. Add after the active item
2013        let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
2014        pane.update(cx, |pane, cx| {
2015            pane.add_item(d, false, false, Some(2), cx);
2016        });
2017        assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
2018
2019        //   1c. Add at the end of the item list (including off the length)
2020        let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
2021        pane.update(cx, |pane, cx| {
2022            pane.add_item(a, false, false, Some(5), cx);
2023        });
2024        assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
2025
2026        //   1d. Add same item to active index
2027        let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
2028        pane.update(cx, |pane, cx| {
2029            pane.add_item(b, false, false, Some(1), cx);
2030        });
2031        assert_item_labels(&pane, ["A", "B*", "C"], cx);
2032
2033        //   1e. Add item to index after same item in last position
2034        let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
2035        pane.update(cx, |pane, cx| {
2036            pane.add_item(c, false, false, Some(2), cx);
2037        });
2038        assert_item_labels(&pane, ["A", "B", "C*"], cx);
2039
2040        // 2. Add without a destination index
2041        //   2a. Add with active item at the start of the item list
2042        let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
2043        pane.update(cx, |pane, cx| {
2044            pane.add_item(d, false, false, None, cx);
2045        });
2046        assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
2047
2048        //   2b. Add with active item at the end of the item list
2049        let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
2050        pane.update(cx, |pane, cx| {
2051            pane.add_item(a, false, false, None, cx);
2052        });
2053        assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
2054
2055        //   2c. Add active item to active item at end of list
2056        let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
2057        pane.update(cx, |pane, cx| {
2058            pane.add_item(c, false, false, None, cx);
2059        });
2060        assert_item_labels(&pane, ["A", "B", "C*"], cx);
2061
2062        //   2d. Add active item to active item at start of list
2063        let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
2064        pane.update(cx, |pane, cx| {
2065            pane.add_item(a, false, false, None, cx);
2066        });
2067        assert_item_labels(&pane, ["A*", "B", "C"], cx);
2068    }
2069
2070    #[gpui::test]
2071    async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
2072        cx.foreground().forbid_parking();
2073        init_test(cx);
2074        let fs = FakeFs::new(cx.background());
2075
2076        let project = Project::test(fs, None, cx).await;
2077        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2078        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
2079
2080        // singleton view
2081        pane.update(cx, |pane, cx| {
2082            let item = TestItem::new()
2083                .with_singleton(true)
2084                .with_label("buffer 1")
2085                .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)]);
2086
2087            pane.add_item(Box::new(cx.add_view(|_| item)), false, false, None, cx);
2088        });
2089        assert_item_labels(&pane, ["buffer 1*"], cx);
2090
2091        // new singleton view with the same project entry
2092        pane.update(cx, |pane, cx| {
2093            let item = TestItem::new()
2094                .with_singleton(true)
2095                .with_label("buffer 1")
2096                .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]);
2097
2098            pane.add_item(Box::new(cx.add_view(|_| item)), false, false, None, cx);
2099        });
2100        assert_item_labels(&pane, ["buffer 1*"], cx);
2101
2102        // new singleton view with different project entry
2103        pane.update(cx, |pane, cx| {
2104            let item = TestItem::new()
2105                .with_singleton(true)
2106                .with_label("buffer 2")
2107                .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)]);
2108            pane.add_item(Box::new(cx.add_view(|_| item)), false, false, None, cx);
2109        });
2110        assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
2111
2112        // new multibuffer view with the same project entry
2113        pane.update(cx, |pane, cx| {
2114            let item = TestItem::new()
2115                .with_singleton(false)
2116                .with_label("multibuffer 1")
2117                .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]);
2118
2119            pane.add_item(Box::new(cx.add_view(|_| item)), false, false, None, cx);
2120        });
2121        assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
2122
2123        // another multibuffer view with the same project entry
2124        pane.update(cx, |pane, cx| {
2125            let item = TestItem::new()
2126                .with_singleton(false)
2127                .with_label("multibuffer 1b")
2128                .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]);
2129
2130            pane.add_item(Box::new(cx.add_view(|_| item)), false, false, None, cx);
2131        });
2132        assert_item_labels(
2133            &pane,
2134            ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
2135            cx,
2136        );
2137    }
2138
2139    #[gpui::test]
2140    async fn test_remove_item_ordering(cx: &mut TestAppContext) {
2141        init_test(cx);
2142        let fs = FakeFs::new(cx.background());
2143
2144        let project = Project::test(fs, None, cx).await;
2145        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2146        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
2147
2148        add_labeled_item(&pane, "A", false, cx);
2149        add_labeled_item(&pane, "B", false, cx);
2150        add_labeled_item(&pane, "C", false, cx);
2151        add_labeled_item(&pane, "D", false, cx);
2152        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2153
2154        pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
2155        add_labeled_item(&pane, "1", false, cx);
2156        assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
2157
2158        pane.update(cx, |pane, cx| pane.close_active_item(&CloseActiveItem, cx))
2159            .unwrap()
2160            .await
2161            .unwrap();
2162        assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
2163
2164        pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
2165        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2166
2167        pane.update(cx, |pane, cx| pane.close_active_item(&CloseActiveItem, cx))
2168            .unwrap()
2169            .await
2170            .unwrap();
2171        assert_item_labels(&pane, ["A", "B*", "C"], cx);
2172
2173        pane.update(cx, |pane, cx| pane.close_active_item(&CloseActiveItem, cx))
2174            .unwrap()
2175            .await
2176            .unwrap();
2177        assert_item_labels(&pane, ["A", "C*"], cx);
2178
2179        pane.update(cx, |pane, cx| pane.close_active_item(&CloseActiveItem, cx))
2180            .unwrap()
2181            .await
2182            .unwrap();
2183        assert_item_labels(&pane, ["A*"], cx);
2184    }
2185
2186    #[gpui::test]
2187    async fn test_close_inactive_items(cx: &mut TestAppContext) {
2188        init_test(cx);
2189        let fs = FakeFs::new(cx.background());
2190
2191        let project = Project::test(fs, None, cx).await;
2192        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2193        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
2194
2195        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
2196
2197        pane.update(cx, |pane, cx| {
2198            pane.close_inactive_items(&CloseInactiveItems, cx)
2199        })
2200        .unwrap()
2201        .await
2202        .unwrap();
2203        assert_item_labels(&pane, ["C*"], cx);
2204    }
2205
2206    #[gpui::test]
2207    async fn test_close_clean_items(cx: &mut TestAppContext) {
2208        init_test(cx);
2209        let fs = FakeFs::new(cx.background());
2210
2211        let project = Project::test(fs, None, cx).await;
2212        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2213        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
2214
2215        add_labeled_item(&pane, "A", true, cx);
2216        add_labeled_item(&pane, "B", false, cx);
2217        add_labeled_item(&pane, "C", true, cx);
2218        add_labeled_item(&pane, "D", false, cx);
2219        add_labeled_item(&pane, "E", false, cx);
2220        assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
2221
2222        pane.update(cx, |pane, cx| pane.close_clean_items(&CloseCleanItems, cx))
2223            .unwrap()
2224            .await
2225            .unwrap();
2226        assert_item_labels(&pane, ["A^", "C*^"], cx);
2227    }
2228
2229    #[gpui::test]
2230    async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
2231        init_test(cx);
2232        let fs = FakeFs::new(cx.background());
2233
2234        let project = Project::test(fs, None, cx).await;
2235        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2236        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
2237
2238        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
2239
2240        pane.update(cx, |pane, cx| {
2241            pane.close_items_to_the_left(&CloseItemsToTheLeft, cx)
2242        })
2243        .unwrap()
2244        .await
2245        .unwrap();
2246        assert_item_labels(&pane, ["C*", "D", "E"], cx);
2247    }
2248
2249    #[gpui::test]
2250    async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
2251        init_test(cx);
2252        let fs = FakeFs::new(cx.background());
2253
2254        let project = Project::test(fs, None, cx).await;
2255        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2256        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
2257
2258        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
2259
2260        pane.update(cx, |pane, cx| {
2261            pane.close_items_to_the_right(&CloseItemsToTheRight, cx)
2262        })
2263        .unwrap()
2264        .await
2265        .unwrap();
2266        assert_item_labels(&pane, ["A", "B", "C*"], cx);
2267    }
2268
2269    #[gpui::test]
2270    async fn test_close_all_items(cx: &mut TestAppContext) {
2271        init_test(cx);
2272        let fs = FakeFs::new(cx.background());
2273
2274        let project = Project::test(fs, None, cx).await;
2275        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2276        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
2277
2278        add_labeled_item(&pane, "A", false, cx);
2279        add_labeled_item(&pane, "B", false, cx);
2280        add_labeled_item(&pane, "C", false, cx);
2281        assert_item_labels(&pane, ["A", "B", "C*"], cx);
2282
2283        pane.update(cx, |pane, cx| pane.close_all_items(&CloseAllItems, cx))
2284            .unwrap()
2285            .await
2286            .unwrap();
2287        assert_item_labels(&pane, [], cx);
2288    }
2289
2290    fn init_test(cx: &mut TestAppContext) {
2291        cx.update(|cx| {
2292            cx.set_global(SettingsStore::test(cx));
2293            theme::init((), cx);
2294            crate::init_settings(cx);
2295        });
2296    }
2297
2298    fn add_labeled_item(
2299        pane: &ViewHandle<Pane>,
2300        label: &str,
2301        is_dirty: bool,
2302        cx: &mut TestAppContext,
2303    ) -> Box<ViewHandle<TestItem>> {
2304        pane.update(cx, |pane, cx| {
2305            let labeled_item =
2306                Box::new(cx.add_view(|_| TestItem::new().with_label(label).with_dirty(is_dirty)));
2307            pane.add_item(labeled_item.clone(), false, false, None, cx);
2308            labeled_item
2309        })
2310    }
2311
2312    fn set_labeled_items<const COUNT: usize>(
2313        pane: &ViewHandle<Pane>,
2314        labels: [&str; COUNT],
2315        cx: &mut TestAppContext,
2316    ) -> [Box<ViewHandle<TestItem>>; COUNT] {
2317        pane.update(cx, |pane, cx| {
2318            pane.items.clear();
2319            let mut active_item_index = 0;
2320
2321            let mut index = 0;
2322            let items = labels.map(|mut label| {
2323                if label.ends_with("*") {
2324                    label = label.trim_end_matches("*");
2325                    active_item_index = index;
2326                }
2327
2328                let labeled_item = Box::new(cx.add_view(|_| TestItem::new().with_label(label)));
2329                pane.add_item(labeled_item.clone(), false, false, None, cx);
2330                index += 1;
2331                labeled_item
2332            });
2333
2334            pane.activate_item(active_item_index, false, false, cx);
2335
2336            items
2337        })
2338    }
2339
2340    // Assert the item label, with the active item label suffixed with a '*'
2341    fn assert_item_labels<const COUNT: usize>(
2342        pane: &ViewHandle<Pane>,
2343        expected_states: [&str; COUNT],
2344        cx: &mut TestAppContext,
2345    ) {
2346        pane.read_with(cx, |pane, cx| {
2347            let actual_states = pane
2348                .items
2349                .iter()
2350                .enumerate()
2351                .map(|(ix, item)| {
2352                    let mut state = item
2353                        .as_any()
2354                        .downcast_ref::<TestItem>()
2355                        .unwrap()
2356                        .read(cx)
2357                        .label
2358                        .clone();
2359                    if ix == pane.active_item_index {
2360                        state.push('*');
2361                    }
2362                    if item.is_dirty(cx) {
2363                        state.push('^');
2364                    }
2365                    state
2366                })
2367                .collect::<Vec<_>>();
2368
2369            assert_eq!(
2370                actual_states, expected_states,
2371                "pane items do not match expectation"
2372            );
2373        })
2374    }
2375}