pane.rs

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