pane.rs

   1use super::{ItemHandle, SplitDirection};
   2use crate::{toolbar::Toolbar, Item, NewFile, NewSearch, NewTerminal, WeakItemHandle, Workspace};
   3use anyhow::Result;
   4use collections::{HashMap, HashSet, VecDeque};
   5use context_menu::{ContextMenu, ContextMenuItem};
   6use drag_and_drop::{DragAndDrop, Draggable};
   7use futures::StreamExt;
   8use gpui::{
   9    actions,
  10    color::Color,
  11    elements::*,
  12    geometry::{
  13        rect::RectF,
  14        vector::{vec2f, Vector2F},
  15    },
  16    impl_actions, impl_internal_actions,
  17    platform::{CursorStyle, NavigationDirection},
  18    AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, EventContext,
  19    ModelHandle, MouseButton, MutableAppContext, PromptLevel, Quad, RenderContext, Task, View,
  20    ViewContext, ViewHandle, WeakViewHandle,
  21};
  22use project::{Project, ProjectEntryId, ProjectPath};
  23use serde::Deserialize;
  24use settings::{Autosave, Settings};
  25use std::{any::Any, cell::RefCell, cmp, mem, path::Path, rc::Rc};
  26use theme::Theme;
  27use util::ResultExt;
  28
  29#[derive(Clone, Deserialize, PartialEq)]
  30pub struct ActivateItem(pub usize);
  31
  32actions!(
  33    pane,
  34    [
  35        ActivatePrevItem,
  36        ActivateNextItem,
  37        ActivateLastItem,
  38        CloseActiveItem,
  39        CloseInactiveItems,
  40        ReopenClosedItem,
  41        SplitLeft,
  42        SplitUp,
  43        SplitRight,
  44        SplitDown,
  45    ]
  46);
  47
  48#[derive(Clone, PartialEq)]
  49pub struct CloseItem {
  50    pub item_id: usize,
  51    pub pane: WeakViewHandle<Pane>,
  52}
  53
  54#[derive(Clone, PartialEq)]
  55pub struct MoveItem {
  56    pub item_id: usize,
  57    pub from: WeakViewHandle<Pane>,
  58    pub to: WeakViewHandle<Pane>,
  59    pub destination_index: usize,
  60}
  61
  62#[derive(Clone, Deserialize, PartialEq)]
  63pub struct GoBack {
  64    #[serde(skip_deserializing)]
  65    pub pane: Option<WeakViewHandle<Pane>>,
  66}
  67
  68#[derive(Clone, Deserialize, PartialEq)]
  69pub struct GoForward {
  70    #[serde(skip_deserializing)]
  71    pub pane: Option<WeakViewHandle<Pane>>,
  72}
  73
  74#[derive(Clone, PartialEq)]
  75pub struct DeploySplitMenu {
  76    position: Vector2F,
  77}
  78
  79#[derive(Clone, PartialEq)]
  80pub struct DeployNewMenu {
  81    position: Vector2F,
  82}
  83
  84impl_actions!(pane, [GoBack, GoForward, ActivateItem]);
  85impl_internal_actions!(pane, [CloseItem, DeploySplitMenu, DeployNewMenu, MoveItem]);
  86
  87const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
  88
  89pub fn init(cx: &mut MutableAppContext) {
  90    cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| {
  91        pane.activate_item(action.0, true, true, cx);
  92    });
  93    cx.add_action(|pane: &mut Pane, _: &ActivateLastItem, cx| {
  94        pane.activate_item(pane.items.len() - 1, true, true, cx);
  95    });
  96    cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
  97        pane.activate_prev_item(cx);
  98    });
  99    cx.add_action(|pane: &mut Pane, _: &ActivateNextItem, cx| {
 100        pane.activate_next_item(cx);
 101    });
 102    cx.add_async_action(Pane::close_active_item);
 103    cx.add_async_action(Pane::close_inactive_items);
 104    cx.add_async_action(|workspace: &mut Workspace, action: &CloseItem, cx| {
 105        let pane = action.pane.upgrade(cx)?;
 106        let task = Pane::close_item(workspace, pane, action.item_id, cx);
 107        Some(cx.foreground().spawn(async move {
 108            task.await?;
 109            Ok(())
 110        }))
 111    });
 112    cx.add_action(
 113        |workspace,
 114         MoveItem {
 115             from,
 116             to,
 117             item_id,
 118             destination_index,
 119         },
 120         cx| {
 121            // Get item handle to move
 122            let from = if let Some(from) = from.upgrade(cx) {
 123                from
 124            } else {
 125                return;
 126            };
 127
 128            // Add item to new pane at given index
 129            let to = if let Some(to) = to.upgrade(cx) {
 130                to
 131            } else {
 132                return;
 133            };
 134
 135            Pane::move_item(workspace, from, to, *item_id, *destination_index, cx)
 136        },
 137    );
 138    cx.add_action(|pane: &mut Pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx));
 139    cx.add_action(|pane: &mut Pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx));
 140    cx.add_action(|pane: &mut Pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx));
 141    cx.add_action(|pane: &mut Pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx));
 142    cx.add_action(Pane::deploy_split_menu);
 143    cx.add_action(Pane::deploy_new_menu);
 144    cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
 145        Pane::reopen_closed_item(workspace, cx).detach();
 146    });
 147    cx.add_action(|workspace: &mut Workspace, action: &GoBack, cx| {
 148        Pane::go_back(
 149            workspace,
 150            action
 151                .pane
 152                .as_ref()
 153                .and_then(|weak_handle| weak_handle.upgrade(cx)),
 154            cx,
 155        )
 156        .detach();
 157    });
 158    cx.add_action(|workspace: &mut Workspace, action: &GoForward, cx| {
 159        Pane::go_forward(
 160            workspace,
 161            action
 162                .pane
 163                .as_ref()
 164                .and_then(|weak_handle| weak_handle.upgrade(cx)),
 165            cx,
 166        )
 167        .detach();
 168    });
 169}
 170
 171pub enum Event {
 172    Focused,
 173    ActivateItem { local: bool },
 174    Remove,
 175    RemoveItem { item_id: usize },
 176    Split(SplitDirection),
 177    ChangeItemTitle,
 178}
 179
 180pub struct Pane {
 181    items: Vec<Box<dyn ItemHandle>>,
 182    is_active: bool,
 183    active_item_index: usize,
 184    last_focused_view: Option<AnyWeakViewHandle>,
 185    autoscroll: bool,
 186    nav_history: Rc<RefCell<NavHistory>>,
 187    toolbar: ViewHandle<Toolbar>,
 188    context_menu: ViewHandle<ContextMenu>,
 189}
 190
 191pub struct ItemNavHistory {
 192    history: Rc<RefCell<NavHistory>>,
 193    item: Rc<dyn WeakItemHandle>,
 194}
 195
 196struct NavHistory {
 197    mode: NavigationMode,
 198    backward_stack: VecDeque<NavigationEntry>,
 199    forward_stack: VecDeque<NavigationEntry>,
 200    closed_stack: VecDeque<NavigationEntry>,
 201    paths_by_item: HashMap<usize, ProjectPath>,
 202    pane: WeakViewHandle<Pane>,
 203}
 204
 205#[derive(Copy, Clone)]
 206enum NavigationMode {
 207    Normal,
 208    GoingBack,
 209    GoingForward,
 210    ClosingItem,
 211    ReopeningClosedItem,
 212    Disabled,
 213}
 214
 215impl Default for NavigationMode {
 216    fn default() -> Self {
 217        Self::Normal
 218    }
 219}
 220
 221pub struct NavigationEntry {
 222    pub item: Rc<dyn WeakItemHandle>,
 223    pub data: Option<Box<dyn Any>>,
 224}
 225
 226struct DraggedItem {
 227    item: Box<dyn ItemHandle>,
 228    pane: WeakViewHandle<Pane>,
 229}
 230
 231pub enum ReorderBehavior {
 232    None,
 233    MoveAfterActive,
 234    MoveToIndex(usize),
 235}
 236
 237impl Pane {
 238    pub fn new(cx: &mut ViewContext<Self>) -> Self {
 239        let handle = cx.weak_handle();
 240        let context_menu = cx.add_view(ContextMenu::new);
 241        Self {
 242            items: Vec::new(),
 243            is_active: true,
 244            active_item_index: 0,
 245            last_focused_view: None,
 246            autoscroll: false,
 247            nav_history: Rc::new(RefCell::new(NavHistory {
 248                mode: NavigationMode::Normal,
 249                backward_stack: Default::default(),
 250                forward_stack: Default::default(),
 251                closed_stack: Default::default(),
 252                paths_by_item: Default::default(),
 253                pane: handle.clone(),
 254            })),
 255            toolbar: cx.add_view(|_| Toolbar::new(handle)),
 256            context_menu,
 257        }
 258    }
 259
 260    pub fn set_active(&mut self, is_active: bool, cx: &mut ViewContext<Self>) {
 261        self.is_active = is_active;
 262        cx.notify();
 263    }
 264
 265    pub fn nav_history_for_item<T: Item>(&self, item: &ViewHandle<T>) -> ItemNavHistory {
 266        ItemNavHistory {
 267            history: self.nav_history.clone(),
 268            item: Rc::new(item.downgrade()),
 269        }
 270    }
 271
 272    pub fn go_back(
 273        workspace: &mut Workspace,
 274        pane: Option<ViewHandle<Pane>>,
 275        cx: &mut ViewContext<Workspace>,
 276    ) -> Task<()> {
 277        Self::navigate_history(
 278            workspace,
 279            pane.unwrap_or_else(|| workspace.active_pane().clone()),
 280            NavigationMode::GoingBack,
 281            cx,
 282        )
 283    }
 284
 285    pub fn go_forward(
 286        workspace: &mut Workspace,
 287        pane: Option<ViewHandle<Pane>>,
 288        cx: &mut ViewContext<Workspace>,
 289    ) -> Task<()> {
 290        Self::navigate_history(
 291            workspace,
 292            pane.unwrap_or_else(|| workspace.active_pane().clone()),
 293            NavigationMode::GoingForward,
 294            cx,
 295        )
 296    }
 297
 298    pub fn reopen_closed_item(
 299        workspace: &mut Workspace,
 300        cx: &mut ViewContext<Workspace>,
 301    ) -> Task<()> {
 302        Self::navigate_history(
 303            workspace,
 304            workspace.active_pane().clone(),
 305            NavigationMode::ReopeningClosedItem,
 306            cx,
 307        )
 308    }
 309
 310    pub fn disable_history(&mut self) {
 311        self.nav_history.borrow_mut().disable();
 312    }
 313
 314    pub fn enable_history(&mut self) {
 315        self.nav_history.borrow_mut().enable();
 316    }
 317
 318    pub fn can_navigate_backward(&self) -> bool {
 319        !self.nav_history.borrow().backward_stack.is_empty()
 320    }
 321
 322    pub fn can_navigate_forward(&self) -> bool {
 323        !self.nav_history.borrow().forward_stack.is_empty()
 324    }
 325
 326    fn history_updated(&mut self, cx: &mut ViewContext<Self>) {
 327        self.toolbar.update(cx, |_, cx| cx.notify());
 328    }
 329
 330    fn navigate_history(
 331        workspace: &mut Workspace,
 332        pane: ViewHandle<Pane>,
 333        mode: NavigationMode,
 334        cx: &mut ViewContext<Workspace>,
 335    ) -> Task<()> {
 336        cx.focus(pane.clone());
 337
 338        let to_load = pane.update(cx, |pane, cx| {
 339            loop {
 340                // Retrieve the weak item handle from the history.
 341                let entry = pane.nav_history.borrow_mut().pop(mode, cx)?;
 342
 343                // If the item is still present in this pane, then activate it.
 344                if let Some(index) = entry
 345                    .item
 346                    .upgrade(cx)
 347                    .and_then(|v| pane.index_for_item(v.as_ref()))
 348                {
 349                    let prev_active_item_index = pane.active_item_index;
 350                    pane.nav_history.borrow_mut().set_mode(mode);
 351                    pane.activate_item(index, true, true, cx);
 352                    pane.nav_history
 353                        .borrow_mut()
 354                        .set_mode(NavigationMode::Normal);
 355
 356                    let mut navigated = prev_active_item_index != pane.active_item_index;
 357                    if let Some(data) = entry.data {
 358                        navigated |= pane.active_item()?.navigate(data, cx);
 359                    }
 360
 361                    if navigated {
 362                        break None;
 363                    }
 364                }
 365                // If the item is no longer present in this pane, then retrieve its
 366                // project path in order to reopen it.
 367                else {
 368                    break pane
 369                        .nav_history
 370                        .borrow()
 371                        .paths_by_item
 372                        .get(&entry.item.id())
 373                        .cloned()
 374                        .map(|project_path| (project_path, entry));
 375                }
 376            }
 377        });
 378
 379        if let Some((project_path, entry)) = to_load {
 380            // If the item was no longer present, then load it again from its previous path.
 381            let pane = pane.downgrade();
 382            let task = workspace.load_path(project_path, cx);
 383            cx.spawn(|workspace, mut cx| async move {
 384                let task = task.await;
 385                if let Some(pane) = pane.upgrade(&cx) {
 386                    let mut navigated = false;
 387                    if let Some((project_entry_id, build_item)) = task.log_err() {
 388                        let prev_active_item_id = pane.update(&mut cx, |pane, _| {
 389                            pane.nav_history.borrow_mut().set_mode(mode);
 390                            pane.active_item().map(|p| p.id())
 391                        });
 392
 393                        let item = workspace.update(&mut cx, |workspace, cx| {
 394                            Self::open_item(
 395                                workspace,
 396                                pane.clone(),
 397                                project_entry_id,
 398                                true,
 399                                cx,
 400                                build_item,
 401                            )
 402                        });
 403
 404                        pane.update(&mut cx, |pane, cx| {
 405                            navigated |= Some(item.id()) != prev_active_item_id;
 406                            pane.nav_history
 407                                .borrow_mut()
 408                                .set_mode(NavigationMode::Normal);
 409                            if let Some(data) = entry.data {
 410                                navigated |= item.navigate(data, cx);
 411                            }
 412                        });
 413                    }
 414
 415                    if !navigated {
 416                        workspace
 417                            .update(&mut cx, |workspace, cx| {
 418                                Self::navigate_history(workspace, pane, mode, cx)
 419                            })
 420                            .await;
 421                    }
 422                }
 423            })
 424        } else {
 425            Task::ready(())
 426        }
 427    }
 428
 429    pub(crate) fn open_item(
 430        workspace: &mut Workspace,
 431        pane: ViewHandle<Pane>,
 432        project_entry_id: ProjectEntryId,
 433        focus_item: bool,
 434        cx: &mut ViewContext<Workspace>,
 435        build_item: impl FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
 436    ) -> Box<dyn ItemHandle> {
 437        let existing_item = pane.update(cx, |pane, cx| {
 438            for item in pane.items.iter() {
 439                if item.project_path(cx).is_some()
 440                    && item.project_entry_ids(cx).as_slice() == [project_entry_id]
 441                {
 442                    let item = item.boxed_clone();
 443                    return Some(item);
 444                }
 445            }
 446            None
 447        });
 448
 449        // Even if the item exists, we re-add it to reorder it after the active item.
 450        // We may revisit this behavior after adding an "activation history" for pane items.
 451        let item = existing_item.unwrap_or_else(|| pane.update(cx, |_, cx| build_item(cx)));
 452        Pane::add_item(workspace, &pane, item.clone(), true, focus_item, None, cx);
 453        item
 454    }
 455
 456    pub(crate) fn add_item(
 457        workspace: &mut Workspace,
 458        pane: &ViewHandle<Pane>,
 459        item: Box<dyn ItemHandle>,
 460        activate_pane: bool,
 461        focus_item: bool,
 462        destination_index: Option<usize>,
 463        cx: &mut ViewContext<Workspace>,
 464    ) {
 465        // If no destination index is specified, add or move the item after the active item.
 466        let mut insertion_index = {
 467            let pane = pane.read(cx);
 468            cmp::min(
 469                if let Some(destination_index) = destination_index {
 470                    destination_index
 471                } else {
 472                    pane.active_item_index + 1
 473                },
 474                pane.items.len(),
 475            )
 476        };
 477
 478        item.added_to_pane(workspace, pane.clone(), cx);
 479
 480        // Does the item already exist?
 481        let project_entry_id = if item.is_singleton(cx) {
 482            item.project_entry_ids(cx).get(0).copied()
 483        } else {
 484            None
 485        };
 486
 487        let existing_item_index = pane.read(cx).items.iter().position(|existing_item| {
 488            if existing_item.id() == item.id() {
 489                true
 490            } else if existing_item.is_singleton(cx) {
 491                existing_item
 492                    .project_entry_ids(cx)
 493                    .get(0)
 494                    .map_or(false, |existing_entry_id| {
 495                        Some(existing_entry_id) == project_entry_id.as_ref()
 496                    })
 497            } else {
 498                false
 499            }
 500        });
 501
 502        if let Some(existing_item_index) = existing_item_index {
 503            // If the item already exists, move it to the desired destination and activate it
 504            pane.update(cx, |pane, cx| {
 505                if existing_item_index != insertion_index {
 506                    cx.reparent(&item);
 507                    let existing_item_is_active = existing_item_index == pane.active_item_index;
 508
 509                    // If the caller didn't specify a destination and the added item is already
 510                    // the active one, don't move it
 511                    if existing_item_is_active && destination_index.is_none() {
 512                        insertion_index = existing_item_index;
 513                    } else {
 514                        pane.items.remove(existing_item_index);
 515                        if existing_item_index < pane.active_item_index {
 516                            pane.active_item_index -= 1;
 517                        }
 518                        insertion_index = insertion_index.min(pane.items.len());
 519
 520                        pane.items.insert(insertion_index, item.clone());
 521
 522                        if existing_item_is_active {
 523                            pane.active_item_index = insertion_index;
 524                        } else if insertion_index <= pane.active_item_index {
 525                            pane.active_item_index += 1;
 526                        }
 527                    }
 528
 529                    cx.notify();
 530                }
 531
 532                pane.activate_item(insertion_index, activate_pane, focus_item, cx);
 533            });
 534        } else {
 535            pane.update(cx, |pane, cx| {
 536                cx.reparent(&item);
 537                pane.items.insert(insertion_index, item);
 538                if insertion_index <= pane.active_item_index {
 539                    pane.active_item_index += 1;
 540                }
 541
 542                pane.activate_item(insertion_index, activate_pane, focus_item, cx);
 543                cx.notify();
 544            });
 545        }
 546    }
 547
 548    pub fn items(&self) -> impl Iterator<Item = &Box<dyn ItemHandle>> {
 549        self.items.iter()
 550    }
 551
 552    pub fn items_of_type<T: View>(&self) -> impl '_ + Iterator<Item = ViewHandle<T>> {
 553        self.items
 554            .iter()
 555            .filter_map(|item| item.to_any().downcast())
 556    }
 557
 558    pub fn active_item(&self) -> Option<Box<dyn ItemHandle>> {
 559        self.items.get(self.active_item_index).cloned()
 560    }
 561
 562    pub fn item_for_entry(
 563        &self,
 564        entry_id: ProjectEntryId,
 565        cx: &AppContext,
 566    ) -> Option<Box<dyn ItemHandle>> {
 567        self.items.iter().find_map(|item| {
 568            if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
 569                Some(item.boxed_clone())
 570            } else {
 571                None
 572            }
 573        })
 574    }
 575
 576    pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option<usize> {
 577        self.items.iter().position(|i| i.id() == item.id())
 578    }
 579
 580    pub fn activate_item(
 581        &mut self,
 582        index: usize,
 583        activate_pane: bool,
 584        focus_item: bool,
 585        cx: &mut ViewContext<Self>,
 586    ) {
 587        use NavigationMode::{GoingBack, GoingForward};
 588        if index < self.items.len() {
 589            let prev_active_item_ix = mem::replace(&mut self.active_item_index, index);
 590            if prev_active_item_ix != self.active_item_index
 591                || matches!(self.nav_history.borrow().mode, GoingBack | GoingForward)
 592            {
 593                if let Some(prev_item) = self.items.get(prev_active_item_ix) {
 594                    prev_item.deactivated(cx);
 595                }
 596                cx.emit(Event::ActivateItem {
 597                    local: activate_pane,
 598                });
 599            }
 600            self.update_toolbar(cx);
 601            if focus_item {
 602                self.focus_active_item(cx);
 603            }
 604            if activate_pane {
 605                cx.emit(Event::Focused);
 606            }
 607            self.autoscroll = true;
 608            cx.notify();
 609        }
 610    }
 611
 612    pub fn activate_prev_item(&mut self, cx: &mut ViewContext<Self>) {
 613        let mut index = self.active_item_index;
 614        if index > 0 {
 615            index -= 1;
 616        } else if !self.items.is_empty() {
 617            index = self.items.len() - 1;
 618        }
 619        self.activate_item(index, true, true, cx);
 620    }
 621
 622    pub fn activate_next_item(&mut self, cx: &mut ViewContext<Self>) {
 623        let mut index = self.active_item_index;
 624        if index + 1 < self.items.len() {
 625            index += 1;
 626        } else {
 627            index = 0;
 628        }
 629        self.activate_item(index, true, true, cx);
 630    }
 631
 632    pub fn close_active_item(
 633        workspace: &mut Workspace,
 634        _: &CloseActiveItem,
 635        cx: &mut ViewContext<Workspace>,
 636    ) -> Option<Task<Result<()>>> {
 637        let pane_handle = workspace.active_pane().clone();
 638        let pane = pane_handle.read(cx);
 639        if pane.items.is_empty() {
 640            None
 641        } else {
 642            let item_id_to_close = pane.items[pane.active_item_index].id();
 643            let task = Self::close_items(workspace, pane_handle, cx, move |item_id| {
 644                item_id == item_id_to_close
 645            });
 646            Some(cx.foreground().spawn(async move {
 647                task.await?;
 648                Ok(())
 649            }))
 650        }
 651    }
 652
 653    pub fn close_inactive_items(
 654        workspace: &mut Workspace,
 655        _: &CloseInactiveItems,
 656        cx: &mut ViewContext<Workspace>,
 657    ) -> Option<Task<Result<()>>> {
 658        let pane_handle = workspace.active_pane().clone();
 659        let pane = pane_handle.read(cx);
 660        if pane.items.is_empty() {
 661            None
 662        } else {
 663            let active_item_id = pane.items[pane.active_item_index].id();
 664            let task =
 665                Self::close_items(workspace, pane_handle, cx, move |id| id != active_item_id);
 666            Some(cx.foreground().spawn(async move {
 667                task.await?;
 668                Ok(())
 669            }))
 670        }
 671    }
 672
 673    pub fn close_item(
 674        workspace: &mut Workspace,
 675        pane: ViewHandle<Pane>,
 676        item_id_to_close: usize,
 677        cx: &mut ViewContext<Workspace>,
 678    ) -> Task<Result<bool>> {
 679        Self::close_items(workspace, pane, cx, move |view_id| {
 680            view_id == item_id_to_close
 681        })
 682    }
 683
 684    pub fn close_items(
 685        workspace: &mut Workspace,
 686        pane: ViewHandle<Pane>,
 687        cx: &mut ViewContext<Workspace>,
 688        should_close: impl 'static + Fn(usize) -> bool,
 689    ) -> Task<Result<bool>> {
 690        let project = workspace.project().clone();
 691
 692        // Find the items to close.
 693        let mut items_to_close = Vec::new();
 694        for item in &pane.read(cx).items {
 695            if should_close(item.id()) {
 696                items_to_close.push(item.boxed_clone());
 697            }
 698        }
 699
 700        // If a buffer is open both in a singleton editor and in a multibuffer, make sure
 701        // to focus the singleton buffer when prompting to save that buffer, as opposed
 702        // to focusing the multibuffer, because this gives the user a more clear idea
 703        // of what content they would be saving.
 704        items_to_close.sort_by_key(|item| !item.is_singleton(cx));
 705
 706        cx.spawn(|workspace, mut cx| async move {
 707            let mut saved_project_entry_ids = HashSet::default();
 708            for item in items_to_close.clone() {
 709                // Find the item's current index and its set of project entries. Avoid
 710                // storing these in advance, in case they have changed since this task
 711                // was started.
 712                let (item_ix, mut project_entry_ids) = pane.read_with(&cx, |pane, cx| {
 713                    (pane.index_for_item(&*item), item.project_entry_ids(cx))
 714                });
 715                let item_ix = if let Some(ix) = item_ix {
 716                    ix
 717                } else {
 718                    continue;
 719                };
 720
 721                // If an item hasn't yet been associated with a project entry, then always
 722                // prompt to save it before closing it. Otherwise, check if the item has
 723                // any project entries that are not open anywhere else in the workspace,
 724                // AND that the user has not already been prompted to save. If there are
 725                // any such project entries, prompt the user to save this item.
 726                let should_save = if project_entry_ids.is_empty() {
 727                    true
 728                } else {
 729                    workspace.read_with(&cx, |workspace, cx| {
 730                        for item in workspace.items(cx) {
 731                            if !items_to_close
 732                                .iter()
 733                                .any(|item_to_close| item_to_close.id() == item.id())
 734                            {
 735                                let other_project_entry_ids = item.project_entry_ids(cx);
 736                                project_entry_ids
 737                                    .retain(|id| !other_project_entry_ids.contains(id));
 738                            }
 739                        }
 740                    });
 741                    project_entry_ids
 742                        .iter()
 743                        .any(|id| saved_project_entry_ids.insert(*id))
 744                };
 745
 746                if should_save
 747                    && !Self::save_item(project.clone(), &pane, item_ix, &*item, true, &mut cx)
 748                        .await?
 749                {
 750                    break;
 751                }
 752
 753                // Remove the item from the pane.
 754                pane.update(&mut cx, |pane, cx| {
 755                    if let Some(item_ix) = pane.items.iter().position(|i| i.id() == item.id()) {
 756                        pane.remove_item(item_ix, cx);
 757                    }
 758                });
 759            }
 760
 761            pane.update(&mut cx, |_, cx| cx.notify());
 762            Ok(true)
 763        })
 764    }
 765
 766    fn remove_item(&mut self, item_ix: usize, cx: &mut ViewContext<Self>) {
 767        if item_ix == self.active_item_index {
 768            // Activate the previous item if possible.
 769            // This returns the user to the previously opened tab if they closed
 770            // a new item they just navigated to.
 771            if item_ix > 0 {
 772                self.activate_prev_item(cx);
 773            } else if item_ix + 1 < self.items.len() {
 774                self.activate_next_item(cx);
 775            }
 776        }
 777
 778        let item = self.items.remove(item_ix);
 779        cx.emit(Event::RemoveItem { item_id: item.id() });
 780        if self.items.is_empty() {
 781            item.deactivated(cx);
 782            self.update_toolbar(cx);
 783            cx.emit(Event::Remove);
 784        }
 785
 786        if item_ix < self.active_item_index {
 787            self.active_item_index -= 1;
 788        }
 789
 790        self.nav_history
 791            .borrow_mut()
 792            .set_mode(NavigationMode::ClosingItem);
 793        item.deactivated(cx);
 794        self.nav_history
 795            .borrow_mut()
 796            .set_mode(NavigationMode::Normal);
 797
 798        if let Some(path) = item.project_path(cx) {
 799            self.nav_history
 800                .borrow_mut()
 801                .paths_by_item
 802                .insert(item.id(), path);
 803        } else {
 804            self.nav_history
 805                .borrow_mut()
 806                .paths_by_item
 807                .remove(&item.id());
 808        }
 809
 810        cx.notify();
 811    }
 812
 813    pub async fn save_item(
 814        project: ModelHandle<Project>,
 815        pane: &ViewHandle<Pane>,
 816        item_ix: usize,
 817        item: &dyn ItemHandle,
 818        should_prompt_for_save: bool,
 819        cx: &mut AsyncAppContext,
 820    ) -> Result<bool> {
 821        const CONFLICT_MESSAGE: &str =
 822            "This file has changed on disk since you started editing it. Do you want to overwrite it?";
 823        const DIRTY_MESSAGE: &str = "This file contains unsaved edits. Do you want to save it?";
 824
 825        let (has_conflict, is_dirty, can_save, is_singleton) = cx.read(|cx| {
 826            (
 827                item.has_conflict(cx),
 828                item.is_dirty(cx),
 829                item.can_save(cx),
 830                item.is_singleton(cx),
 831            )
 832        });
 833
 834        if has_conflict && can_save {
 835            let mut answer = pane.update(cx, |pane, cx| {
 836                pane.activate_item(item_ix, true, true, cx);
 837                cx.prompt(
 838                    PromptLevel::Warning,
 839                    CONFLICT_MESSAGE,
 840                    &["Overwrite", "Discard", "Cancel"],
 841                )
 842            });
 843            match answer.next().await {
 844                Some(0) => cx.update(|cx| item.save(project, cx)).await?,
 845                Some(1) => cx.update(|cx| item.reload(project, cx)).await?,
 846                _ => return Ok(false),
 847            }
 848        } else if is_dirty && (can_save || is_singleton) {
 849            let will_autosave = cx.read(|cx| {
 850                matches!(
 851                    cx.global::<Settings>().autosave,
 852                    Autosave::OnFocusChange | Autosave::OnWindowChange
 853                ) && Self::can_autosave_item(&*item, cx)
 854            });
 855            let should_save = if should_prompt_for_save && !will_autosave {
 856                let mut answer = pane.update(cx, |pane, cx| {
 857                    pane.activate_item(item_ix, true, true, cx);
 858                    cx.prompt(
 859                        PromptLevel::Warning,
 860                        DIRTY_MESSAGE,
 861                        &["Save", "Don't Save", "Cancel"],
 862                    )
 863                });
 864                match answer.next().await {
 865                    Some(0) => true,
 866                    Some(1) => false,
 867                    _ => return Ok(false),
 868                }
 869            } else {
 870                true
 871            };
 872
 873            if should_save {
 874                if can_save {
 875                    cx.update(|cx| item.save(project, cx)).await?;
 876                } else if is_singleton {
 877                    let start_abs_path = project
 878                        .read_with(cx, |project, cx| {
 879                            let worktree = project.visible_worktrees(cx).next()?;
 880                            Some(worktree.read(cx).as_local()?.abs_path().to_path_buf())
 881                        })
 882                        .unwrap_or_else(|| Path::new("").into());
 883
 884                    let mut abs_path = cx.update(|cx| cx.prompt_for_new_path(&start_abs_path));
 885                    if let Some(abs_path) = abs_path.next().await.flatten() {
 886                        cx.update(|cx| item.save_as(project, abs_path, cx)).await?;
 887                    } else {
 888                        return Ok(false);
 889                    }
 890                }
 891            }
 892        }
 893        Ok(true)
 894    }
 895
 896    fn can_autosave_item(item: &dyn ItemHandle, cx: &AppContext) -> bool {
 897        let is_deleted = item.project_entry_ids(cx).is_empty();
 898        item.is_dirty(cx) && !item.has_conflict(cx) && item.can_save(cx) && !is_deleted
 899    }
 900
 901    pub fn autosave_item(
 902        item: &dyn ItemHandle,
 903        project: ModelHandle<Project>,
 904        cx: &mut MutableAppContext,
 905    ) -> Task<Result<()>> {
 906        if Self::can_autosave_item(item, cx) {
 907            item.save(project, cx)
 908        } else {
 909            Task::ready(Ok(()))
 910        }
 911    }
 912
 913    pub fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
 914        if let Some(active_item) = self.active_item() {
 915            cx.focus(active_item);
 916        }
 917    }
 918
 919    fn move_item(
 920        workspace: &mut Workspace,
 921        from: ViewHandle<Pane>,
 922        to: ViewHandle<Pane>,
 923        item_to_move: usize,
 924        destination_index: usize,
 925        cx: &mut ViewContext<Workspace>,
 926    ) {
 927        let item_to_move = from
 928            .read(cx)
 929            .items()
 930            .enumerate()
 931            .find(|(_, item_handle)| item_handle.id() == item_to_move);
 932
 933        if item_to_move.is_none() {
 934            log::warn!("Tried to move item handle which was not in `from` pane. Maybe tab was closed during drop");
 935            return;
 936        }
 937
 938        let (item_ix, item_handle) = item_to_move.unwrap();
 939        // This automatically removes duplicate items in the pane
 940        Pane::add_item(
 941            workspace,
 942            &to,
 943            item_handle.clone(),
 944            true,
 945            true,
 946            Some(destination_index),
 947            cx,
 948        );
 949
 950        if from != to {
 951            // Close item from previous pane
 952            from.update(cx, |from, cx| {
 953                from.remove_item(item_ix, cx);
 954            });
 955        }
 956
 957        cx.focus(to);
 958    }
 959
 960    pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext<Self>) {
 961        cx.emit(Event::Split(direction));
 962    }
 963
 964    fn deploy_split_menu(&mut self, action: &DeploySplitMenu, cx: &mut ViewContext<Self>) {
 965        self.context_menu.update(cx, |menu, cx| {
 966            menu.show(
 967                action.position,
 968                vec![
 969                    ContextMenuItem::item("Split Right", SplitRight),
 970                    ContextMenuItem::item("Split Left", SplitLeft),
 971                    ContextMenuItem::item("Split Up", SplitUp),
 972                    ContextMenuItem::item("Split Down", SplitDown),
 973                ],
 974                cx,
 975            );
 976        });
 977    }
 978
 979    fn deploy_new_menu(&mut self, action: &DeployNewMenu, cx: &mut ViewContext<Self>) {
 980        self.context_menu.update(cx, |menu, cx| {
 981            menu.show(
 982                action.position,
 983                vec![
 984                    ContextMenuItem::item("New File", NewFile),
 985                    ContextMenuItem::item("New Terminal", NewTerminal),
 986                    ContextMenuItem::item("New Search", NewSearch),
 987                ],
 988                cx,
 989            );
 990        });
 991    }
 992
 993    pub fn toolbar(&self) -> &ViewHandle<Toolbar> {
 994        &self.toolbar
 995    }
 996
 997    fn update_toolbar(&mut self, cx: &mut ViewContext<Self>) {
 998        let active_item = self
 999            .items
1000            .get(self.active_item_index)
1001            .map(|item| item.as_ref());
1002        self.toolbar.update(cx, |toolbar, cx| {
1003            toolbar.set_active_pane_item(active_item, cx);
1004        });
1005    }
1006
1007    fn render_tab_bar(&mut self, cx: &mut RenderContext<Self>) -> impl Element {
1008        let theme = cx.global::<Settings>().theme.clone();
1009        let filler_index = self.items.len();
1010
1011        enum Tabs {}
1012        enum Tab {}
1013        enum Filler {}
1014        let pane = cx.handle();
1015        MouseEventHandler::new::<Tabs, _, _>(0, cx, |_, cx| {
1016            let autoscroll = if mem::take(&mut self.autoscroll) {
1017                Some(self.active_item_index)
1018            } else {
1019                None
1020            };
1021
1022            let pane_active = self.is_active;
1023
1024            let mut row = Flex::row().scrollable::<Tabs, _>(1, autoscroll, cx);
1025            for (ix, (item, detail)) in self
1026                .items
1027                .iter()
1028                .cloned()
1029                .zip(self.tab_details(cx))
1030                .enumerate()
1031            {
1032                let detail = if detail == 0 { None } else { Some(detail) };
1033                let tab_active = ix == self.active_item_index;
1034
1035                row.add_child({
1036                    MouseEventHandler::new::<Tab, _, _>(ix, cx, {
1037                        let item = item.clone();
1038                        let pane = pane.clone();
1039                        let detail = detail.clone();
1040
1041                        let theme = cx.global::<Settings>().theme.clone();
1042
1043                        move |mouse_state, cx| {
1044                            let tab_style =
1045                                theme.workspace.tab_bar.tab_style(pane_active, tab_active);
1046                            let hovered = mouse_state.hovered;
1047                            Self::render_tab(
1048                                &item,
1049                                pane,
1050                                detail,
1051                                hovered,
1052                                Self::tab_overlay_color(hovered, theme.as_ref(), cx),
1053                                tab_style,
1054                                cx,
1055                            )
1056                        }
1057                    })
1058                    .with_cursor_style(if pane_active && tab_active {
1059                        CursorStyle::Arrow
1060                    } else {
1061                        CursorStyle::PointingHand
1062                    })
1063                    .on_down(MouseButton::Left, move |_, cx| {
1064                        cx.dispatch_action(ActivateItem(ix));
1065                    })
1066                    .on_click(MouseButton::Middle, {
1067                        let item = item.clone();
1068                        let pane = pane.clone();
1069                        move |_, cx: &mut EventContext| {
1070                            cx.dispatch_action(CloseItem {
1071                                item_id: item.id(),
1072                                pane: pane.clone(),
1073                            })
1074                        }
1075                    })
1076                    .on_up(MouseButton::Left, {
1077                        let pane = pane.clone();
1078                        move |_, cx: &mut EventContext| Pane::handle_dropped_item(&pane, ix, cx)
1079                    })
1080                    .as_draggable(
1081                        DraggedItem {
1082                            item,
1083                            pane: pane.clone(),
1084                        },
1085                        {
1086                            let theme = cx.global::<Settings>().theme.clone();
1087
1088                            let detail = detail.clone();
1089                            move |dragged_item, cx: &mut RenderContext<Workspace>| {
1090                                let tab_style = &theme.workspace.tab_bar.dragged_tab;
1091                                Self::render_tab(
1092                                    &dragged_item.item,
1093                                    dragged_item.pane.clone(),
1094                                    detail,
1095                                    false,
1096                                    None,
1097                                    &tab_style,
1098                                    cx,
1099                                )
1100                            }
1101                        },
1102                    )
1103                    .boxed()
1104                })
1105            }
1106
1107            // Use the inactive tab style along with the current pane's active status to decide how to render
1108            // the filler
1109            let filler_style = theme.workspace.tab_bar.tab_style(pane_active, false);
1110            row.add_child(
1111                MouseEventHandler::new::<Filler, _, _>(0, cx, |mouse_state, cx| {
1112                    let mut filler = Empty::new()
1113                        .contained()
1114                        .with_style(filler_style.container)
1115                        .with_border(filler_style.container.border);
1116
1117                    if let Some(overlay) = Self::tab_overlay_color(mouse_state.hovered, &theme, cx)
1118                    {
1119                        filler = filler.with_overlay_color(overlay);
1120                    }
1121
1122                    filler.boxed()
1123                })
1124                .flex(1., true)
1125                .named("filler"),
1126            );
1127
1128            row.boxed()
1129        })
1130        .on_up(MouseButton::Left, move |_, cx| {
1131            Pane::handle_dropped_item(&pane, filler_index, cx)
1132        })
1133    }
1134
1135    fn tab_details(&self, cx: &AppContext) -> Vec<usize> {
1136        let mut tab_details = (0..self.items.len()).map(|_| 0).collect::<Vec<_>>();
1137
1138        let mut tab_descriptions = HashMap::default();
1139        let mut done = false;
1140        while !done {
1141            done = true;
1142
1143            // Store item indices by their tab description.
1144            for (ix, (item, detail)) in self.items.iter().zip(&tab_details).enumerate() {
1145                if let Some(description) = item.tab_description(*detail, cx) {
1146                    if *detail == 0
1147                        || Some(&description) != item.tab_description(detail - 1, cx).as_ref()
1148                    {
1149                        tab_descriptions
1150                            .entry(description)
1151                            .or_insert(Vec::new())
1152                            .push(ix);
1153                    }
1154                }
1155            }
1156
1157            // If two or more items have the same tab description, increase their level
1158            // of detail and try again.
1159            for (_, item_ixs) in tab_descriptions.drain() {
1160                if item_ixs.len() > 1 {
1161                    done = false;
1162                    for ix in item_ixs {
1163                        tab_details[ix] += 1;
1164                    }
1165                }
1166            }
1167        }
1168
1169        tab_details
1170    }
1171
1172    fn render_tab<V: View>(
1173        item: &Box<dyn ItemHandle>,
1174        pane: WeakViewHandle<Pane>,
1175        detail: Option<usize>,
1176        hovered: bool,
1177        overlay: Option<Color>,
1178        tab_style: &theme::Tab,
1179        cx: &mut RenderContext<V>,
1180    ) -> ElementBox {
1181        let title = item.tab_content(detail, &tab_style, cx);
1182
1183        let mut tab = Flex::row()
1184            .with_child(
1185                Align::new({
1186                    let diameter = 7.0;
1187                    let icon_color = if item.has_conflict(cx) {
1188                        Some(tab_style.icon_conflict)
1189                    } else if item.is_dirty(cx) {
1190                        Some(tab_style.icon_dirty)
1191                    } else {
1192                        None
1193                    };
1194
1195                    ConstrainedBox::new(
1196                        Canvas::new(move |bounds, _, cx| {
1197                            if let Some(color) = icon_color {
1198                                let square = RectF::new(bounds.origin(), vec2f(diameter, diameter));
1199                                cx.scene.push_quad(Quad {
1200                                    bounds: square,
1201                                    background: Some(color),
1202                                    border: Default::default(),
1203                                    corner_radius: diameter / 2.,
1204                                });
1205                            }
1206                        })
1207                        .boxed(),
1208                    )
1209                    .with_width(diameter)
1210                    .with_height(diameter)
1211                    .boxed()
1212                })
1213                .boxed(),
1214            )
1215            .with_child(
1216                Container::new(Align::new(title).boxed())
1217                    .with_style(ContainerStyle {
1218                        margin: Margin {
1219                            left: tab_style.spacing,
1220                            right: tab_style.spacing,
1221                            ..Default::default()
1222                        },
1223                        ..Default::default()
1224                    })
1225                    .boxed(),
1226            )
1227            .with_child(
1228                Align::new(
1229                    ConstrainedBox::new(if hovered {
1230                        let item_id = item.id();
1231                        enum TabCloseButton {}
1232                        let icon = Svg::new("icons/x_mark_thin_8.svg");
1233                        MouseEventHandler::new::<TabCloseButton, _, _>(
1234                            item_id,
1235                            cx,
1236                            |mouse_state, _| {
1237                                if mouse_state.hovered {
1238                                    icon.with_color(tab_style.icon_close_active).boxed()
1239                                } else {
1240                                    icon.with_color(tab_style.icon_close).boxed()
1241                                }
1242                            },
1243                        )
1244                        .with_padding(Padding::uniform(4.))
1245                        .with_cursor_style(CursorStyle::PointingHand)
1246                        .on_click(MouseButton::Left, {
1247                            let pane = pane.clone();
1248                            move |_, cx| {
1249                                cx.dispatch_action(CloseItem {
1250                                    item_id,
1251                                    pane: pane.clone(),
1252                                })
1253                            }
1254                        })
1255                        .on_click(MouseButton::Middle, |_, cx| cx.propogate_event())
1256                        .named("close-tab-icon")
1257                    } else {
1258                        Empty::new().boxed()
1259                    })
1260                    .with_width(tab_style.icon_width)
1261                    .boxed(),
1262                )
1263                .boxed(),
1264            )
1265            .contained()
1266            .with_style(tab_style.container);
1267
1268        if let Some(overlay) = overlay {
1269            tab = tab.with_overlay_color(overlay);
1270        }
1271
1272        tab.constrained().with_height(tab_style.height).boxed()
1273    }
1274
1275    fn handle_dropped_item(pane: &WeakViewHandle<Pane>, index: usize, cx: &mut EventContext) {
1276        if let Some((_, dragged_item)) = cx
1277            .global::<DragAndDrop<Workspace>>()
1278            .currently_dragged::<DraggedItem>(cx.window_id)
1279        {
1280            cx.dispatch_action(MoveItem {
1281                item_id: dragged_item.item.id(),
1282                from: dragged_item.pane.clone(),
1283                to: pane.clone(),
1284                destination_index: index,
1285            })
1286        } else {
1287            cx.propogate_event();
1288        }
1289    }
1290
1291    fn tab_overlay_color(
1292        hovered: bool,
1293        theme: &Theme,
1294        cx: &mut RenderContext<Self>,
1295    ) -> Option<Color> {
1296        if hovered
1297            && cx
1298                .global::<DragAndDrop<Workspace>>()
1299                .currently_dragged::<DraggedItem>(cx.window_id())
1300                .is_some()
1301        {
1302            Some(theme.workspace.tab_bar.drop_target_overlay_color)
1303        } else {
1304            None
1305        }
1306    }
1307}
1308
1309impl Entity for Pane {
1310    type Event = Event;
1311}
1312
1313impl View for Pane {
1314    fn ui_name() -> &'static str {
1315        "Pane"
1316    }
1317
1318    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
1319        enum SplitIcon {}
1320
1321        let this = cx.handle();
1322
1323        Stack::new()
1324            .with_child(
1325                EventHandler::new(if let Some(active_item) = self.active_item() {
1326                    Flex::column()
1327                        .with_child({
1328                            let mut tab_row = Flex::row()
1329                                .with_child(self.render_tab_bar(cx).flex(1., true).named("tabs"));
1330
1331                            if self.is_active {
1332                                tab_row.add_children([
1333                                    MouseEventHandler::new::<SplitIcon, _, _>(
1334                                        0,
1335                                        cx,
1336                                        |mouse_state, cx| {
1337                                            let theme =
1338                                                &cx.global::<Settings>().theme.workspace.tab_bar;
1339                                            let style =
1340                                                theme.pane_button.style_for(mouse_state, false);
1341                                            Svg::new("icons/plus_12.svg")
1342                                                .with_color(style.color)
1343                                                .constrained()
1344                                                .with_width(style.icon_width)
1345                                                .aligned()
1346                                                .contained()
1347                                                .with_style(style.container)
1348                                                .constrained()
1349                                                .with_width(style.button_width)
1350                                                .with_height(style.button_width)
1351                                                .aligned()
1352                                                .boxed()
1353                                        },
1354                                    )
1355                                    .with_cursor_style(CursorStyle::PointingHand)
1356                                    .on_down(MouseButton::Left, |e, cx| {
1357                                        cx.dispatch_action(DeployNewMenu {
1358                                            position: e.position,
1359                                        });
1360                                    })
1361                                    .boxed(),
1362                                    MouseEventHandler::new::<SplitIcon, _, _>(
1363                                        1,
1364                                        cx,
1365                                        |mouse_state, cx| {
1366                                            let theme =
1367                                                &cx.global::<Settings>().theme.workspace.tab_bar;
1368                                            let style =
1369                                                theme.pane_button.style_for(mouse_state, false);
1370                                            Svg::new("icons/split_12.svg")
1371                                                .with_color(style.color)
1372                                                .constrained()
1373                                                .with_width(style.icon_width)
1374                                                .aligned()
1375                                                .contained()
1376                                                .with_style(style.container)
1377                                                .constrained()
1378                                                .with_width(style.button_width)
1379                                                .with_height(style.button_width)
1380                                                .aligned()
1381                                                .boxed()
1382                                        },
1383                                    )
1384                                    .with_cursor_style(CursorStyle::PointingHand)
1385                                    .on_down(MouseButton::Left, |e, cx| {
1386                                        cx.dispatch_action(DeploySplitMenu {
1387                                            position: e.position,
1388                                        });
1389                                    })
1390                                    .boxed(),
1391                                ])
1392                            }
1393
1394                            tab_row
1395                                .constrained()
1396                                .with_height(cx.global::<Settings>().theme.workspace.tab_bar.height)
1397                                .named("tab bar")
1398                        })
1399                        .with_child(ChildView::new(&self.toolbar).boxed())
1400                        .with_child(ChildView::new(active_item).flex(1., true).boxed())
1401                        .boxed()
1402                } else {
1403                    enum EmptyPane {}
1404                    let theme = cx.global::<Settings>().theme.clone();
1405
1406                    MouseEventHandler::new::<EmptyPane, _, _>(0, cx, |_, _| {
1407                        Empty::new()
1408                            .contained()
1409                            .with_background_color(theme.workspace.background)
1410                            .boxed()
1411                    })
1412                    .on_down(MouseButton::Left, |_, cx| {
1413                        cx.focus_parent_view();
1414                    })
1415                    .boxed()
1416                })
1417                .on_navigate_mouse_down(move |direction, cx| {
1418                    let this = this.clone();
1419                    match direction {
1420                        NavigationDirection::Back => {
1421                            cx.dispatch_action(GoBack { pane: Some(this) })
1422                        }
1423                        NavigationDirection::Forward => {
1424                            cx.dispatch_action(GoForward { pane: Some(this) })
1425                        }
1426                    }
1427
1428                    true
1429                })
1430                .boxed(),
1431            )
1432            .with_child(ChildView::new(&self.context_menu).boxed())
1433            .named("pane")
1434    }
1435
1436    fn on_focus_in(&mut self, focused: AnyViewHandle, cx: &mut ViewContext<Self>) {
1437        if cx.is_self_focused() {
1438            if let Some(last_focused_view) = self
1439                .last_focused_view
1440                .as_ref()
1441                .and_then(|handle| handle.upgrade(cx))
1442            {
1443                cx.focus(last_focused_view);
1444            } else {
1445                self.focus_active_item(cx);
1446            }
1447        } else {
1448            self.last_focused_view = Some(focused.downgrade());
1449        }
1450        cx.emit(Event::Focused);
1451    }
1452}
1453
1454impl ItemNavHistory {
1455    pub fn push<D: 'static + Any>(&self, data: Option<D>, cx: &mut MutableAppContext) {
1456        self.history.borrow_mut().push(data, self.item.clone(), cx);
1457    }
1458
1459    pub fn pop_backward(&self, cx: &mut MutableAppContext) -> Option<NavigationEntry> {
1460        self.history.borrow_mut().pop(NavigationMode::GoingBack, cx)
1461    }
1462
1463    pub fn pop_forward(&self, cx: &mut MutableAppContext) -> Option<NavigationEntry> {
1464        self.history
1465            .borrow_mut()
1466            .pop(NavigationMode::GoingForward, cx)
1467    }
1468}
1469
1470impl NavHistory {
1471    fn set_mode(&mut self, mode: NavigationMode) {
1472        self.mode = mode;
1473    }
1474
1475    fn disable(&mut self) {
1476        self.mode = NavigationMode::Disabled;
1477    }
1478
1479    fn enable(&mut self) {
1480        self.mode = NavigationMode::Normal;
1481    }
1482
1483    fn pop(&mut self, mode: NavigationMode, cx: &mut MutableAppContext) -> Option<NavigationEntry> {
1484        let entry = match mode {
1485            NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
1486                return None
1487            }
1488            NavigationMode::GoingBack => &mut self.backward_stack,
1489            NavigationMode::GoingForward => &mut self.forward_stack,
1490            NavigationMode::ReopeningClosedItem => &mut self.closed_stack,
1491        }
1492        .pop_back();
1493        if entry.is_some() {
1494            self.did_update(cx);
1495        }
1496        entry
1497    }
1498
1499    fn push<D: 'static + Any>(
1500        &mut self,
1501        data: Option<D>,
1502        item: Rc<dyn WeakItemHandle>,
1503        cx: &mut MutableAppContext,
1504    ) {
1505        match self.mode {
1506            NavigationMode::Disabled => {}
1507            NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
1508                if self.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
1509                    self.backward_stack.pop_front();
1510                }
1511                self.backward_stack.push_back(NavigationEntry {
1512                    item,
1513                    data: data.map(|data| Box::new(data) as Box<dyn Any>),
1514                });
1515                self.forward_stack.clear();
1516            }
1517            NavigationMode::GoingBack => {
1518                if self.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
1519                    self.forward_stack.pop_front();
1520                }
1521                self.forward_stack.push_back(NavigationEntry {
1522                    item,
1523                    data: data.map(|data| Box::new(data) as Box<dyn Any>),
1524                });
1525            }
1526            NavigationMode::GoingForward => {
1527                if self.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
1528                    self.backward_stack.pop_front();
1529                }
1530                self.backward_stack.push_back(NavigationEntry {
1531                    item,
1532                    data: data.map(|data| Box::new(data) as Box<dyn Any>),
1533                });
1534            }
1535            NavigationMode::ClosingItem => {
1536                if self.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
1537                    self.closed_stack.pop_front();
1538                }
1539                self.closed_stack.push_back(NavigationEntry {
1540                    item,
1541                    data: data.map(|data| Box::new(data) as Box<dyn Any>),
1542                });
1543            }
1544        }
1545        self.did_update(cx);
1546    }
1547
1548    fn did_update(&self, cx: &mut MutableAppContext) {
1549        if let Some(pane) = self.pane.upgrade(cx) {
1550            cx.defer(move |cx| pane.update(cx, |pane, cx| pane.history_updated(cx)));
1551        }
1552    }
1553}
1554
1555#[cfg(test)]
1556mod tests {
1557    use super::*;
1558    use crate::tests::TestItem;
1559    use gpui::TestAppContext;
1560    use project::FakeFs;
1561
1562    #[gpui::test]
1563    async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
1564        cx.foreground().forbid_parking();
1565        Settings::test_async(cx);
1566        let fs = FakeFs::new(cx.background());
1567
1568        let project = Project::test(fs, None, cx).await;
1569        let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
1570        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
1571
1572        // 1. Add with a destination index
1573        //   a. Add before the active item
1574        set_labeled_items(&workspace, &pane, ["A", "B*", "C"], cx);
1575        workspace.update(cx, |workspace, cx| {
1576            Pane::add_item(
1577                workspace,
1578                &pane,
1579                Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
1580                false,
1581                false,
1582                Some(0),
1583                cx,
1584            );
1585        });
1586        assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
1587
1588        //   b. Add after the active item
1589        set_labeled_items(&workspace, &pane, ["A", "B*", "C"], cx);
1590        workspace.update(cx, |workspace, cx| {
1591            Pane::add_item(
1592                workspace,
1593                &pane,
1594                Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
1595                false,
1596                false,
1597                Some(2),
1598                cx,
1599            );
1600        });
1601        assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
1602
1603        //   c. Add at the end of the item list (including off the length)
1604        set_labeled_items(&workspace, &pane, ["A", "B*", "C"], cx);
1605        workspace.update(cx, |workspace, cx| {
1606            Pane::add_item(
1607                workspace,
1608                &pane,
1609                Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
1610                false,
1611                false,
1612                Some(5),
1613                cx,
1614            );
1615        });
1616        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
1617
1618        // 2. Add without a destination index
1619        //   a. Add with active item at the start of the item list
1620        set_labeled_items(&workspace, &pane, ["A*", "B", "C"], cx);
1621        workspace.update(cx, |workspace, cx| {
1622            Pane::add_item(
1623                workspace,
1624                &pane,
1625                Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
1626                false,
1627                false,
1628                None,
1629                cx,
1630            );
1631        });
1632        set_labeled_items(&workspace, &pane, ["A", "D*", "B", "C"], cx);
1633
1634        //   b. Add with active item at the end of the item list
1635        set_labeled_items(&workspace, &pane, ["A", "B", "C*"], cx);
1636        workspace.update(cx, |workspace, cx| {
1637            Pane::add_item(
1638                workspace,
1639                &pane,
1640                Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
1641                false,
1642                false,
1643                None,
1644                cx,
1645            );
1646        });
1647        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
1648    }
1649
1650    #[gpui::test]
1651    async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
1652        cx.foreground().forbid_parking();
1653        Settings::test_async(cx);
1654        let fs = FakeFs::new(cx.background());
1655
1656        let project = Project::test(fs, None, cx).await;
1657        let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
1658        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
1659
1660        // 1. Add with a destination index
1661        //   1a. Add before the active item
1662        let [_, _, _, d] = set_labeled_items(&workspace, &pane, ["A", "B*", "C", "D"], cx);
1663        workspace.update(cx, |workspace, cx| {
1664            Pane::add_item(workspace, &pane, d, false, false, Some(0), cx);
1665        });
1666        assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
1667
1668        //   1b. Add after the active item
1669        let [_, _, _, d] = set_labeled_items(&workspace, &pane, ["A", "B*", "C", "D"], cx);
1670        workspace.update(cx, |workspace, cx| {
1671            Pane::add_item(workspace, &pane, d, false, false, Some(2), cx);
1672        });
1673        assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
1674
1675        //   1c. Add at the end of the item list (including off the length)
1676        let [a, _, _, _] = set_labeled_items(&workspace, &pane, ["A", "B*", "C", "D"], cx);
1677        workspace.update(cx, |workspace, cx| {
1678            Pane::add_item(workspace, &pane, a, false, false, Some(5), cx);
1679        });
1680        assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
1681
1682        //   1d. Add same item to active index
1683        let [_, b, _] = set_labeled_items(&workspace, &pane, ["A", "B*", "C"], cx);
1684        workspace.update(cx, |workspace, cx| {
1685            Pane::add_item(workspace, &pane, b, false, false, Some(1), cx);
1686        });
1687        assert_item_labels(&pane, ["A", "B*", "C"], cx);
1688
1689        //   1e. Add item to index after same item in last position
1690        let [_, _, c] = set_labeled_items(&workspace, &pane, ["A", "B*", "C"], cx);
1691        workspace.update(cx, |workspace, cx| {
1692            Pane::add_item(workspace, &pane, c, false, false, Some(2), cx);
1693        });
1694        assert_item_labels(&pane, ["A", "B", "C*"], cx);
1695
1696        // 2. Add without a destination index
1697        //   2a. Add with active item at the start of the item list
1698        let [_, _, _, d] = set_labeled_items(&workspace, &pane, ["A*", "B", "C", "D"], cx);
1699        workspace.update(cx, |workspace, cx| {
1700            Pane::add_item(workspace, &pane, d, false, false, None, cx);
1701        });
1702        assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
1703
1704        //   2b. Add with active item at the end of the item list
1705        let [a, _, _, _] = set_labeled_items(&workspace, &pane, ["A", "B", "C", "D*"], cx);
1706        workspace.update(cx, |workspace, cx| {
1707            Pane::add_item(workspace, &pane, a, false, false, None, cx);
1708        });
1709        assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
1710
1711        //   2c. Add active item to active item at end of list
1712        let [_, _, c] = set_labeled_items(&workspace, &pane, ["A", "B", "C*"], cx);
1713        workspace.update(cx, |workspace, cx| {
1714            Pane::add_item(workspace, &pane, c, false, false, None, cx);
1715        });
1716        assert_item_labels(&pane, ["A", "B", "C*"], cx);
1717
1718        //   2d. Add active item to active item at start of list
1719        let [a, _, _] = set_labeled_items(&workspace, &pane, ["A*", "B", "C"], cx);
1720        workspace.update(cx, |workspace, cx| {
1721            Pane::add_item(workspace, &pane, a, false, false, None, cx);
1722        });
1723        assert_item_labels(&pane, ["A*", "B", "C"], cx);
1724    }
1725
1726    #[gpui::test]
1727    async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
1728        cx.foreground().forbid_parking();
1729        Settings::test_async(cx);
1730        let fs = FakeFs::new(cx.background());
1731
1732        let project = Project::test(fs, None, cx).await;
1733        let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
1734        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
1735
1736        // singleton view
1737        workspace.update(cx, |workspace, cx| {
1738            let item = TestItem::new()
1739                .with_singleton(true)
1740                .with_label("buffer 1")
1741                .with_project_entry_ids(&[1]);
1742
1743            Pane::add_item(
1744                workspace,
1745                &pane,
1746                Box::new(cx.add_view(|_| item)),
1747                false,
1748                false,
1749                None,
1750                cx,
1751            );
1752        });
1753        assert_item_labels(&pane, ["buffer 1*"], cx);
1754
1755        // new singleton view with the same project entry
1756        workspace.update(cx, |workspace, cx| {
1757            let item = TestItem::new()
1758                .with_singleton(true)
1759                .with_label("buffer 1")
1760                .with_project_entry_ids(&[1]);
1761
1762            Pane::add_item(
1763                workspace,
1764                &pane,
1765                Box::new(cx.add_view(|_| item)),
1766                false,
1767                false,
1768                None,
1769                cx,
1770            );
1771        });
1772        assert_item_labels(&pane, ["buffer 1*"], cx);
1773
1774        // new singleton view with different project entry
1775        workspace.update(cx, |workspace, cx| {
1776            let item = TestItem::new()
1777                .with_singleton(true)
1778                .with_label("buffer 2")
1779                .with_project_entry_ids(&[2]);
1780
1781            Pane::add_item(
1782                workspace,
1783                &pane,
1784                Box::new(cx.add_view(|_| item)),
1785                false,
1786                false,
1787                None,
1788                cx,
1789            );
1790        });
1791        assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
1792
1793        // new multibuffer view with the same project entry
1794        workspace.update(cx, |workspace, cx| {
1795            let item = TestItem::new()
1796                .with_singleton(false)
1797                .with_label("multibuffer 1")
1798                .with_project_entry_ids(&[1]);
1799
1800            Pane::add_item(
1801                workspace,
1802                &pane,
1803                Box::new(cx.add_view(|_| item)),
1804                false,
1805                false,
1806                None,
1807                cx,
1808            );
1809        });
1810        assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
1811
1812        // another multibuffer view with the same project entry
1813        workspace.update(cx, |workspace, cx| {
1814            let item = TestItem::new()
1815                .with_singleton(false)
1816                .with_label("multibuffer 1b")
1817                .with_project_entry_ids(&[1]);
1818
1819            Pane::add_item(
1820                workspace,
1821                &pane,
1822                Box::new(cx.add_view(|_| item)),
1823                false,
1824                false,
1825                None,
1826                cx,
1827            );
1828        });
1829        assert_item_labels(
1830            &pane,
1831            ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
1832            cx,
1833        );
1834    }
1835
1836    fn set_labeled_items<const COUNT: usize>(
1837        workspace: &ViewHandle<Workspace>,
1838        pane: &ViewHandle<Pane>,
1839        labels: [&str; COUNT],
1840        cx: &mut TestAppContext,
1841    ) -> [Box<ViewHandle<TestItem>>; COUNT] {
1842        pane.update(cx, |pane, _| {
1843            pane.items.clear();
1844        });
1845
1846        workspace.update(cx, |workspace, cx| {
1847            let mut active_item_index = 0;
1848
1849            let mut index = 0;
1850            let items = labels.map(|mut label| {
1851                if label.ends_with("*") {
1852                    label = label.trim_end_matches("*");
1853                    active_item_index = index;
1854                }
1855
1856                let labeled_item = Box::new(cx.add_view(|_| TestItem::new().with_label(label)));
1857                Pane::add_item(
1858                    workspace,
1859                    pane,
1860                    labeled_item.clone(),
1861                    false,
1862                    false,
1863                    None,
1864                    cx,
1865                );
1866                index += 1;
1867                labeled_item
1868            });
1869
1870            pane.update(cx, |pane, cx| {
1871                pane.activate_item(active_item_index, false, false, cx)
1872            });
1873
1874            items
1875        })
1876    }
1877
1878    // Assert the item label, with the active item label suffixed with a '*'
1879    fn assert_item_labels<const COUNT: usize>(
1880        pane: &ViewHandle<Pane>,
1881        expected_states: [&str; COUNT],
1882        cx: &mut TestAppContext,
1883    ) {
1884        pane.read_with(cx, |pane, cx| {
1885            let actual_states = pane
1886                .items
1887                .iter()
1888                .enumerate()
1889                .map(|(ix, item)| {
1890                    let mut state = item
1891                        .to_any()
1892                        .downcast::<TestItem>()
1893                        .unwrap()
1894                        .read(cx)
1895                        .label
1896                        .clone();
1897                    if ix == pane.active_item_index {
1898                        state.push('*');
1899                    }
1900                    state
1901                })
1902                .collect::<Vec<_>>();
1903
1904            assert_eq!(
1905                actual_states, expected_states,
1906                "pane items do not match expectation"
1907            );
1908        })
1909    }
1910}