pane.rs

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