pane.rs

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