pane.rs

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