pane.rs

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