pane.rs

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