Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T04:59:45.029Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

pane.rs

9504 lines · 356.9 KB · rust
1use crate::{
2    CloseWindow, NewCenterTerminal, NewFile, NewTerminal, OpenInTerminal, OpenOptions,
3    OpenTerminal, OpenVisible, SplitDirection, ToggleFileFinder, ToggleProjectSymbols, ToggleZoom,
4    Workspace, WorkspaceItemBuilder, ZoomIn, ZoomOut,
5    focus_follows_mouse::FocusFollowsMouse as _,
6    invalid_item_view::InvalidItemView,
7    item::{
8        ActivateOnClose, ClosePosition, Item, ItemBufferKind, ItemHandle, ItemSettings,
9        PreviewTabsSettings, ProjectItemKind, SaveOptions, ShowCloseButton, ShowDiagnostics,
10        TabContentParams, TabTooltipContent, WeakItemHandle,
11    },
12    move_item,
13    notifications::NotifyResultExt,
14    toolbar::Toolbar,
15    workspace_settings::{AutosaveSetting, FocusFollowsMouse, TabBarSettings, WorkspaceSettings},
16};
17use anyhow::Result;
18use collections::{BTreeSet, HashMap, HashSet, VecDeque};
19use futures::{StreamExt, stream::FuturesUnordered};
20use gpui::{
21    Action, Anchor, AnyElement, App, AsyncWindowContext, ClickEvent, ClipboardItem, Context, Div,
22    DragMoveEvent, Entity, EntityId, EventEmitter, ExternalPaths, FocusHandle, FocusOutEvent,
23    Focusable, KeyContext, MouseButton, NavigationDirection, Pixels, Point, PromptLevel, Render,
24    ScrollHandle, Subscription, Task, TaskExt, WeakEntity, WeakFocusHandle, Window, actions,
25    anchored, deferred, prelude::*,
26};
27use itertools::Itertools;
28use language::{Capability, DiagnosticSeverity};
29use parking_lot::Mutex;
30use project::{DirectoryLister, Project, ProjectEntryId, ProjectPath, WorktreeId};
31use schemars::JsonSchema;
32use serde::Deserialize;
33use settings::{Settings, SettingsStore};
34use std::{
35    any::Any,
36    cmp, fmt, mem,
37    num::NonZeroUsize,
38    path::PathBuf,
39    rc::Rc,
40    sync::{
41        Arc,
42        atomic::{AtomicUsize, Ordering},
43    },
44    time::Duration,
45};
46use theme_settings::ThemeSettings;
47use ui::{
48    ContextMenu, ContextMenuEntry, ContextMenuItem, DecoratedIcon, IconButtonShape, IconDecoration,
49    IconDecorationKind, Indicator, PopoverMenu, PopoverMenuHandle, Tab, TabBar, TabPosition,
50    Tooltip, prelude::*, right_click_menu,
51};
52use util::{
53    ResultExt, debug_panic, markdown::MarkdownInlineCode, maybe, paths::PathStyle,
54    serde::default_true, truncate_and_remove_front,
55};
56
57/// A selected entry in e.g. project panel.
58#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
59pub struct SelectedEntry {
60    pub worktree_id: WorktreeId,
61    pub entry_id: ProjectEntryId,
62}
63
64/// A group of selected entries from project panel.
65#[derive(Debug)]
66pub struct DraggedSelection {
67    pub active_selection: SelectedEntry,
68    pub marked_selections: Arc<[SelectedEntry]>,
69}
70
71impl DraggedSelection {
72    pub fn items<'a>(&'a self) -> Box<dyn Iterator<Item = &'a SelectedEntry> + 'a> {
73        if self.marked_selections.contains(&self.active_selection) {
74            Box::new(self.marked_selections.iter())
75        } else {
76            Box::new(std::iter::once(&self.active_selection))
77        }
78    }
79}
80
81#[derive(Clone, Copy, PartialEq, Debug, Deserialize, JsonSchema)]
82#[serde(rename_all = "snake_case")]
83pub enum SaveIntent {
84    /// write all files (even if unchanged)
85    /// prompt before overwriting on-disk changes
86    Save,
87    /// same as Save, but always formats regardless of the format_on_save setting
88    FormatAndSave,
89    /// same as Save, but without auto formatting
90    SaveWithoutFormat,
91    /// write any files that have local changes
92    /// prompt before overwriting on-disk changes
93    SaveAll,
94    /// always prompt for a new path
95    SaveAs,
96    /// prompt "you have unsaved changes" before writing
97    Close,
98    /// write all dirty files, don't prompt on conflict
99    Overwrite,
100    /// skip all save-related behavior
101    Skip,
102}
103
104/// Activates a specific item in the pane by its index.
105#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default, Action)]
106#[action(namespace = pane)]
107pub struct ActivateItem(pub usize);
108
109/// Closes the currently active item in the pane.
110#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default, Action)]
111#[action(namespace = pane)]
112#[serde(deny_unknown_fields)]
113pub struct CloseActiveItem {
114    #[serde(default)]
115    pub save_intent: Option<SaveIntent>,
116    #[serde(default)]
117    pub close_pinned: bool,
118}
119
120/// Closes all inactive items in the pane.
121#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default, Action)]
122#[action(namespace = pane)]
123#[serde(deny_unknown_fields)]
124#[action(deprecated_aliases = ["pane::CloseInactiveItems"])]
125pub struct CloseOtherItems {
126    #[serde(default)]
127    pub save_intent: Option<SaveIntent>,
128    #[serde(default)]
129    pub close_pinned: bool,
130}
131
132/// Closes all multibuffers in the pane.
133#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default, Action)]
134#[action(namespace = pane)]
135#[serde(deny_unknown_fields)]
136pub struct CloseMultibufferItems {
137    #[serde(default)]
138    pub save_intent: Option<SaveIntent>,
139    #[serde(default)]
140    pub close_pinned: bool,
141}
142
143/// Closes all items in the pane.
144#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default, Action)]
145#[action(namespace = pane)]
146#[serde(deny_unknown_fields)]
147pub struct CloseAllItems {
148    #[serde(default)]
149    pub save_intent: Option<SaveIntent>,
150    #[serde(default)]
151    pub close_pinned: bool,
152}
153
154/// Closes all items that have no unsaved changes.
155#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default, Action)]
156#[action(namespace = pane)]
157#[serde(deny_unknown_fields)]
158pub struct CloseCleanItems {
159    #[serde(default)]
160    pub close_pinned: bool,
161}
162
163/// Closes all items to the right of the current item.
164#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default, Action)]
165#[action(namespace = pane)]
166#[serde(deny_unknown_fields)]
167pub struct CloseItemsToTheRight {
168    #[serde(default)]
169    pub close_pinned: bool,
170}
171
172/// Closes all items to the left of the current item.
173#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default, Action)]
174#[action(namespace = pane)]
175#[serde(deny_unknown_fields)]
176pub struct CloseItemsToTheLeft {
177    #[serde(default)]
178    pub close_pinned: bool,
179}
180
181/// Reveals the current item in the project panel.
182#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default, Action)]
183#[action(namespace = pane)]
184#[serde(deny_unknown_fields)]
185pub struct RevealInProjectPanel {
186    #[serde(skip)]
187    pub entry_id: Option<u64>,
188}
189
190/// Opens the search interface with the specified configuration.
191#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default, Action)]
192#[action(namespace = pane)]
193#[serde(deny_unknown_fields)]
194pub struct DeploySearch {
195    #[serde(default)]
196    pub replace_enabled: bool,
197    #[serde(default)]
198    pub included_files: Option<String>,
199    #[serde(default)]
200    pub excluded_files: Option<String>,
201    #[serde(default)]
202    pub query: Option<String>,
203    #[serde(default)]
204    pub regex: Option<bool>,
205    #[serde(default)]
206    pub case_sensitive: Option<bool>,
207    #[serde(default)]
208    pub whole_word: Option<bool>,
209    #[serde(default)]
210    pub include_ignored: Option<bool>,
211}
212
213#[derive(Clone, Copy, PartialEq, Debug, Deserialize, JsonSchema, Default)]
214#[serde(deny_unknown_fields)]
215pub enum SplitMode {
216    /// Clone the current pane.
217    #[default]
218    ClonePane,
219    /// Create an empty new pane.
220    EmptyPane,
221    /// Move the item into a new pane. This will map to nop if only one pane exists.
222    MovePane,
223}
224
225macro_rules! split_structs {
226    ($($name:ident => $doc:literal),* $(,)?) => {
227        $(
228            #[doc = $doc]
229            #[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default, Action)]
230            #[action(namespace = pane)]
231            #[serde(deny_unknown_fields, default)]
232            pub struct $name {
233                pub mode: SplitMode,
234            }
235        )*
236    };
237}
238
239split_structs!(
240    SplitLeft => "Splits the pane to the left.",
241    SplitRight => "Splits the pane to the right.",
242    SplitUp => "Splits the pane upward.",
243    SplitDown => "Splits the pane downward.",
244    SplitHorizontal => "Splits the pane horizontally.",
245    SplitVertical => "Splits the pane vertically."
246);
247
248/// Activates the previous item in the pane.
249#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Action)]
250#[action(namespace = pane)]
251#[serde(deny_unknown_fields, default)]
252pub struct ActivatePreviousItem {
253    /// Whether to wrap from the first item to the last item.
254    #[serde(default = "default_true")]
255    pub wrap_around: bool,
256}
257
258impl Default for ActivatePreviousItem {
259    fn default() -> Self {
260        Self { wrap_around: true }
261    }
262}
263
264/// Activates the next item in the pane.
265#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Action)]
266#[action(namespace = pane)]
267#[serde(deny_unknown_fields, default)]
268pub struct ActivateNextItem {
269    /// Whether to wrap from the last item to the first item.
270    #[serde(default = "default_true")]
271    pub wrap_around: bool,
272}
273
274impl Default for ActivateNextItem {
275    fn default() -> Self {
276        Self { wrap_around: true }
277    }
278}
279
280actions!(
281    pane,
282    [
283        /// Activates the last item in the pane.
284        ActivateLastItem,
285        /// Switches to the alternate file.
286        AlternateFile,
287        /// Navigates back in history.
288        GoBack,
289        /// Navigates forward in history.
290        GoForward,
291        /// Navigates back in the tag stack.
292        GoToOlderTag,
293        /// Navigates forward in the tag stack.
294        GoToNewerTag,
295        /// Joins this pane into the next pane.
296        JoinIntoNext,
297        /// Joins all panes into one.
298        JoinAll,
299        /// Reopens the most recently closed item.
300        ReopenClosedItem,
301        /// Splits the pane to the left, moving the current item.
302        SplitAndMoveLeft,
303        /// Splits the pane upward, moving the current item.
304        SplitAndMoveUp,
305        /// Splits the pane to the right, moving the current item.
306        SplitAndMoveRight,
307        /// Splits the pane downward, moving the current item.
308        SplitAndMoveDown,
309        /// Swaps the current item with the one to the left.
310        SwapItemLeft,
311        /// Swaps the current item with the one to the right.
312        SwapItemRight,
313        /// Toggles preview mode for the current tab.
314        TogglePreviewTab,
315        /// Toggles pin status for the current tab.
316        TogglePinTab,
317        /// Unpins all tabs in the pane.
318        UnpinAllTabs,
319    ]
320);
321
322const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
323
324pub enum Event {
325    AddItem {
326        item: Box<dyn ItemHandle>,
327    },
328    ActivateItem {
329        local: bool,
330        focus_changed: bool,
331    },
332    Remove {
333        focus_on_pane: Option<Entity<Pane>>,
334    },
335    RemovedItem {
336        item: Box<dyn ItemHandle>,
337    },
338    Split {
339        direction: SplitDirection,
340        mode: SplitMode,
341    },
342    ItemPinned,
343    ItemUnpinned,
344    JoinAll,
345    JoinIntoNext,
346    ChangeItemTitle,
347    Focus,
348    ZoomIn,
349    ZoomOut,
350    UserSavedItem {
351        item: Box<dyn WeakItemHandle>,
352        save_intent: SaveIntent,
353    },
354}
355
356impl fmt::Debug for Event {
357    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
358        match self {
359            Event::AddItem { item } => f
360                .debug_struct("AddItem")
361                .field("item", &item.item_id())
362                .finish(),
363            Event::ActivateItem { local, .. } => f
364                .debug_struct("ActivateItem")
365                .field("local", local)
366                .finish(),
367            Event::Remove { .. } => f.write_str("Remove"),
368            Event::RemovedItem { item } => f
369                .debug_struct("RemovedItem")
370                .field("item", &item.item_id())
371                .finish(),
372            Event::Split { direction, mode } => f
373                .debug_struct("Split")
374                .field("direction", direction)
375                .field("mode", mode)
376                .finish(),
377            Event::JoinAll => f.write_str("JoinAll"),
378            Event::JoinIntoNext => f.write_str("JoinIntoNext"),
379            Event::ChangeItemTitle => f.write_str("ChangeItemTitle"),
380            Event::Focus => f.write_str("Focus"),
381            Event::ZoomIn => f.write_str("ZoomIn"),
382            Event::ZoomOut => f.write_str("ZoomOut"),
383            Event::UserSavedItem { item, save_intent } => f
384                .debug_struct("UserSavedItem")
385                .field("item", &item.id())
386                .field("save_intent", save_intent)
387                .finish(),
388            Event::ItemPinned => f.write_str("ItemPinned"),
389            Event::ItemUnpinned => f.write_str("ItemUnpinned"),
390        }
391    }
392}
393
394/// A container for 0 to many items that are open in the workspace.
395/// Treats all items uniformly via the [`ItemHandle`] trait, whether it's an editor, search results multibuffer, terminal or something else,
396/// responsible for managing item tabs, focus and zoom states and drag and drop features.
397/// Can be split, see `PaneGroup` for more details.
398pub struct Pane {
399    alternate_file_items: (
400        Option<Box<dyn WeakItemHandle>>,
401        Option<Box<dyn WeakItemHandle>>,
402    ),
403    focus_handle: FocusHandle,
404    items: Vec<Box<dyn ItemHandle>>,
405    activation_history: Vec<ActivationHistoryEntry>,
406    next_activation_timestamp: Arc<AtomicUsize>,
407    zoomed: bool,
408    was_focused: bool,
409    active_item_index: usize,
410    preview_item_id: Option<EntityId>,
411    last_focus_handle_by_item: HashMap<EntityId, WeakFocusHandle>,
412    nav_history: NavHistory,
413    toolbar: Entity<Toolbar>,
414    pub(crate) workspace: WeakEntity<Workspace>,
415    project: WeakEntity<Project>,
416    pub drag_split_direction: Option<SplitDirection>,
417    can_drop_predicate: Option<Arc<dyn Fn(&dyn Any, &mut Window, &mut App) -> bool>>,
418    can_split_predicate:
419        Option<Arc<dyn Fn(&mut Self, &dyn Any, &mut Window, &mut Context<Self>) -> bool>>,
420    can_toggle_zoom: bool,
421    should_display_tab_bar: Rc<dyn Fn(&Window, &mut Context<Pane>) -> bool>,
422    should_display_welcome_page: bool,
423    render_tab_bar_buttons: Rc<
424        dyn Fn(
425            &mut Pane,
426            &mut Window,
427            &mut Context<Pane>,
428        ) -> (Option<AnyElement>, Option<AnyElement>),
429    >,
430    render_tab_bar: Rc<dyn Fn(&mut Pane, &mut Window, &mut Context<Pane>) -> AnyElement>,
431    show_tab_bar_buttons: bool,
432    max_tabs: Option<NonZeroUsize>,
433    use_max_tabs: bool,
434    _subscriptions: Vec<Subscription>,
435    tab_bar_scroll_handle: ScrollHandle,
436    /// This is set to true if a user scroll has occurred more recently than a system scroll
437    /// We want to suppress certain system scrolls when the user has intentionally scrolled
438    suppress_scroll: bool,
439    /// Is None if navigation buttons are permanently turned off (and should not react to setting changes).
440    /// Otherwise, when `display_nav_history_buttons` is Some, it determines whether nav buttons should be displayed.
441    display_nav_history_buttons: Option<bool>,
442    double_click_dispatch_action: Box<dyn Action>,
443    save_modals_spawned: HashSet<EntityId>,
444    close_pane_if_empty: bool,
445    pub new_item_context_menu_handle: PopoverMenuHandle<ContextMenu>,
446    pub split_item_context_menu_handle: PopoverMenuHandle<ContextMenu>,
447    pinned_tab_count: usize,
448    diagnostics: HashMap<ProjectPath, DiagnosticSeverity>,
449    zoom_out_on_close: bool,
450    focus_follows_mouse: FocusFollowsMouse,
451    diagnostic_summary_update: Task<()>,
452    /// If a certain project item wants to get recreated with specific data, it can persist its data before the recreation here.
453    pub project_item_restoration_data: HashMap<ProjectItemKind, Box<dyn Any + Send>>,
454    welcome_page: Option<Entity<crate::welcome::WelcomePage>>,
455
456    pub in_center_group: bool,
457}
458
459pub struct ActivationHistoryEntry {
460    pub entity_id: EntityId,
461    pub timestamp: usize,
462}
463
464#[derive(Clone)]
465pub struct ItemNavHistory {
466    history: NavHistory,
467    item: Arc<dyn WeakItemHandle>,
468}
469
470#[derive(Clone)]
471pub struct NavHistory(Arc<Mutex<NavHistoryState>>);
472
473#[derive(Clone)]
474struct NavHistoryState {
475    mode: NavigationMode,
476    backward_stack: VecDeque<NavigationEntry>,
477    forward_stack: VecDeque<NavigationEntry>,
478    closed_stack: VecDeque<NavigationEntry>,
479    tag_stack: VecDeque<TagStackEntry>,
480    tag_stack_pos: usize,
481    paths_by_item: HashMap<EntityId, (ProjectPath, Option<PathBuf>)>,
482    pane: WeakEntity<Pane>,
483    next_timestamp: Arc<AtomicUsize>,
484    preview_item_id: Option<EntityId>,
485}
486
487#[derive(Debug, Default, Copy, Clone)]
488pub enum NavigationMode {
489    #[default]
490    Normal,
491    GoingBack,
492    GoingForward,
493    ClosingItem,
494    ReopeningClosedItem,
495    Disabled,
496}
497
498#[derive(Debug, Default, Copy, Clone)]
499pub enum TagNavigationMode {
500    #[default]
501    Older,
502    Newer,
503}
504
505#[derive(Clone)]
506pub struct NavigationEntry {
507    pub item: Arc<dyn WeakItemHandle + Send + Sync>,
508    pub data: Option<Arc<dyn Any + Send + Sync>>,
509    pub timestamp: usize,
510    pub is_preview: bool,
511    /// Row position for Neovim-style deduplication. When set, entries with the
512    /// same item and row are considered duplicates and deduplicated.
513    pub row: Option<u32>,
514}
515
516#[derive(Clone)]
517pub struct TagStackEntry {
518    pub origin: NavigationEntry,
519    pub target: NavigationEntry,
520}
521
522#[derive(Clone)]
523pub struct DraggedTab {
524    pub pane: Entity<Pane>,
525    pub item: Box<dyn ItemHandle>,
526    pub ix: usize,
527    pub detail: usize,
528    pub is_active: bool,
529}
530
531impl EventEmitter<Event> for Pane {}
532
533pub enum Side {
534    Left,
535    Right,
536}
537
538#[derive(Copy, Clone)]
539enum PinOperation {
540    Pin,
541    Unpin,
542}
543
544impl Pane {
545    pub fn new(
546        workspace: WeakEntity<Workspace>,
547        project: Entity<Project>,
548        next_timestamp: Arc<AtomicUsize>,
549        can_drop_predicate: Option<Arc<dyn Fn(&dyn Any, &mut Window, &mut App) -> bool + 'static>>,
550        double_click_dispatch_action: Box<dyn Action>,
551        use_max_tabs: bool,
552        window: &mut Window,
553        cx: &mut Context<Self>,
554    ) -> Self {
555        let focus_handle = cx.focus_handle();
556        let max_tabs = if use_max_tabs {
557            WorkspaceSettings::get_global(cx).max_tabs
558        } else {
559            None
560        };
561
562        let subscriptions = vec![
563            cx.on_focus(&focus_handle, window, Pane::focus_in),
564            cx.on_focus_in(&focus_handle, window, Pane::focus_in),
565            cx.on_focus_out(&focus_handle, window, Pane::focus_out),
566            cx.observe_global_in::<SettingsStore>(window, Self::settings_changed),
567            cx.subscribe(&project, Self::project_events),
568        ];
569
570        let handle = cx.entity().downgrade();
571
572        Self {
573            alternate_file_items: (None, None),
574            focus_handle,
575            items: Vec::new(),
576            activation_history: Vec::new(),
577            next_activation_timestamp: next_timestamp.clone(),
578            was_focused: false,
579            zoomed: false,
580            active_item_index: 0,
581            preview_item_id: None,
582            max_tabs,
583            use_max_tabs,
584            last_focus_handle_by_item: Default::default(),
585            nav_history: NavHistory(Arc::new(Mutex::new(NavHistoryState {
586                mode: NavigationMode::Normal,
587                backward_stack: Default::default(),
588                forward_stack: Default::default(),
589                closed_stack: Default::default(),
590                tag_stack: Default::default(),
591                tag_stack_pos: Default::default(),
592                paths_by_item: Default::default(),
593                pane: handle,
594                next_timestamp,
595                preview_item_id: None,
596            }))),
597            toolbar: cx.new(|_| Toolbar::new()),
598            tab_bar_scroll_handle: ScrollHandle::new(),
599            suppress_scroll: false,
600            drag_split_direction: None,
601            workspace,
602            project: project.downgrade(),
603            can_drop_predicate,
604            can_split_predicate: None,
605            can_toggle_zoom: true,
606            should_display_tab_bar: Rc::new(|_, cx| TabBarSettings::get_global(cx).show),
607            should_display_welcome_page: false,
608            render_tab_bar_buttons: Rc::new(default_render_tab_bar_buttons),
609            render_tab_bar: Rc::new(Self::render_tab_bar),
610            show_tab_bar_buttons: TabBarSettings::get_global(cx).show_tab_bar_buttons,
611            display_nav_history_buttons: Some(
612                TabBarSettings::get_global(cx).show_nav_history_buttons,
613            ),
614            _subscriptions: subscriptions,
615            double_click_dispatch_action,
616            save_modals_spawned: HashSet::default(),
617            close_pane_if_empty: true,
618            split_item_context_menu_handle: Default::default(),
619            new_item_context_menu_handle: Default::default(),
620            pinned_tab_count: 0,
621            diagnostics: Default::default(),
622            zoom_out_on_close: true,
623            focus_follows_mouse: WorkspaceSettings::get_global(cx).focus_follows_mouse,
624            diagnostic_summary_update: Task::ready(()),
625            project_item_restoration_data: HashMap::default(),
626            welcome_page: None,
627            in_center_group: false,
628        }
629    }
630
631    fn alternate_file(&mut self, _: &AlternateFile, window: &mut Window, cx: &mut Context<Pane>) {
632        let (_, alternative) = &self.alternate_file_items;
633        if let Some(alternative) = alternative {
634            let existing = self
635                .items()
636                .find_position(|item| item.item_id() == alternative.id());
637            if let Some((ix, _)) = existing {
638                self.activate_item(ix, true, true, window, cx);
639            } else if let Some(upgraded) = alternative.upgrade() {
640                self.add_item(upgraded, true, true, None, window, cx);
641            }
642        }
643    }
644
645    pub fn track_alternate_file_items(&mut self) {
646        if let Some(item) = self.active_item().map(|item| item.downgrade_item()) {
647            let (current, _) = &self.alternate_file_items;
648            match current {
649                Some(current) => {
650                    if current.id() != item.id() {
651                        self.alternate_file_items =
652                            (Some(item), self.alternate_file_items.0.take());
653                    }
654                }
655                None => {
656                    self.alternate_file_items = (Some(item), None);
657                }
658            }
659        }
660    }
661
662    pub fn has_focus(&self, window: &Window, cx: &App) -> bool {
663        // We not only check whether our focus handle contains focus, but also
664        // whether the active item might have focus, because we might have just activated an item
665        // that hasn't rendered yet.
666        // Before the next render, we might transfer focus
667        // to the item, and `focus_handle.contains_focus` returns false because the `active_item`
668        // is not hooked up to us in the dispatch tree.
669        self.focus_handle.contains_focused(window, cx)
670            || self
671                .active_item()
672                .is_some_and(|item| item.item_focus_handle(cx).contains_focused(window, cx))
673    }
674
675    fn focus_in(&mut self, window: &mut Window, cx: &mut Context<Self>) {
676        if !self.was_focused {
677            self.was_focused = true;
678            self.update_history(self.active_item_index);
679            if !self.suppress_scroll && self.items.get(self.active_item_index).is_some() {
680                self.update_active_tab(self.active_item_index);
681            }
682            cx.emit(Event::Focus);
683            cx.notify();
684        }
685
686        self.toolbar.update(cx, |toolbar, cx| {
687            toolbar.focus_changed(true, window, cx);
688        });
689
690        if let Some(active_item) = self.active_item() {
691            if self.focus_handle.is_focused(window) {
692                // Schedule a redraw next frame, so that the focus changes below take effect
693                cx.on_next_frame(window, |_, _, cx| {
694                    cx.notify();
695                });
696
697                // Pane was focused directly. We need to either focus a view inside the active item,
698                // or focus the active item itself
699                if let Some(weak_last_focus_handle) =
700                    self.last_focus_handle_by_item.get(&active_item.item_id())
701                    && let Some(focus_handle) = weak_last_focus_handle.upgrade()
702                {
703                    focus_handle.focus(window, cx);
704                    return;
705                }
706
707                active_item.item_focus_handle(cx).focus(window, cx);
708            } else if let Some(focused) = window.focused(cx)
709                && !self.context_menu_focused(window, cx)
710            {
711                self.last_focus_handle_by_item
712                    .insert(active_item.item_id(), focused.downgrade());
713            }
714        } else if self.should_display_welcome_page
715            && let Some(welcome_page) = self.welcome_page.as_ref()
716        {
717            if self.focus_handle.is_focused(window) {
718                welcome_page.read(cx).focus_handle(cx).focus(window, cx);
719            }
720        }
721    }
722
723    pub fn context_menu_focused(&self, window: &mut Window, cx: &mut Context<Self>) -> bool {
724        self.new_item_context_menu_handle.is_focused(window, cx)
725            || self.split_item_context_menu_handle.is_focused(window, cx)
726    }
727
728    fn focus_out(&mut self, _event: FocusOutEvent, window: &mut Window, cx: &mut Context<Self>) {
729        self.was_focused = false;
730        self.toolbar.update(cx, |toolbar, cx| {
731            toolbar.focus_changed(false, window, cx);
732        });
733
734        cx.notify();
735    }
736
737    fn project_events(
738        &mut self,
739        _project: Entity<Project>,
740        event: &project::Event,
741        cx: &mut Context<Self>,
742    ) {
743        match event {
744            project::Event::DiskBasedDiagnosticsFinished { .. }
745            | project::Event::DiagnosticsUpdated { .. } => {
746                if ItemSettings::get_global(cx).show_diagnostics != ShowDiagnostics::Off {
747                    self.diagnostic_summary_update = cx.spawn(async move |this, cx| {
748                        cx.background_executor()
749                            .timer(Duration::from_millis(30))
750                            .await;
751                        this.update(cx, |this, cx| {
752                            this.update_diagnostics(cx);
753                            cx.notify();
754                        })
755                        .log_err();
756                    });
757                }
758            }
759            _ => {}
760        }
761    }
762
763    fn update_diagnostics(&mut self, cx: &mut Context<Self>) {
764        let Some(project) = self.project.upgrade() else {
765            return;
766        };
767        let show_diagnostics = ItemSettings::get_global(cx).show_diagnostics;
768        self.diagnostics = if show_diagnostics != ShowDiagnostics::Off {
769            project
770                .read(cx)
771                .diagnostic_summaries(false, cx)
772                .filter_map(|(project_path, _, diagnostic_summary)| {
773                    if diagnostic_summary.error_count > 0 {
774                        Some((project_path, DiagnosticSeverity::ERROR))
775                    } else if diagnostic_summary.warning_count > 0
776                        && show_diagnostics != ShowDiagnostics::Errors
777                    {
778                        Some((project_path, DiagnosticSeverity::WARNING))
779                    } else {
780                        None
781                    }
782                })
783                .collect()
784        } else {
785            HashMap::default()
786        }
787    }
788
789    fn settings_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
790        let tab_bar_settings = TabBarSettings::get_global(cx);
791
792        if let Some(display_nav_history_buttons) = self.display_nav_history_buttons.as_mut() {
793            *display_nav_history_buttons = tab_bar_settings.show_nav_history_buttons;
794        }
795
796        self.show_tab_bar_buttons = tab_bar_settings.show_tab_bar_buttons;
797
798        if !PreviewTabsSettings::get_global(cx).enabled {
799            self.preview_item_id = None;
800            self.nav_history.0.lock().preview_item_id = None;
801        }
802
803        let workspace_settings = WorkspaceSettings::get_global(cx);
804
805        self.focus_follows_mouse = workspace_settings.focus_follows_mouse;
806
807        let new_max_tabs = workspace_settings.max_tabs;
808
809        if self.use_max_tabs && new_max_tabs != self.max_tabs {
810            self.max_tabs = new_max_tabs;
811            self.close_items_on_settings_change(window, cx);
812        }
813
814        self.update_diagnostics(cx);
815        cx.notify();
816    }
817
818    pub fn active_item_index(&self) -> usize {
819        self.active_item_index
820    }
821
822    pub fn is_active_item_pinned(&self) -> bool {
823        self.is_tab_pinned(self.active_item_index)
824    }
825
826    pub fn activation_history(&self) -> &[ActivationHistoryEntry] {
827        &self.activation_history
828    }
829
830    pub fn set_should_display_tab_bar<F>(&mut self, should_display_tab_bar: F)
831    where
832        F: 'static + Fn(&Window, &mut Context<Pane>) -> bool,
833    {
834        self.should_display_tab_bar = Rc::new(should_display_tab_bar);
835    }
836
837    pub fn set_should_display_welcome_page(&mut self, should_display_welcome_page: bool) {
838        self.should_display_welcome_page = should_display_welcome_page;
839    }
840
841    pub fn set_can_split(
842        &mut self,
843        can_split_predicate: Option<
844            Arc<dyn Fn(&mut Self, &dyn Any, &mut Window, &mut Context<Self>) -> bool + 'static>,
845        >,
846    ) {
847        self.can_split_predicate = can_split_predicate;
848    }
849
850    pub fn set_can_toggle_zoom(&mut self, can_toggle_zoom: bool, cx: &mut Context<Self>) {
851        self.can_toggle_zoom = can_toggle_zoom;
852        cx.notify();
853    }
854
855    pub fn set_close_pane_if_empty(&mut self, close_pane_if_empty: bool, cx: &mut Context<Self>) {
856        self.close_pane_if_empty = close_pane_if_empty;
857        cx.notify();
858    }
859
860    pub fn set_can_navigate(&mut self, can_navigate: bool, cx: &mut Context<Self>) {
861        self.toolbar.update(cx, |toolbar, cx| {
862            toolbar.set_can_navigate(can_navigate, cx);
863        });
864        cx.notify();
865    }
866
867    pub fn set_render_tab_bar<F>(&mut self, cx: &mut Context<Self>, render: F)
868    where
869        F: 'static + Fn(&mut Pane, &mut Window, &mut Context<Pane>) -> AnyElement,
870    {
871        self.render_tab_bar = Rc::new(render);
872        cx.notify();
873    }
874
875    pub fn set_render_tab_bar_buttons<F>(&mut self, cx: &mut Context<Self>, render: F)
876    where
877        F: 'static
878            + Fn(
879                &mut Pane,
880                &mut Window,
881                &mut Context<Pane>,
882            ) -> (Option<AnyElement>, Option<AnyElement>),
883    {
884        self.render_tab_bar_buttons = Rc::new(render);
885        cx.notify();
886    }
887
888    pub fn nav_history_for_item<T: Item>(&self, item: &Entity<T>) -> ItemNavHistory {
889        ItemNavHistory {
890            history: self.nav_history.clone(),
891            item: Arc::new(item.downgrade()),
892        }
893    }
894
895    pub fn nav_history(&self) -> &NavHistory {
896        &self.nav_history
897    }
898
899    pub fn nav_history_mut(&mut self) -> &mut NavHistory {
900        &mut self.nav_history
901    }
902
903    pub fn fork_nav_history(&self) -> NavHistory {
904        let history = self.nav_history.0.lock().clone();
905        NavHistory(Arc::new(Mutex::new(history)))
906    }
907
908    pub fn set_nav_history(&mut self, history: NavHistory, cx: &Context<Self>) {
909        self.nav_history = history;
910        self.nav_history().0.lock().pane = cx.entity().downgrade();
911    }
912
913    pub fn disable_history(&mut self) {
914        self.nav_history.disable();
915    }
916
917    pub fn enable_history(&mut self) {
918        self.nav_history.enable();
919    }
920
921    pub fn can_navigate_backward(&self) -> bool {
922        !self.nav_history.0.lock().backward_stack.is_empty()
923    }
924
925    pub fn can_navigate_forward(&self) -> bool {
926        !self.nav_history.0.lock().forward_stack.is_empty()
927    }
928
929    pub fn navigate_backward(&mut self, _: &GoBack, window: &mut Window, cx: &mut Context<Self>) {
930        if let Some(workspace) = self.workspace.upgrade() {
931            let pane = cx.entity().downgrade();
932            window.defer(cx, move |window, cx| {
933                workspace.update(cx, |workspace, cx| {
934                    workspace.go_back(pane, window, cx).detach_and_log_err(cx)
935                })
936            })
937        }
938    }
939
940    fn navigate_forward(&mut self, _: &GoForward, window: &mut Window, cx: &mut Context<Self>) {
941        if let Some(workspace) = self.workspace.upgrade() {
942            let pane = cx.entity().downgrade();
943            window.defer(cx, move |window, cx| {
944                workspace.update(cx, |workspace, cx| {
945                    workspace
946                        .go_forward(pane, window, cx)
947                        .detach_and_log_err(cx)
948                })
949            })
950        }
951    }
952
953    pub fn go_to_older_tag(
954        &mut self,
955        _: &GoToOlderTag,
956        window: &mut Window,
957        cx: &mut Context<Self>,
958    ) {
959        if let Some(workspace) = self.workspace.upgrade() {
960            let pane = cx.entity().downgrade();
961            window.defer(cx, move |window, cx| {
962                workspace.update(cx, |workspace, cx| {
963                    workspace
964                        .navigate_tag_history(pane, TagNavigationMode::Older, window, cx)
965                        .detach_and_log_err(cx)
966                })
967            })
968        }
969    }
970
971    pub fn go_to_newer_tag(
972        &mut self,
973        _: &GoToNewerTag,
974        window: &mut Window,
975        cx: &mut Context<Self>,
976    ) {
977        if let Some(workspace) = self.workspace.upgrade() {
978            let pane = cx.entity().downgrade();
979            window.defer(cx, move |window, cx| {
980                workspace.update(cx, |workspace, cx| {
981                    workspace
982                        .navigate_tag_history(pane, TagNavigationMode::Newer, window, cx)
983                        .detach_and_log_err(cx)
984                })
985            })
986        }
987    }
988
989    fn history_updated(&mut self, cx: &mut Context<Self>) {
990        self.toolbar.update(cx, |_, cx| cx.notify());
991    }
992
993    pub fn preview_item_id(&self) -> Option<EntityId> {
994        self.preview_item_id
995    }
996
997    pub fn preview_item(&self) -> Option<Box<dyn ItemHandle>> {
998        self.preview_item_id
999            .and_then(|id| self.items.iter().find(|item| item.item_id() == id))
1000            .cloned()
1001    }
1002
1003    pub fn preview_item_idx(&self) -> Option<usize> {
1004        if let Some(preview_item_id) = self.preview_item_id {
1005            self.items
1006                .iter()
1007                .position(|item| item.item_id() == preview_item_id)
1008        } else {
1009            None
1010        }
1011    }
1012
1013    pub fn is_active_preview_item(&self, item_id: EntityId) -> bool {
1014        self.preview_item_id == Some(item_id)
1015    }
1016
1017    /// Promotes the item with the given ID to not be a preview item.
1018    /// This does nothing if it wasn't already a preview item.
1019    pub fn unpreview_item_if_preview(&mut self, item_id: EntityId) {
1020        if self.is_active_preview_item(item_id) {
1021            self.preview_item_id = None;
1022            self.nav_history.0.lock().preview_item_id = None;
1023        }
1024    }
1025
1026    /// Marks the item with the given ID as the preview item.
1027    /// This will be ignored if the global setting `preview_tabs` is disabled.
1028    ///
1029    /// The old preview item (if there was one) is closed and its index is returned.
1030    pub fn replace_preview_item_id(
1031        &mut self,
1032        item_id: EntityId,
1033        window: &mut Window,
1034        cx: &mut Context<Self>,
1035    ) -> Option<usize> {
1036        let idx = self.close_current_preview_item(window, cx);
1037        self.set_preview_item_id(Some(item_id), cx);
1038        idx
1039    }
1040
1041    /// Marks the item with the given ID as the preview item.
1042    /// This will be ignored if the global setting `preview_tabs` is disabled.
1043    ///
1044    /// This is a low-level method. Prefer `unpreview_item_if_preview()` or `set_new_preview_item()`.
1045    pub(crate) fn set_preview_item_id(&mut self, item_id: Option<EntityId>, cx: &App) {
1046        if item_id.is_none() || PreviewTabsSettings::get_global(cx).enabled {
1047            self.preview_item_id = item_id;
1048            self.nav_history.0.lock().preview_item_id = item_id;
1049        }
1050    }
1051
1052    /// Should only be used when deserializing a pane.
1053    pub fn set_pinned_count(&mut self, count: usize) {
1054        self.pinned_tab_count = count;
1055    }
1056
1057    pub fn pinned_count(&self) -> usize {
1058        self.pinned_tab_count
1059    }
1060
1061    pub fn handle_item_edit(&mut self, item_id: EntityId, cx: &App) {
1062        if let Some(preview_item) = self.preview_item()
1063            && preview_item.item_id() == item_id
1064            && !preview_item.preserve_preview(cx)
1065        {
1066            self.unpreview_item_if_preview(item_id);
1067        }
1068    }
1069
1070    pub(crate) fn open_item(
1071        &mut self,
1072        project_entry_id: Option<ProjectEntryId>,
1073        project_path: ProjectPath,
1074        focus_item: bool,
1075        allow_preview: bool,
1076        activate: bool,
1077        suggested_position: Option<usize>,
1078        window: &mut Window,
1079        cx: &mut Context<Self>,
1080        build_item: WorkspaceItemBuilder,
1081    ) -> Box<dyn ItemHandle> {
1082        let mut existing_item = None;
1083        if let Some(project_entry_id) = project_entry_id {
1084            for (index, item) in self.items.iter().enumerate() {
1085                if item.buffer_kind(cx) == ItemBufferKind::Singleton
1086                    && item.project_entry_ids(cx).as_slice() == [project_entry_id]
1087                {
1088                    let item = item.boxed_clone();
1089                    existing_item = Some((index, item));
1090                    break;
1091                }
1092            }
1093        } else {
1094            for (index, item) in self.items.iter().enumerate() {
1095                if item.buffer_kind(cx) == ItemBufferKind::Singleton
1096                    && item.project_path(cx).as_ref() == Some(&project_path)
1097                {
1098                    let item = item.boxed_clone();
1099                    existing_item = Some((index, item));
1100                    break;
1101                }
1102            }
1103        }
1104
1105        let preview_was_active = self.preview_item_idx() == Some(self.active_item_index);
1106
1107        let set_up_existing_item =
1108            |index: usize, pane: &mut Self, window: &mut Window, cx: &mut Context<Self>| {
1109                if !allow_preview && let Some(item) = pane.items.get(index) {
1110                    pane.unpreview_item_if_preview(item.item_id());
1111                }
1112                if activate {
1113                    pane.activate_item(index, focus_item, focus_item, window, cx);
1114                }
1115            };
1116        let set_up_new_item = |new_item: Box<dyn ItemHandle>,
1117                               destination_index: Option<usize>,
1118                               pane: &mut Self,
1119                               window: &mut Window,
1120                               cx: &mut Context<Self>| {
1121            let new_item_id = new_item.item_id();
1122
1123            if allow_preview && preview_was_active {
1124                pane.set_preview_item_id(Some(new_item_id), cx);
1125            }
1126
1127            if let Some(text) = new_item.telemetry_event_text(cx) {
1128                telemetry::event!(text);
1129            }
1130
1131            pane.add_item_inner(
1132                new_item,
1133                true,
1134                focus_item,
1135                activate,
1136                destination_index,
1137                window,
1138                cx,
1139            );
1140
1141            if allow_preview && !preview_was_active {
1142                pane.set_preview_item_id(Some(new_item_id), cx);
1143            }
1144        };
1145
1146        if let Some((index, existing_item)) = existing_item {
1147            set_up_existing_item(index, self, window, cx);
1148            existing_item
1149        } else {
1150            // If the item is being opened as preview and we have an existing preview tab,
1151            // open the new item in the position of the existing preview tab.
1152            let destination_index = if allow_preview {
1153                self.close_current_preview_item(window, cx)
1154            } else {
1155                suggested_position
1156            };
1157
1158            let new_item = build_item(self, window, cx);
1159            // A special case that won't ever get a `project_entry_id` but has to be deduplicated nonetheless.
1160            if let Some(invalid_buffer_view) = new_item.downcast::<InvalidItemView>() {
1161                let mut already_open_view = None;
1162                let mut views_to_close = HashSet::default();
1163                for existing_error_view in self
1164                    .items_of_type::<InvalidItemView>()
1165                    .filter(|item| item.read(cx).abs_path == invalid_buffer_view.read(cx).abs_path)
1166                {
1167                    if already_open_view.is_none()
1168                        && existing_error_view.read(cx).error == invalid_buffer_view.read(cx).error
1169                    {
1170                        already_open_view = Some(existing_error_view);
1171                    } else {
1172                        views_to_close.insert(existing_error_view.item_id());
1173                    }
1174                }
1175
1176                let resulting_item = match already_open_view {
1177                    Some(already_open_view) => {
1178                        if let Some(index) = self.index_for_item_id(already_open_view.item_id()) {
1179                            set_up_existing_item(index, self, window, cx);
1180                        }
1181                        Box::new(already_open_view) as Box<_>
1182                    }
1183                    None => {
1184                        set_up_new_item(new_item.clone(), destination_index, self, window, cx);
1185                        new_item
1186                    }
1187                };
1188
1189                self.close_items(window, cx, SaveIntent::Skip, &|existing_item| {
1190                    views_to_close.contains(&existing_item)
1191                })
1192                .detach();
1193
1194                resulting_item
1195            } else {
1196                set_up_new_item(new_item.clone(), destination_index, self, window, cx);
1197                new_item
1198            }
1199        }
1200    }
1201
1202    pub fn close_current_preview_item(
1203        &mut self,
1204        window: &mut Window,
1205        cx: &mut Context<Self>,
1206    ) -> Option<usize> {
1207        let item_idx = self.preview_item_idx()?;
1208        let id = self.preview_item_id()?;
1209        self.preview_item_id = None;
1210
1211        let prev_active_item_index = self.active_item_index;
1212        self.remove_item(id, false, false, window, cx);
1213        self.active_item_index = prev_active_item_index;
1214        if item_idx < prev_active_item_index {
1215            self.active_item_index -= 1;
1216        }
1217        self.nav_history.0.lock().preview_item_id = None;
1218
1219        if item_idx < self.items.len() {
1220            Some(item_idx)
1221        } else {
1222            None
1223        }
1224    }
1225
1226    pub fn add_item_inner(
1227        &mut self,
1228        item: Box<dyn ItemHandle>,
1229        activate_pane: bool,
1230        focus_item: bool,
1231        activate: bool,
1232        destination_index: Option<usize>,
1233        window: &mut Window,
1234        cx: &mut Context<Self>,
1235    ) {
1236        let item_already_exists = self
1237            .items
1238            .iter()
1239            .any(|existing_item| existing_item.item_id() == item.item_id());
1240
1241        if !item_already_exists {
1242            self.close_items_on_item_open(window, cx);
1243        }
1244
1245        if item.buffer_kind(cx) == ItemBufferKind::Singleton
1246            && let Some(&entry_id) = item.project_entry_ids(cx).first()
1247        {
1248            let Some(project) = self.project.upgrade() else {
1249                return;
1250            };
1251
1252            let project = project.read(cx);
1253            if let Some(project_path) = project.path_for_entry(entry_id, cx) {
1254                let abs_path = project.absolute_path(&project_path, cx);
1255                self.nav_history
1256                    .0
1257                    .lock()
1258                    .paths_by_item
1259                    .insert(item.item_id(), (project_path, abs_path));
1260            }
1261        }
1262        // If no destination index is specified, add or move the item after the
1263        // active item (or at the start of tab bar, if the active item is pinned)
1264        let mut insertion_index = {
1265            cmp::min(
1266                if let Some(destination_index) = destination_index {
1267                    destination_index
1268                } else {
1269                    cmp::max(self.active_item_index + 1, self.pinned_count())
1270                },
1271                self.items.len(),
1272            )
1273        };
1274
1275        // Does the item already exist?
1276        let project_entry_id = if item.buffer_kind(cx) == ItemBufferKind::Singleton {
1277            item.project_entry_ids(cx).first().copied()
1278        } else {
1279            None
1280        };
1281
1282        let existing_item_index = self.items.iter().position(|existing_item| {
1283            if existing_item.item_id() == item.item_id() {
1284                true
1285            } else if existing_item.buffer_kind(cx) == ItemBufferKind::Singleton {
1286                existing_item
1287                    .project_entry_ids(cx)
1288                    .first()
1289                    .is_some_and(|existing_entry_id| {
1290                        Some(existing_entry_id) == project_entry_id.as_ref()
1291                    })
1292            } else {
1293                false
1294            }
1295        });
1296        if let Some(existing_item_index) = existing_item_index {
1297            // If the item already exists, move it to the desired destination and activate it
1298
1299            if existing_item_index != insertion_index {
1300                let existing_item_is_active = existing_item_index == self.active_item_index;
1301
1302                // If the caller didn't specify a destination and the added item is already
1303                // the active one, don't move it
1304                if existing_item_is_active && destination_index.is_none() {
1305                    insertion_index = existing_item_index;
1306                } else {
1307                    self.items.remove(existing_item_index);
1308                    if existing_item_index < self.active_item_index {
1309                        self.active_item_index -= 1;
1310                    }
1311                    insertion_index = insertion_index.min(self.items.len());
1312
1313                    self.items.insert(insertion_index, item.clone());
1314
1315                    if existing_item_is_active {
1316                        self.active_item_index = insertion_index;
1317                    } else if insertion_index <= self.active_item_index {
1318                        self.active_item_index += 1;
1319                    }
1320                }
1321
1322                cx.notify();
1323            }
1324
1325            if activate {
1326                self.activate_item(insertion_index, activate_pane, focus_item, window, cx);
1327            }
1328        } else {
1329            self.items.insert(insertion_index, item.clone());
1330            cx.notify();
1331
1332            if activate {
1333                if insertion_index <= self.active_item_index
1334                    && self.preview_item_idx() != Some(self.active_item_index)
1335                {
1336                    self.active_item_index += 1;
1337                }
1338
1339                self.activate_item(insertion_index, activate_pane, focus_item, window, cx);
1340            }
1341        }
1342
1343        cx.emit(Event::AddItem { item });
1344    }
1345
1346    pub fn add_item(
1347        &mut self,
1348        item: Box<dyn ItemHandle>,
1349        activate_pane: bool,
1350        focus_item: bool,
1351        destination_index: Option<usize>,
1352        window: &mut Window,
1353        cx: &mut Context<Self>,
1354    ) {
1355        if let Some(text) = item.telemetry_event_text(cx) {
1356            telemetry::event!(text);
1357        }
1358
1359        self.add_item_inner(
1360            item,
1361            activate_pane,
1362            focus_item,
1363            true,
1364            destination_index,
1365            window,
1366            cx,
1367        )
1368    }
1369
1370    pub fn items_len(&self) -> usize {
1371        self.items.len()
1372    }
1373
1374    pub fn items(&self) -> impl DoubleEndedIterator<Item = &Box<dyn ItemHandle>> {
1375        self.items.iter()
1376    }
1377
1378    pub fn items_of_type<T: Render>(&self) -> impl '_ + Iterator<Item = Entity<T>> {
1379        self.items
1380            .iter()
1381            .filter_map(|item| item.to_any_view().downcast().ok())
1382    }
1383
1384    pub fn active_item(&self) -> Option<Box<dyn ItemHandle>> {
1385        self.items.get(self.active_item_index).cloned()
1386    }
1387
1388    fn active_item_id(&self) -> EntityId {
1389        self.items[self.active_item_index].item_id()
1390    }
1391
1392    pub fn pixel_position_of_cursor(&self, cx: &App) -> Option<Point<Pixels>> {
1393        self.items
1394            .get(self.active_item_index)?
1395            .pixel_position_of_cursor(cx)
1396    }
1397
1398    pub fn item_for_entry(
1399        &self,
1400        entry_id: ProjectEntryId,
1401        cx: &App,
1402    ) -> Option<Box<dyn ItemHandle>> {
1403        self.items.iter().find_map(|item| {
1404            if item.buffer_kind(cx) == ItemBufferKind::Singleton
1405                && (item.project_entry_ids(cx).as_slice() == [entry_id])
1406            {
1407                Some(item.boxed_clone())
1408            } else {
1409                None
1410            }
1411        })
1412    }
1413
1414    pub fn item_for_path(
1415        &self,
1416        project_path: ProjectPath,
1417        cx: &App,
1418    ) -> Option<Box<dyn ItemHandle>> {
1419        self.items.iter().find_map(move |item| {
1420            if item.buffer_kind(cx) == ItemBufferKind::Singleton
1421                && (item.project_path(cx).as_slice() == [project_path.clone()])
1422            {
1423                Some(item.boxed_clone())
1424            } else {
1425                None
1426            }
1427        })
1428    }
1429
1430    pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option<usize> {
1431        self.index_for_item_id(item.item_id())
1432    }
1433
1434    fn index_for_item_id(&self, item_id: EntityId) -> Option<usize> {
1435        self.items.iter().position(|i| i.item_id() == item_id)
1436    }
1437
1438    pub fn item_for_index(&self, ix: usize) -> Option<&dyn ItemHandle> {
1439        self.items.get(ix).map(|i| i.as_ref())
1440    }
1441
1442    pub fn toggle_zoom(&mut self, _: &ToggleZoom, window: &mut Window, cx: &mut Context<Self>) {
1443        if !self.can_toggle_zoom {
1444            cx.propagate();
1445        } else if self.zoomed {
1446            cx.emit(Event::ZoomOut);
1447        } else if !self.items.is_empty() {
1448            if !self.focus_handle.contains_focused(window, cx) {
1449                cx.focus_self(window);
1450            }
1451            cx.emit(Event::ZoomIn);
1452        }
1453    }
1454
1455    pub fn zoom_in(&mut self, _: &ZoomIn, window: &mut Window, cx: &mut Context<Self>) {
1456        if !self.can_toggle_zoom {
1457            cx.propagate();
1458        } else if !self.zoomed && !self.items.is_empty() {
1459            if !self.focus_handle.contains_focused(window, cx) {
1460                cx.focus_self(window);
1461            }
1462            cx.emit(Event::ZoomIn);
1463        }
1464    }
1465
1466    pub fn zoom_out(&mut self, _: &ZoomOut, _window: &mut Window, cx: &mut Context<Self>) {
1467        if !self.can_toggle_zoom {
1468            cx.propagate();
1469        } else if self.zoomed {
1470            cx.emit(Event::ZoomOut);
1471        }
1472    }
1473
1474    pub fn activate_item(
1475        &mut self,
1476        index: usize,
1477        activate_pane: bool,
1478        focus_item: bool,
1479        window: &mut Window,
1480        cx: &mut Context<Self>,
1481    ) {
1482        use NavigationMode::{GoingBack, GoingForward};
1483        if index < self.items.len() {
1484            let prev_active_item_ix = mem::replace(&mut self.active_item_index, index);
1485            if (prev_active_item_ix != self.active_item_index
1486                || matches!(self.nav_history.mode(), GoingBack | GoingForward))
1487                && let Some(prev_item) = self.items.get(prev_active_item_ix)
1488            {
1489                prev_item.deactivated(window, cx);
1490            }
1491            self.update_history(index);
1492            self.update_toolbar(window, cx);
1493            self.update_status_bar(window, cx);
1494
1495            if focus_item {
1496                self.focus_active_item(window, cx);
1497            }
1498
1499            cx.emit(Event::ActivateItem {
1500                local: activate_pane,
1501                focus_changed: focus_item,
1502            });
1503
1504            self.update_active_tab(index);
1505            cx.notify();
1506        }
1507    }
1508
1509    fn update_active_tab(&mut self, index: usize) {
1510        if !self.is_tab_pinned(index) {
1511            self.suppress_scroll = false;
1512            self.tab_bar_scroll_handle
1513                .scroll_to_item(index - self.pinned_tab_count);
1514        }
1515    }
1516
1517    fn update_history(&mut self, index: usize) {
1518        if let Some(newly_active_item) = self.items.get(index) {
1519            self.activation_history
1520                .retain(|entry| entry.entity_id != newly_active_item.item_id());
1521            self.activation_history.push(ActivationHistoryEntry {
1522                entity_id: newly_active_item.item_id(),
1523                timestamp: self
1524                    .next_activation_timestamp
1525                    .fetch_add(1, Ordering::SeqCst),
1526            });
1527        }
1528    }
1529
1530    pub fn activate_previous_item(
1531        &mut self,
1532        action: &ActivatePreviousItem,
1533        window: &mut Window,
1534        cx: &mut Context<Self>,
1535    ) {
1536        let mut index = self.active_item_index;
1537        if index > 0 {
1538            index -= 1;
1539        } else if action.wrap_around && !self.items.is_empty() {
1540            index = self.items.len() - 1;
1541        }
1542        self.activate_item(index, true, true, window, cx);
1543    }
1544
1545    pub fn activate_next_item(
1546        &mut self,
1547        action: &ActivateNextItem,
1548        window: &mut Window,
1549        cx: &mut Context<Self>,
1550    ) {
1551        let mut index = self.active_item_index;
1552        if index + 1 < self.items.len() {
1553            index += 1;
1554        } else if action.wrap_around {
1555            index = 0;
1556        }
1557        self.activate_item(index, true, true, window, cx);
1558    }
1559
1560    pub fn swap_item_left(
1561        &mut self,
1562        _: &SwapItemLeft,
1563        window: &mut Window,
1564        cx: &mut Context<Self>,
1565    ) {
1566        let index = self.active_item_index;
1567        if index == 0 {
1568            return;
1569        }
1570
1571        self.items.swap(index, index - 1);
1572        self.activate_item(index - 1, true, true, window, cx);
1573    }
1574
1575    pub fn swap_item_right(
1576        &mut self,
1577        _: &SwapItemRight,
1578        window: &mut Window,
1579        cx: &mut Context<Self>,
1580    ) {
1581        let index = self.active_item_index;
1582        if index + 1 >= self.items.len() {
1583            return;
1584        }
1585
1586        self.items.swap(index, index + 1);
1587        self.activate_item(index + 1, true, true, window, cx);
1588    }
1589
1590    pub fn activate_last_item(
1591        &mut self,
1592        _: &ActivateLastItem,
1593        window: &mut Window,
1594        cx: &mut Context<Self>,
1595    ) {
1596        let index = self.items.len().saturating_sub(1);
1597        self.activate_item(index, true, true, window, cx);
1598    }
1599
1600    pub fn close_active_item(
1601        &mut self,
1602        action: &CloseActiveItem,
1603        window: &mut Window,
1604        cx: &mut Context<Self>,
1605    ) -> Task<Result<()>> {
1606        if self.items.is_empty() {
1607            // Close the window when there's no active items to close, if configured
1608            if WorkspaceSettings::get_global(cx)
1609                .when_closing_with_no_tabs
1610                .should_close()
1611            {
1612                window.dispatch_action(Box::new(CloseWindow), cx);
1613            }
1614
1615            return Task::ready(Ok(()));
1616        }
1617        if self.is_tab_pinned(self.active_item_index) && !action.close_pinned {
1618            // Activate any non-pinned tab in same pane
1619            let non_pinned_tab_index = self
1620                .items()
1621                .enumerate()
1622                .find(|(index, _item)| !self.is_tab_pinned(*index))
1623                .map(|(index, _item)| index);
1624            if let Some(index) = non_pinned_tab_index {
1625                self.activate_item(index, false, false, window, cx);
1626                return Task::ready(Ok(()));
1627            }
1628
1629            // Activate any non-pinned tab in different pane
1630            let current_pane = cx.entity();
1631            cx.defer_in(window, move |this, window, cx| {
1632                this.workspace
1633                    .update(cx, |workspace, cx| {
1634                        let panes = workspace.center.panes();
1635                        let pane_with_unpinned_tab = panes.iter().find(|pane| {
1636                            if **pane == &current_pane {
1637                                return false;
1638                            }
1639                            pane.read(cx).has_unpinned_tabs()
1640                        });
1641                        if let Some(pane) = pane_with_unpinned_tab {
1642                            pane.update(cx, |pane, cx| pane.activate_unpinned_tab(window, cx));
1643                        }
1644                    })
1645                    .ok();
1646            });
1647
1648            return Task::ready(Ok(()));
1649        };
1650
1651        let active_item_id = self.active_item_id();
1652
1653        self.close_item_by_id(
1654            active_item_id,
1655            action.save_intent.unwrap_or(SaveIntent::Close),
1656            window,
1657            cx,
1658        )
1659    }
1660
1661    pub fn close_item_by_id(
1662        &mut self,
1663        item_id_to_close: EntityId,
1664        save_intent: SaveIntent,
1665        window: &mut Window,
1666        cx: &mut Context<Self>,
1667    ) -> Task<Result<()>> {
1668        self.close_items(window, cx, save_intent, &move |view_id| {
1669            view_id == item_id_to_close
1670        })
1671    }
1672
1673    pub fn close_items_for_project_path(
1674        &mut self,
1675        project_path: &ProjectPath,
1676        save_intent: SaveIntent,
1677        close_pinned: bool,
1678        window: &mut Window,
1679        cx: &mut Context<Self>,
1680    ) -> Task<Result<()>> {
1681        let pinned_item_ids = self.pinned_item_ids();
1682        let matching_item_ids: Vec<_> = self
1683            .items()
1684            .filter(|item| item.project_path(cx).as_ref() == Some(project_path))
1685            .map(|item| item.item_id())
1686            .collect();
1687        self.close_items(window, cx, save_intent, &move |item_id| {
1688            matching_item_ids.contains(&item_id)
1689                && (close_pinned || !pinned_item_ids.contains(&item_id))
1690        })
1691    }
1692
1693    pub fn close_other_items(
1694        &mut self,
1695        action: &CloseOtherItems,
1696        target_item_id: Option<EntityId>,
1697        window: &mut Window,
1698        cx: &mut Context<Self>,
1699    ) -> Task<Result<()>> {
1700        if self.items.is_empty() {
1701            return Task::ready(Ok(()));
1702        }
1703
1704        let active_item_id = match target_item_id {
1705            Some(result) => result,
1706            None => self.active_item_id(),
1707        };
1708
1709        self.unpreview_item_if_preview(active_item_id);
1710
1711        let pinned_item_ids = self.pinned_item_ids();
1712
1713        self.close_items(
1714            window,
1715            cx,
1716            action.save_intent.unwrap_or(SaveIntent::Close),
1717            &move |item_id| {
1718                item_id != active_item_id
1719                    && (action.close_pinned || !pinned_item_ids.contains(&item_id))
1720            },
1721        )
1722    }
1723
1724    pub fn close_multibuffer_items(
1725        &mut self,
1726        action: &CloseMultibufferItems,
1727        window: &mut Window,
1728        cx: &mut Context<Self>,
1729    ) -> Task<Result<()>> {
1730        if self.items.is_empty() {
1731            return Task::ready(Ok(()));
1732        }
1733
1734        let pinned_item_ids = self.pinned_item_ids();
1735        let multibuffer_items = self.multibuffer_item_ids(cx);
1736
1737        self.close_items(
1738            window,
1739            cx,
1740            action.save_intent.unwrap_or(SaveIntent::Close),
1741            &move |item_id| {
1742                (action.close_pinned || !pinned_item_ids.contains(&item_id))
1743                    && multibuffer_items.contains(&item_id)
1744            },
1745        )
1746    }
1747
1748    pub fn close_clean_items(
1749        &mut self,
1750        action: &CloseCleanItems,
1751        window: &mut Window,
1752        cx: &mut Context<Self>,
1753    ) -> Task<Result<()>> {
1754        if self.items.is_empty() {
1755            return Task::ready(Ok(()));
1756        }
1757
1758        let clean_item_ids = self.clean_item_ids(cx);
1759        let pinned_item_ids = self.pinned_item_ids();
1760
1761        self.close_items(window, cx, SaveIntent::Close, &move |item_id| {
1762            clean_item_ids.contains(&item_id)
1763                && (action.close_pinned || !pinned_item_ids.contains(&item_id))
1764        })
1765    }
1766
1767    pub fn close_items_to_the_left_by_id(
1768        &mut self,
1769        item_id: Option<EntityId>,
1770        action: &CloseItemsToTheLeft,
1771        window: &mut Window,
1772        cx: &mut Context<Self>,
1773    ) -> Task<Result<()>> {
1774        self.close_items_to_the_side_by_id(item_id, Side::Left, action.close_pinned, window, cx)
1775    }
1776
1777    pub fn close_items_to_the_right_by_id(
1778        &mut self,
1779        item_id: Option<EntityId>,
1780        action: &CloseItemsToTheRight,
1781        window: &mut Window,
1782        cx: &mut Context<Self>,
1783    ) -> Task<Result<()>> {
1784        self.close_items_to_the_side_by_id(item_id, Side::Right, action.close_pinned, window, cx)
1785    }
1786
1787    pub fn close_items_to_the_side_by_id(
1788        &mut self,
1789        item_id: Option<EntityId>,
1790        side: Side,
1791        close_pinned: bool,
1792        window: &mut Window,
1793        cx: &mut Context<Self>,
1794    ) -> Task<Result<()>> {
1795        if self.items.is_empty() {
1796            return Task::ready(Ok(()));
1797        }
1798
1799        let item_id = item_id.unwrap_or_else(|| self.active_item_id());
1800        let to_the_side_item_ids = self.to_the_side_item_ids(item_id, side);
1801        let pinned_item_ids = self.pinned_item_ids();
1802
1803        self.close_items(window, cx, SaveIntent::Close, &move |item_id| {
1804            to_the_side_item_ids.contains(&item_id)
1805                && (close_pinned || !pinned_item_ids.contains(&item_id))
1806        })
1807    }
1808
1809    pub fn close_all_items(
1810        &mut self,
1811        action: &CloseAllItems,
1812        window: &mut Window,
1813        cx: &mut Context<Self>,
1814    ) -> Task<Result<()>> {
1815        if self.items.is_empty() {
1816            return Task::ready(Ok(()));
1817        }
1818
1819        let pinned_item_ids = self.pinned_item_ids();
1820
1821        self.close_items(
1822            window,
1823            cx,
1824            action.save_intent.unwrap_or(SaveIntent::Close),
1825            &|item_id| action.close_pinned || !pinned_item_ids.contains(&item_id),
1826        )
1827    }
1828
1829    fn close_items_on_item_open(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1830        let target = self.max_tabs.map(|m| m.get());
1831        let protect_active_item = false;
1832        self.close_items_to_target_count(target, protect_active_item, window, cx);
1833    }
1834
1835    fn close_items_on_settings_change(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1836        let target = self.max_tabs.map(|m| m.get() + 1);
1837        // The active item in this case is the settings.json file, which should be protected from being closed
1838        let protect_active_item = true;
1839        self.close_items_to_target_count(target, protect_active_item, window, cx);
1840    }
1841
1842    fn close_items_to_target_count(
1843        &mut self,
1844        target_count: Option<usize>,
1845        protect_active_item: bool,
1846        window: &mut Window,
1847        cx: &mut Context<Self>,
1848    ) {
1849        let Some(target_count) = target_count else {
1850            return;
1851        };
1852
1853        let mut index_list = Vec::new();
1854        let mut items_len = self.items_len();
1855        let mut indexes: HashMap<EntityId, usize> = HashMap::default();
1856        let active_ix = self.active_item_index();
1857
1858        for (index, item) in self.items.iter().enumerate() {
1859            indexes.insert(item.item_id(), index);
1860        }
1861
1862        // Close least recently used items to reach target count.
1863        // The target count is allowed to be exceeded, as we protect pinned
1864        // items, dirty items, and sometimes, the active item.
1865        for entry in self.activation_history.iter() {
1866            if items_len < target_count {
1867                break;
1868            }
1869
1870            let Some(&index) = indexes.get(&entry.entity_id) else {
1871                continue;
1872            };
1873
1874            if protect_active_item && index == active_ix {
1875                continue;
1876            }
1877
1878            if let Some(true) = self.items.get(index).map(|item| item.is_dirty(cx)) {
1879                continue;
1880            }
1881
1882            if self.is_tab_pinned(index) {
1883                continue;
1884            }
1885
1886            index_list.push(index);
1887            items_len -= 1;
1888        }
1889        // The sort and reverse is necessary since we remove items
1890        // using their index position, hence removing from the end
1891        // of the list first to avoid changing indexes.
1892        index_list.sort_unstable();
1893        index_list
1894            .iter()
1895            .rev()
1896            .for_each(|&index| self._remove_item(index, false, false, None, window, cx));
1897    }
1898
1899    // Usually when you close an item that has unsaved changes, we prompt you to
1900    // save it. That said, if you still have the buffer open in a different pane
1901    // we can close this one without fear of losing data.
1902    pub fn skip_save_on_close(item: &dyn ItemHandle, workspace: &Workspace, cx: &App) -> bool {
1903        let mut dirty_project_item_ids = Vec::new();
1904        item.for_each_project_item(cx, &mut |project_item_id, project_item| {
1905            if project_item.is_dirty() {
1906                dirty_project_item_ids.push(project_item_id);
1907            }
1908        });
1909        if dirty_project_item_ids.is_empty() {
1910            return !(item.buffer_kind(cx) == ItemBufferKind::Singleton && item.is_dirty(cx));
1911        }
1912
1913        for open_item in workspace.items(cx) {
1914            if open_item.item_id() == item.item_id() {
1915                continue;
1916            }
1917            if open_item.buffer_kind(cx) != ItemBufferKind::Singleton {
1918                continue;
1919            }
1920            let other_project_item_ids = open_item.project_item_model_ids(cx);
1921            dirty_project_item_ids.retain(|id| !other_project_item_ids.contains(id));
1922        }
1923        dirty_project_item_ids.is_empty()
1924    }
1925
1926    pub(super) fn file_names_for_prompt(
1927        items: &mut dyn Iterator<Item = &Box<dyn ItemHandle>>,
1928        cx: &App,
1929    ) -> String {
1930        let mut file_names = BTreeSet::default();
1931        for item in items {
1932            item.for_each_project_item(cx, &mut |_, project_item| {
1933                if !project_item.is_dirty() {
1934                    return;
1935                }
1936                let filename = project_item
1937                    .project_path(cx)
1938                    .and_then(|path| path.path.file_name().map(ToOwned::to_owned));
1939                file_names.insert(filename.unwrap_or("untitled".to_string()));
1940            });
1941        }
1942        if file_names.len() > 6 {
1943            format!(
1944                "{}\n.. and {} more",
1945                file_names.iter().take(5).join("\n"),
1946                file_names.len() - 5
1947            )
1948        } else {
1949            file_names.into_iter().join("\n")
1950        }
1951    }
1952
1953    pub fn close_items(
1954        &self,
1955        window: &mut Window,
1956        cx: &mut Context<Pane>,
1957        mut save_intent: SaveIntent,
1958        should_close: &dyn Fn(EntityId) -> bool,
1959    ) -> Task<Result<()>> {
1960        // Find the items to close.
1961        let mut items_to_close = Vec::new();
1962        for item in &self.items {
1963            if should_close(item.item_id()) {
1964                items_to_close.push(item.boxed_clone());
1965            }
1966        }
1967
1968        let active_item_id = self.active_item().map(|item| item.item_id());
1969
1970        items_to_close.sort_by_key(|item| {
1971            let path = item.project_path(cx);
1972            // Put the currently active item at the end, because if the currently active item is not closed last
1973            // closing the currently active item will cause the focus to switch to another item
1974            // This will cause Zed to expand the content of the currently active item
1975            //
1976            // Beyond that sort in order of project path, with untitled files and multibuffers coming last.
1977            (active_item_id == Some(item.item_id()), path.is_none(), path)
1978        });
1979
1980        let workspace = self.workspace.clone();
1981        let Some(project) = self.project.upgrade() else {
1982            return Task::ready(Ok(()));
1983        };
1984        cx.spawn_in(window, async move |pane, cx| {
1985            let dirty_items = workspace.update(cx, |workspace, cx| {
1986                items_to_close
1987                    .iter()
1988                    .filter(|item| {
1989                        item.is_dirty(cx) && !Self::skip_save_on_close(item.as_ref(), workspace, cx)
1990                    })
1991                    .map(|item| item.boxed_clone())
1992                    .collect::<Vec<_>>()
1993            })?;
1994
1995            if save_intent == SaveIntent::Close && dirty_items.len() > 1 {
1996                let answer = pane.update_in(cx, |_, window, cx| {
1997                    let detail = Self::file_names_for_prompt(&mut dirty_items.iter(), cx);
1998                    window.prompt(
1999                        PromptLevel::Warning,
2000                        "Do you want to save changes to the following files?",
2001                        Some(&detail),
2002                        &["Save all", "Discard all", "Cancel"],
2003                        cx,
2004                    )
2005                })?;
2006                match answer.await {
2007                    Ok(0) => save_intent = SaveIntent::SaveAll,
2008                    Ok(1) => save_intent = SaveIntent::Skip,
2009                    Ok(2) => return Ok(()),
2010                    _ => {}
2011                }
2012            }
2013
2014            for item_to_close in items_to_close {
2015                let mut should_close = true;
2016                let mut should_save = true;
2017                if save_intent == SaveIntent::Close {
2018                    workspace.update(cx, |workspace, cx| {
2019                        if Self::skip_save_on_close(item_to_close.as_ref(), workspace, cx) {
2020                            should_save = false;
2021                        }
2022                    })?;
2023                }
2024
2025                if should_save {
2026                    match Self::save_item(project.clone(), &pane, &*item_to_close, save_intent, cx)
2027                        .await
2028                    {
2029                        Ok(success) => {
2030                            if !success {
2031                                should_close = false;
2032                            }
2033                        }
2034                        Err(err) => {
2035                            let answer = pane.update_in(cx, |_, window, cx| {
2036                                let detail = Self::file_names_for_prompt(
2037                                    &mut [&item_to_close].into_iter(),
2038                                    cx,
2039                                );
2040                                window.prompt(
2041                                    PromptLevel::Warning,
2042                                    &format!("Unable to save file: {}", &err),
2043                                    Some(&detail),
2044                                    &["Close Without Saving", "Cancel"],
2045                                    cx,
2046                                )
2047                            })?;
2048                            match answer.await {
2049                                Ok(0) => {}
2050                                Ok(1..) | Err(_) => should_close = false,
2051                            }
2052                        }
2053                    }
2054                }
2055
2056                // Remove the item from the pane.
2057                if should_close {
2058                    pane.update_in(cx, |pane, window, cx| {
2059                        pane.remove_item(
2060                            item_to_close.item_id(),
2061                            false,
2062                            pane.close_pane_if_empty,
2063                            window,
2064                            cx,
2065                        );
2066                    })
2067                    .ok();
2068                }
2069            }
2070
2071            pane.update(cx, |_, cx| cx.notify()).ok();
2072            Ok(())
2073        })
2074    }
2075
2076    pub fn take_active_item(
2077        &mut self,
2078        window: &mut Window,
2079        cx: &mut Context<Self>,
2080    ) -> Option<Box<dyn ItemHandle>> {
2081        let item = self.active_item()?;
2082        self.remove_item(item.item_id(), false, false, window, cx);
2083        Some(item)
2084    }
2085
2086    pub fn remove_item(
2087        &mut self,
2088        item_id: EntityId,
2089        activate_pane: bool,
2090        close_pane_if_empty: bool,
2091        window: &mut Window,
2092        cx: &mut Context<Self>,
2093    ) {
2094        let Some(item_index) = self.index_for_item_id(item_id) else {
2095            return;
2096        };
2097        self._remove_item(
2098            item_index,
2099            activate_pane,
2100            close_pane_if_empty,
2101            None,
2102            window,
2103            cx,
2104        )
2105    }
2106
2107    pub fn remove_item_and_focus_on_pane(
2108        &mut self,
2109        item_index: usize,
2110        activate_pane: bool,
2111        focus_on_pane_if_closed: Entity<Pane>,
2112        window: &mut Window,
2113        cx: &mut Context<Self>,
2114    ) {
2115        self._remove_item(
2116            item_index,
2117            activate_pane,
2118            true,
2119            Some(focus_on_pane_if_closed),
2120            window,
2121            cx,
2122        )
2123    }
2124
2125    fn _remove_item(
2126        &mut self,
2127        item_index: usize,
2128        activate_pane: bool,
2129        close_pane_if_empty: bool,
2130        focus_on_pane_if_closed: Option<Entity<Pane>>,
2131        window: &mut Window,
2132        cx: &mut Context<Self>,
2133    ) {
2134        let activate_on_close = &ItemSettings::get_global(cx).activate_on_close;
2135        self.activation_history
2136            .retain(|entry| entry.entity_id != self.items[item_index].item_id());
2137
2138        if self.is_tab_pinned(item_index) {
2139            self.pinned_tab_count -= 1;
2140        }
2141        if item_index == self.active_item_index {
2142            let left_neighbour_index = || item_index.min(self.items.len()).saturating_sub(1);
2143            let index_to_activate = match activate_on_close {
2144                ActivateOnClose::History => self
2145                    .activation_history
2146                    .pop()
2147                    .and_then(|last_activated_item| {
2148                        self.items.iter().enumerate().find_map(|(index, item)| {
2149                            (item.item_id() == last_activated_item.entity_id).then_some(index)
2150                        })
2151                    })
2152                    // We didn't have a valid activation history entry, so fallback
2153                    // to activating the item to the left
2154                    .unwrap_or_else(left_neighbour_index),
2155                ActivateOnClose::Neighbour => {
2156                    self.activation_history.pop();
2157                    if item_index + 1 < self.items.len() {
2158                        item_index + 1
2159                    } else {
2160                        item_index.saturating_sub(1)
2161                    }
2162                }
2163                ActivateOnClose::LeftNeighbour => {
2164                    self.activation_history.pop();
2165                    left_neighbour_index()
2166                }
2167            };
2168
2169            let should_activate = activate_pane || self.has_focus(window, cx);
2170            if self.items.len() == 1 && should_activate {
2171                self.focus_handle.focus(window, cx);
2172            } else {
2173                self.activate_item(
2174                    index_to_activate,
2175                    should_activate,
2176                    should_activate,
2177                    window,
2178                    cx,
2179                );
2180            }
2181        }
2182
2183        let item = self.items.remove(item_index);
2184
2185        cx.emit(Event::RemovedItem { item: item.clone() });
2186        if self.items.is_empty() {
2187            item.deactivated(window, cx);
2188            if close_pane_if_empty {
2189                self.update_toolbar(window, cx);
2190                cx.emit(Event::Remove {
2191                    focus_on_pane: focus_on_pane_if_closed,
2192                });
2193            }
2194        }
2195
2196        if item_index < self.active_item_index {
2197            self.active_item_index -= 1;
2198        }
2199
2200        let mode = self.nav_history.mode();
2201        self.nav_history.set_mode(NavigationMode::ClosingItem);
2202        item.deactivated(window, cx);
2203        item.on_removed(cx);
2204        self.nav_history.set_mode(mode);
2205        self.unpreview_item_if_preview(item.item_id());
2206
2207        if let Some(path) = item.project_path(cx) {
2208            let abs_path = self
2209                .nav_history
2210                .0
2211                .lock()
2212                .paths_by_item
2213                .get(&item.item_id())
2214                .and_then(|(_, abs_path)| abs_path.clone());
2215
2216            self.nav_history
2217                .0
2218                .lock()
2219                .paths_by_item
2220                .insert(item.item_id(), (path, abs_path));
2221        } else {
2222            self.nav_history
2223                .0
2224                .lock()
2225                .paths_by_item
2226                .remove(&item.item_id());
2227        }
2228
2229        if self.zoom_out_on_close && self.items.is_empty() && close_pane_if_empty && self.zoomed {
2230            cx.emit(Event::ZoomOut);
2231        }
2232
2233        cx.notify();
2234    }
2235
2236    pub async fn save_item(
2237        project: Entity<Project>,
2238        pane: &WeakEntity<Pane>,
2239        item: &dyn ItemHandle,
2240        save_intent: SaveIntent,
2241        cx: &mut AsyncWindowContext,
2242    ) -> Result<bool> {
2243        const CONFLICT_MESSAGE: &str = "This file has changed on disk since you started editing it. Do you want to overwrite it?";
2244
2245        const DELETED_MESSAGE: &str = "This file has been deleted on disk since you started editing it. Do you want to recreate it?";
2246
2247        let path_style = project.read_with(cx, |project, cx| project.path_style(cx));
2248        if save_intent == SaveIntent::Skip {
2249            let is_saveable_singleton = cx.update(|_window, cx| {
2250                item.can_save(cx) && item.buffer_kind(cx) == ItemBufferKind::Singleton
2251            })?;
2252            if is_saveable_singleton {
2253                pane.update_in(cx, |_, window, cx| item.reload(project, window, cx))?
2254                    .await
2255                    .log_err();
2256            }
2257            return Ok(true);
2258        };
2259        let Some(item_ix) = pane
2260            .read_with(cx, |pane, _| pane.index_for_item(item))
2261            .ok()
2262            .flatten()
2263        else {
2264            return Ok(true);
2265        };
2266
2267        let (
2268            mut has_conflict,
2269            mut is_dirty,
2270            mut can_save,
2271            can_save_as,
2272            is_singleton,
2273            has_deleted_file,
2274        ) = cx.update(|_window, cx| {
2275            (
2276                item.has_conflict(cx),
2277                item.is_dirty(cx),
2278                item.can_save(cx),
2279                item.can_save_as(cx),
2280                item.buffer_kind(cx) == ItemBufferKind::Singleton,
2281                item.has_deleted_file(cx),
2282            )
2283        })?;
2284
2285        // when saving a single buffer, we ignore whether or not it's dirty.
2286        if save_intent == SaveIntent::Save
2287            || save_intent == SaveIntent::FormatAndSave
2288            || save_intent == SaveIntent::SaveWithoutFormat
2289        {
2290            is_dirty = true;
2291        }
2292
2293        if save_intent == SaveIntent::SaveAs {
2294            is_dirty = true;
2295            has_conflict = false;
2296            can_save = false;
2297        }
2298
2299        if save_intent == SaveIntent::Overwrite {
2300            has_conflict = false;
2301        }
2302
2303        let should_format = save_intent != SaveIntent::SaveWithoutFormat;
2304        let force_format = save_intent == SaveIntent::FormatAndSave;
2305
2306        if has_conflict && can_save {
2307            if has_deleted_file && is_singleton {
2308                let answer = pane.update_in(cx, |pane, window, cx| {
2309                    pane.activate_item(item_ix, true, true, window, cx);
2310                    window.prompt(
2311                        PromptLevel::Warning,
2312                        DELETED_MESSAGE,
2313                        None,
2314                        &["Save", "Close", "Cancel"],
2315                        cx,
2316                    )
2317                })?;
2318                match answer.await {
2319                    Ok(0) => {
2320                        pane.update_in(cx, |_, window, cx| {
2321                            item.save(
2322                                SaveOptions {
2323                                    format: should_format,
2324                                    force_format,
2325                                    autosave: false,
2326                                },
2327                                project,
2328                                window,
2329                                cx,
2330                            )
2331                        })?
2332                        .await?
2333                    }
2334                    Ok(1) => {
2335                        pane.update_in(cx, |pane, window, cx| {
2336                            pane.remove_item(item.item_id(), false, true, window, cx)
2337                        })?;
2338                    }
2339                    _ => return Ok(false),
2340                }
2341                return Ok(true);
2342            } else {
2343                let answer = pane.update_in(cx, |pane, window, cx| {
2344                    pane.activate_item(item_ix, true, true, window, cx);
2345                    window.prompt(
2346                        PromptLevel::Warning,
2347                        CONFLICT_MESSAGE,
2348                        None,
2349                        &["Overwrite", "Discard Edits", "Cancel"],
2350                        cx,
2351                    )
2352                })?;
2353                match answer.await {
2354                    Ok(0) => {
2355                        pane.update_in(cx, |_, window, cx| {
2356                            item.save(
2357                                SaveOptions {
2358                                    format: should_format,
2359                                    force_format,
2360                                    autosave: false,
2361                                },
2362                                project,
2363                                window,
2364                                cx,
2365                            )
2366                        })?
2367                        .await?
2368                    }
2369                    Ok(1) => {
2370                        pane.update_in(cx, |_, window, cx| item.reload(project, window, cx))?
2371                            .await?
2372                    }
2373                    _ => return Ok(false),
2374                }
2375            }
2376        } else if is_dirty && (can_save || can_save_as) {
2377            if save_intent == SaveIntent::Close {
2378                let will_autosave = cx.update(|_window, cx| {
2379                    item.can_autosave(cx)
2380                        && item.workspace_settings(cx).autosave.should_save_on_close()
2381                })?;
2382                if !will_autosave {
2383                    let item_id = item.item_id();
2384                    let answer_task = pane.update_in(cx, |pane, window, cx| {
2385                        if pane.save_modals_spawned.insert(item_id) {
2386                            pane.activate_item(item_ix, true, true, window, cx);
2387                            let prompt = dirty_message_for(item.project_path(cx), path_style);
2388                            Some(window.prompt(
2389                                PromptLevel::Warning,
2390                                &prompt,
2391                                None,
2392                                &["Save", "Don't Save", "Cancel"],
2393                                cx,
2394                            ))
2395                        } else {
2396                            None
2397                        }
2398                    })?;
2399                    if let Some(answer_task) = answer_task {
2400                        let answer = answer_task.await;
2401                        pane.update(cx, |pane, _| {
2402                            if !pane.save_modals_spawned.remove(&item_id) {
2403                                debug_panic!(
2404                                    "save modal was not present in spawned modals after awaiting for its answer"
2405                                )
2406                            }
2407                        })?;
2408                        match answer {
2409                            Ok(0) => {}
2410                            Ok(1) => {
2411                                // Don't save this file - reload from disk to discard changes
2412                                pane.update_in(cx, |pane, _, cx| {
2413                                    if pane.is_tab_pinned(item_ix) && !item.can_save(cx) {
2414                                        pane.pinned_tab_count -= 1;
2415                                    }
2416                                })
2417                                .log_err();
2418                                if can_save && is_singleton {
2419                                    pane.update_in(cx, |_, window, cx| {
2420                                        item.reload(project.clone(), window, cx)
2421                                    })?
2422                                    .await
2423                                    .log_err();
2424                                }
2425                                return Ok(true);
2426                            }
2427                            _ => return Ok(false), // Cancel
2428                        }
2429                    } else {
2430                        return Ok(false);
2431                    }
2432                }
2433            }
2434
2435            if can_save {
2436                pane.update_in(cx, |pane, window, cx| {
2437                    pane.unpreview_item_if_preview(item.item_id());
2438                    item.save(
2439                        SaveOptions {
2440                            format: should_format,
2441                            force_format,
2442                            autosave: false,
2443                        },
2444                        project,
2445                        window,
2446                        cx,
2447                    )
2448                })?
2449                .await?;
2450            } else if can_save_as && is_singleton {
2451                let suggested_name =
2452                    cx.update(|_window, cx| item.suggested_filename(cx).to_string())?;
2453                let new_path = pane.update_in(cx, |pane, window, cx| {
2454                    pane.activate_item(item_ix, true, true, window, cx);
2455                    pane.workspace.update(cx, |workspace, cx| {
2456                        let lister = if workspace.project().read(cx).is_local() {
2457                            DirectoryLister::Local(
2458                                workspace.project().clone(),
2459                                workspace.app_state().fs.clone(),
2460                            )
2461                        } else {
2462                            DirectoryLister::Project(workspace.project().clone())
2463                        };
2464                        workspace.prompt_for_new_path(lister, Some(suggested_name), window, cx)
2465                    })
2466                })??;
2467                let Some(new_path) = new_path.await.ok().flatten().into_iter().flatten().next()
2468                else {
2469                    return Ok(false);
2470                };
2471
2472                let project_path = pane
2473                    .update(cx, |pane, cx| {
2474                        pane.project
2475                            .update(cx, |project, cx| {
2476                                project.find_or_create_worktree(new_path, true, cx)
2477                            })
2478                            .ok()
2479                    })
2480                    .ok()
2481                    .flatten();
2482                let save_task = if let Some(project_path) = project_path {
2483                    let (worktree, path) = project_path.await?;
2484                    let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
2485                    let new_path = ProjectPath { worktree_id, path };
2486
2487                    pane.update_in(cx, |pane, window, cx| {
2488                        if let Some(item) = pane.item_for_path(new_path.clone(), cx) {
2489                            pane.remove_item(item.item_id(), false, false, window, cx);
2490                        }
2491
2492                        item.save_as(project.clone(), new_path, window, cx)
2493                    })?
2494                } else {
2495                    return Ok(false);
2496                };
2497
2498                save_task.await?;
2499                if should_format {
2500                    pane.update_in(cx, |pane, window, cx| {
2501                        pane.unpreview_item_if_preview(item.item_id());
2502                        item.save(
2503                            SaveOptions {
2504                                format: true,
2505                                autosave: false,
2506                                force_format,
2507                            },
2508                            project,
2509                            window,
2510                            cx,
2511                        )
2512                    })?
2513                    .await?;
2514                }
2515                return Ok(true);
2516            }
2517        }
2518
2519        pane.update(cx, |_, cx| {
2520            cx.emit(Event::UserSavedItem {
2521                item: item.downgrade_item(),
2522                save_intent,
2523            });
2524            true
2525        })
2526    }
2527
2528    pub fn autosave_item(
2529        item: &dyn ItemHandle,
2530        project: Entity<Project>,
2531        window: &mut Window,
2532        cx: &mut App,
2533    ) -> Task<Result<()>> {
2534        let format = !matches!(
2535            item.workspace_settings(cx).autosave,
2536            AutosaveSetting::AfterDelay { .. }
2537        );
2538        if item.can_autosave(cx) {
2539            item.save(
2540                SaveOptions {
2541                    format,
2542                    force_format: false,
2543                    autosave: true,
2544                },
2545                project,
2546                window,
2547                cx,
2548            )
2549        } else {
2550            Task::ready(Ok(()))
2551        }
2552    }
2553
2554    pub fn focus_active_item(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2555        if let Some(active_item) = self.active_item() {
2556            let focus_handle = active_item.item_focus_handle(cx);
2557            window.focus(&focus_handle, cx);
2558        }
2559    }
2560
2561    pub fn split(
2562        &mut self,
2563        direction: SplitDirection,
2564        mode: SplitMode,
2565        window: &mut Window,
2566        cx: &mut Context<Self>,
2567    ) {
2568        if self.items.len() <= 1 && mode == SplitMode::MovePane {
2569            // MovePane with only one pane present behaves like a SplitEmpty in the opposite direction
2570            let active_item = self.active_item();
2571            cx.emit(Event::Split {
2572                direction: direction.opposite(),
2573                mode: SplitMode::EmptyPane,
2574            });
2575            // ensure that we focus the moved pane
2576            // in this case we know that the window is the same as the active_item
2577            if let Some(active_item) = active_item {
2578                cx.defer_in(window, move |_, window, cx| {
2579                    let focus_handle = active_item.item_focus_handle(cx);
2580                    window.focus(&focus_handle, cx);
2581                });
2582            }
2583        } else {
2584            cx.emit(Event::Split { direction, mode });
2585        }
2586    }
2587
2588    pub fn toolbar(&self) -> &Entity<Toolbar> {
2589        &self.toolbar
2590    }
2591
2592    pub fn handle_deleted_project_item(
2593        &mut self,
2594        entry_id: ProjectEntryId,
2595        window: &mut Window,
2596        cx: &mut Context<Pane>,
2597    ) -> Option<()> {
2598        let item_id = self.items().find_map(|item| {
2599            if item.buffer_kind(cx) == ItemBufferKind::Singleton
2600                && item.project_entry_ids(cx).as_slice() == [entry_id]
2601            {
2602                Some(item.item_id())
2603            } else {
2604                None
2605            }
2606        })?;
2607
2608        self.remove_item(item_id, false, true, window, cx);
2609        self.nav_history.remove_item(item_id);
2610
2611        Some(())
2612    }
2613
2614    fn update_toolbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2615        let active_item = self
2616            .items
2617            .get(self.active_item_index)
2618            .map(|item| item.as_ref());
2619        self.toolbar.update(cx, |toolbar, cx| {
2620            toolbar.set_active_item(active_item, window, cx);
2621        });
2622    }
2623
2624    fn update_status_bar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2625        let workspace = self.workspace.clone();
2626        let pane = cx.entity();
2627
2628        window.defer(cx, move |window, cx| {
2629            let Ok(status_bar) =
2630                workspace.read_with(cx, |workspace, _| workspace.status_bar.clone())
2631            else {
2632                return;
2633            };
2634
2635            status_bar.update(cx, move |status_bar, cx| {
2636                status_bar.set_active_pane(&pane, window, cx);
2637            });
2638        });
2639    }
2640
2641    fn entry_abs_path(&self, entry: ProjectEntryId, cx: &App) -> Option<PathBuf> {
2642        let worktree = self
2643            .workspace
2644            .upgrade()?
2645            .read(cx)
2646            .project()
2647            .read(cx)
2648            .worktree_for_entry(entry, cx)?
2649            .read(cx);
2650        let entry = worktree.entry_for_id(entry)?;
2651        Some(match &entry.canonical_path {
2652            Some(canonical_path) => canonical_path.to_path_buf(),
2653            None => worktree.absolutize(&entry.path),
2654        })
2655    }
2656
2657    pub fn icon_color(selected: bool) -> Color {
2658        if selected {
2659            Color::Default
2660        } else {
2661            Color::Muted
2662        }
2663    }
2664
2665    fn toggle_pin_tab(&mut self, _: &TogglePinTab, window: &mut Window, cx: &mut Context<Self>) {
2666        if self.items.is_empty() {
2667            return;
2668        }
2669        let active_tab_ix = self.active_item_index();
2670        if self.is_tab_pinned(active_tab_ix) {
2671            self.unpin_tab_at(active_tab_ix, window, cx);
2672        } else {
2673            self.pin_tab_at(active_tab_ix, window, cx);
2674        }
2675    }
2676
2677    fn unpin_all_tabs(&mut self, _: &UnpinAllTabs, window: &mut Window, cx: &mut Context<Self>) {
2678        if self.items.is_empty() {
2679            return;
2680        }
2681
2682        let pinned_item_ids = self.pinned_item_ids().into_iter().rev();
2683
2684        for pinned_item_id in pinned_item_ids {
2685            if let Some(ix) = self.index_for_item_id(pinned_item_id) {
2686                self.unpin_tab_at(ix, window, cx);
2687            }
2688        }
2689    }
2690
2691    fn pin_tab_at(&mut self, ix: usize, window: &mut Window, cx: &mut Context<Self>) {
2692        self.change_tab_pin_state(ix, PinOperation::Pin, window, cx);
2693    }
2694
2695    fn unpin_tab_at(&mut self, ix: usize, window: &mut Window, cx: &mut Context<Self>) {
2696        self.change_tab_pin_state(ix, PinOperation::Unpin, window, cx);
2697    }
2698
2699    fn change_tab_pin_state(
2700        &mut self,
2701        ix: usize,
2702        operation: PinOperation,
2703        window: &mut Window,
2704        cx: &mut Context<Self>,
2705    ) {
2706        maybe!({
2707            let pane = cx.entity();
2708
2709            let destination_index = match operation {
2710                PinOperation::Pin => self.pinned_tab_count.min(ix),
2711                PinOperation::Unpin => self.pinned_tab_count.checked_sub(1)?,
2712            };
2713
2714            let id = self.item_for_index(ix)?.item_id();
2715            let should_activate = ix == self.active_item_index;
2716
2717            if matches!(operation, PinOperation::Pin) {
2718                self.unpreview_item_if_preview(id);
2719            }
2720
2721            match operation {
2722                PinOperation::Pin => self.pinned_tab_count += 1,
2723                PinOperation::Unpin => self.pinned_tab_count -= 1,
2724            }
2725
2726            if ix == destination_index {
2727                cx.notify();
2728            } else {
2729                self.workspace
2730                    .update(cx, |_, cx| {
2731                        cx.defer_in(window, move |_, window, cx| {
2732                            move_item(
2733                                &pane,
2734                                &pane,
2735                                id,
2736                                destination_index,
2737                                should_activate,
2738                                window,
2739                                cx,
2740                            );
2741                        });
2742                    })
2743                    .ok()?;
2744            }
2745
2746            let event = match operation {
2747                PinOperation::Pin => Event::ItemPinned,
2748                PinOperation::Unpin => Event::ItemUnpinned,
2749            };
2750
2751            cx.emit(event);
2752
2753            Some(())
2754        });
2755    }
2756
2757    fn is_tab_pinned(&self, ix: usize) -> bool {
2758        self.pinned_tab_count > ix
2759    }
2760
2761    fn has_unpinned_tabs(&self) -> bool {
2762        self.pinned_tab_count < self.items.len()
2763    }
2764
2765    fn activate_unpinned_tab(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2766        if self.items.is_empty() {
2767            return;
2768        }
2769        let Some(index) = self
2770            .items()
2771            .enumerate()
2772            .find_map(|(index, _item)| (!self.is_tab_pinned(index)).then_some(index))
2773        else {
2774            return;
2775        };
2776        self.activate_item(index, true, true, window, cx);
2777    }
2778
2779    fn tab_icon_element(
2780        &self,
2781        item: &dyn ItemHandle,
2782        is_active: bool,
2783        window: &Window,
2784        cx: &App,
2785    ) -> Option<AnyElement> {
2786        let icon = item
2787            .tab_icon(window, cx)?
2788            .size(IconSize::Small)
2789            .color(Color::Muted);
2790
2791        let item_diagnostic = item
2792            .project_path(cx)
2793            .and_then(|project_path| self.diagnostics.get(&project_path));
2794
2795        let Some(diagnostic) = item_diagnostic else {
2796            return Some(icon.into_any_element());
2797        };
2798
2799        let knockout_item_color = if is_active {
2800            cx.theme().colors().tab_active_background
2801        } else {
2802            cx.theme().colors().tab_bar_background
2803        };
2804
2805        let (icon_decoration, icon_color) = if matches!(diagnostic, &DiagnosticSeverity::ERROR) {
2806            (IconDecorationKind::X, Color::Error)
2807        } else {
2808            (IconDecorationKind::Triangle, Color::Warning)
2809        };
2810
2811        Some(
2812            DecoratedIcon::new(
2813                icon,
2814                Some(
2815                    IconDecoration::new(icon_decoration, knockout_item_color, cx)
2816                        .color(icon_color.color(cx))
2817                        .position(Point {
2818                            x: px(-2.),
2819                            y: px(-2.),
2820                        }),
2821                ),
2822            )
2823            .into_any_element(),
2824        )
2825    }
2826
2827    fn render_tab(
2828        &self,
2829        ix: usize,
2830        item: &dyn ItemHandle,
2831        detail: usize,
2832        focus_handle: &FocusHandle,
2833        window: &mut Window,
2834        cx: &mut Context<Pane>,
2835    ) -> impl IntoElement + use<> {
2836        let is_active = ix == self.active_item_index;
2837        let is_preview = self
2838            .preview_item_id
2839            .map(|id| id == item.item_id())
2840            .unwrap_or(false);
2841
2842        let label = item.tab_content(
2843            TabContentParams {
2844                detail: Some(detail),
2845                selected: is_active,
2846                preview: is_preview,
2847                deemphasized: !self.has_focus(window, cx),
2848                max_title_len: None,
2849                truncate_title_middle: false,
2850            },
2851            window,
2852            cx,
2853        );
2854
2855        let icon = self.tab_icon_element(item, is_active, window, cx);
2856
2857        let settings = ItemSettings::get_global(cx);
2858        let close_side = &settings.close_position;
2859        let show_close_button = &settings.show_close_button;
2860        let indicator = render_item_indicator(item.boxed_clone(), cx);
2861        let tab_tooltip_content = item.tab_tooltip_content(cx);
2862        let item_id = item.item_id();
2863        let is_first_item = ix == 0;
2864        let is_last_item = ix == self.items.len() - 1;
2865        let is_pinned = self.is_tab_pinned(ix);
2866        let position_relative_to_active_item = ix.cmp(&self.active_item_index);
2867
2868        let read_only_toggle = |toggleable: bool| {
2869            IconButton::new("toggle_read_only", IconName::FileLock)
2870                .size(ButtonSize::None)
2871                .shape(IconButtonShape::Square)
2872                .icon_color(Color::Muted)
2873                .icon_size(IconSize::Small)
2874                .disabled(!toggleable)
2875                .tooltip(move |_, cx| {
2876                    if toggleable {
2877                        Tooltip::with_meta(
2878                            "Unlock File",
2879                            None,
2880                            "This will make this file editable",
2881                            cx,
2882                        )
2883                    } else {
2884                        Tooltip::with_meta("Locked File", None, "This file is read-only", cx)
2885                    }
2886                })
2887                .on_click(cx.listener(move |pane, _, window, cx| {
2888                    if let Some(item) = pane.item_for_index(ix) {
2889                        item.toggle_read_only(window, cx);
2890                    }
2891                }))
2892        };
2893
2894        let has_file_icon = icon.is_some();
2895
2896        let capability = item.capability(cx);
2897        let tab = Tab::new(ix)
2898            .position(if is_first_item {
2899                TabPosition::First
2900            } else if is_last_item {
2901                TabPosition::Last
2902            } else {
2903                TabPosition::Middle(position_relative_to_active_item)
2904            })
2905            .close_side(match close_side {
2906                ClosePosition::Left => ui::TabCloseSide::Start,
2907                ClosePosition::Right => ui::TabCloseSide::End,
2908            })
2909            .toggle_state(is_active)
2910            .on_click(cx.listener({
2911                let item_handle = item.boxed_clone();
2912                move |pane: &mut Self, event: &ClickEvent, window, cx| {
2913                    if event.click_count() > 1 {
2914                        pane.unpreview_item_if_preview(item_id);
2915                        let extra_actions = item_handle.tab_extra_context_menu_actions(window, cx);
2916                        if let Some((_, action)) = extra_actions
2917                            .into_iter()
2918                            .find(|(label, _)| label.as_ref() == "Rename")
2919                        {
2920                            // Dispatch action directly through the focus handle to avoid
2921                            // relay_action's intermediate focus step which can interfere
2922                            // with inline editors.
2923                            let focus_handle = item_handle.item_focus_handle(cx);
2924                            focus_handle.dispatch_action(&*action, window, cx);
2925                            return;
2926                        }
2927                    }
2928                    pane.activate_item(ix, true, true, window, cx)
2929                }
2930            }))
2931            .on_aux_click(
2932                cx.listener(move |pane: &mut Self, event: &ClickEvent, window, cx| {
2933                    if !event.is_middle_click() || is_pinned {
2934                        return;
2935                    }
2936
2937                    pane.close_item_by_id(item_id, SaveIntent::Close, window, cx)
2938                        .detach_and_log_err(cx);
2939                    cx.stop_propagation();
2940                }),
2941            )
2942            .on_drag(
2943                DraggedTab {
2944                    item: item.boxed_clone(),
2945                    pane: cx.entity(),
2946                    detail,
2947                    is_active,
2948                    ix,
2949                },
2950                |tab, _, _, cx| cx.new(|_| tab.clone()),
2951            )
2952            .drag_over::<DraggedTab>(move |tab, dragged_tab: &DraggedTab, _, cx| {
2953                let mut styled_tab = tab
2954                    .bg(cx.theme().colors().drop_target_background)
2955                    .border_color(cx.theme().colors().drop_target_border)
2956                    .border_0();
2957
2958                if ix < dragged_tab.ix {
2959                    styled_tab = styled_tab.border_l_2();
2960                } else if ix > dragged_tab.ix {
2961                    styled_tab = styled_tab.border_r_2();
2962                }
2963
2964                styled_tab
2965            })
2966            .drag_over::<DraggedSelection>(|tab, _, _, cx| {
2967                tab.bg(cx.theme().colors().drop_target_background)
2968            })
2969            .when_some(self.can_drop_predicate.clone(), |this, p| {
2970                this.can_drop(move |a, window, cx| p(a, window, cx))
2971            })
2972            .on_drop(
2973                cx.listener(move |this, dragged_tab: &DraggedTab, window, cx| {
2974                    this.drag_split_direction = None;
2975                    this.handle_tab_drop(dragged_tab, ix, false, window, cx)
2976                }),
2977            )
2978            .on_drop(
2979                cx.listener(move |this, selection: &DraggedSelection, window, cx| {
2980                    this.drag_split_direction = None;
2981                    this.handle_dragged_selection_drop(selection, Some(ix), window, cx)
2982                }),
2983            )
2984            .on_drop(cx.listener(move |this, paths, window, cx| {
2985                this.drag_split_direction = None;
2986                this.handle_external_paths_drop(paths, window, cx)
2987            }))
2988            .start_slot::<Indicator>(indicator)
2989            .map(|this| {
2990                let end_slot_action: &'static dyn Action;
2991                let end_slot_tooltip_text: &'static str;
2992                let end_slot = if is_pinned {
2993                    end_slot_action = &TogglePinTab;
2994                    end_slot_tooltip_text = "Unpin Tab";
2995                    IconButton::new("unpin tab", IconName::Pin)
2996                        .shape(IconButtonShape::Square)
2997                        .icon_color(Color::Muted)
2998                        .size(ButtonSize::None)
2999                        .icon_size(IconSize::Small)
3000                        .on_click(cx.listener(move |pane, _, window, cx| {
3001                            pane.unpin_tab_at(ix, window, cx);
3002                        }))
3003                } else {
3004                    end_slot_action = &CloseActiveItem {
3005                        save_intent: None,
3006                        close_pinned: false,
3007                    };
3008                    end_slot_tooltip_text = "Close Tab";
3009                    match show_close_button {
3010                        ShowCloseButton::Always => IconButton::new("close tab", IconName::Close),
3011                        ShowCloseButton::Hover => {
3012                            IconButton::new("close tab", IconName::Close).visible_on_hover("")
3013                        }
3014                        ShowCloseButton::Hidden => return this,
3015                    }
3016                    .shape(IconButtonShape::Square)
3017                    .icon_color(Color::Muted)
3018                    .size(ButtonSize::None)
3019                    .icon_size(IconSize::Small)
3020                    .on_click(cx.listener(move |pane, _, window, cx| {
3021                        pane.close_item_by_id(item_id, SaveIntent::Close, window, cx)
3022                            .detach_and_log_err(cx);
3023                    }))
3024                }
3025                .map(|this| {
3026                    if is_active {
3027                        let focus_handle = focus_handle.clone();
3028                        this.tooltip(move |window, cx| {
3029                            Tooltip::for_action_in(
3030                                end_slot_tooltip_text,
3031                                end_slot_action,
3032                                &window.focused(cx).unwrap_or_else(|| focus_handle.clone()),
3033                                cx,
3034                            )
3035                        })
3036                    } else {
3037                        this.tooltip(Tooltip::text(end_slot_tooltip_text))
3038                    }
3039                });
3040                this.end_slot(end_slot)
3041            })
3042            .child(
3043                h_flex()
3044                    .id(("pane-tab-content", ix))
3045                    .gap_1()
3046                    .children(if let Some(icon) = icon {
3047                        Some(icon)
3048                    } else if !capability.editable() {
3049                        Some(read_only_toggle(capability == Capability::Read).into_any_element())
3050                    } else {
3051                        None
3052                    })
3053                    .child(label)
3054                    .map(|this| match tab_tooltip_content {
3055                        Some(TabTooltipContent::Text(text)) => {
3056                            if capability.editable() {
3057                                this.tooltip(Tooltip::text(text))
3058                            } else {
3059                                this.tooltip(move |_, cx| {
3060                                    let text = text.clone();
3061                                    Tooltip::with_meta(text, None, "Read-Only File", cx)
3062                                })
3063                            }
3064                        }
3065                        Some(TabTooltipContent::Custom(element_fn)) => {
3066                            this.tooltip(move |window, cx| element_fn(window, cx))
3067                        }
3068                        None => this,
3069                    })
3070                    .when(capability == Capability::Read && has_file_icon, |this| {
3071                        this.child(read_only_toggle(true))
3072                    }),
3073            );
3074
3075        let single_entry_to_resolve = (self.items[ix].buffer_kind(cx) == ItemBufferKind::Singleton)
3076            .then(|| self.items[ix].project_entry_ids(cx).get(0).copied())
3077            .flatten();
3078
3079        let total_items = self.items.len();
3080        let has_multibuffer_items = self
3081            .items
3082            .iter()
3083            .any(|item| item.buffer_kind(cx) == ItemBufferKind::Multibuffer);
3084        let has_items_to_left = ix > 0;
3085        let has_items_to_right = ix < total_items - 1;
3086        let has_clean_items = self.items.iter().any(|item| !item.is_dirty(cx));
3087        let is_pinned = self.is_tab_pinned(ix);
3088
3089        let pane = cx.entity().downgrade();
3090        let menu_context = item.item_focus_handle(cx);
3091        let item_handle = item.boxed_clone();
3092
3093        right_click_menu(ix)
3094            .trigger(|_, _, _| tab)
3095            .menu(move |window, cx| {
3096                let pane = pane.clone();
3097                let menu_context = menu_context.clone();
3098                let extra_actions = item_handle.tab_extra_context_menu_actions(window, cx);
3099                ContextMenu::build(window, cx, move |mut menu, window, cx| {
3100                    let close_active_item_action = CloseActiveItem {
3101                        save_intent: None,
3102                        close_pinned: true,
3103                    };
3104                    let close_inactive_items_action = CloseOtherItems {
3105                        save_intent: None,
3106                        close_pinned: false,
3107                    };
3108                    let close_multibuffers_action = CloseMultibufferItems {
3109                        save_intent: None,
3110                        close_pinned: false,
3111                    };
3112                    let close_items_to_the_left_action = CloseItemsToTheLeft {
3113                        close_pinned: false,
3114                    };
3115                    let close_items_to_the_right_action = CloseItemsToTheRight {
3116                        close_pinned: false,
3117                    };
3118                    let close_clean_items_action = CloseCleanItems {
3119                        close_pinned: false,
3120                    };
3121                    let close_all_items_action = CloseAllItems {
3122                        save_intent: None,
3123                        close_pinned: false,
3124                    };
3125                    if let Some(pane) = pane.upgrade() {
3126                        menu = menu
3127                            .entry(
3128                                "Close",
3129                                Some(Box::new(close_active_item_action)),
3130                                window.handler_for(&pane, move |pane, window, cx| {
3131                                    pane.close_item_by_id(item_id, SaveIntent::Close, window, cx)
3132                                        .detach_and_log_err(cx);
3133                                }),
3134                            )
3135                            .item(ContextMenuItem::Entry(
3136                                ContextMenuEntry::new("Close Others")
3137                                    .action(Box::new(close_inactive_items_action.clone()))
3138                                    .disabled(total_items == 1)
3139                                    .handler(window.handler_for(&pane, move |pane, window, cx| {
3140                                        pane.close_other_items(
3141                                            &close_inactive_items_action,
3142                                            Some(item_id),
3143                                            window,
3144                                            cx,
3145                                        )
3146                                        .detach_and_log_err(cx);
3147                                    })),
3148                            ))
3149                            // We make this optional, instead of using disabled as to not overwhelm the context menu unnecessarily
3150                            .extend(has_multibuffer_items.then(|| {
3151                                ContextMenuItem::Entry(
3152                                    ContextMenuEntry::new("Close Multibuffers")
3153                                        .action(Box::new(close_multibuffers_action.clone()))
3154                                        .handler(window.handler_for(
3155                                            &pane,
3156                                            move |pane, window, cx| {
3157                                                pane.close_multibuffer_items(
3158                                                    &close_multibuffers_action,
3159                                                    window,
3160                                                    cx,
3161                                                )
3162                                                .detach_and_log_err(cx);
3163                                            },
3164                                        )),
3165                                )
3166                            }))
3167                            .separator()
3168                            .item(ContextMenuItem::Entry(
3169                                ContextMenuEntry::new("Close Left")
3170                                    .action(Box::new(close_items_to_the_left_action.clone()))
3171                                    .disabled(!has_items_to_left)
3172                                    .handler(window.handler_for(&pane, move |pane, window, cx| {
3173                                        pane.close_items_to_the_left_by_id(
3174                                            Some(item_id),
3175                                            &close_items_to_the_left_action,
3176                                            window,
3177                                            cx,
3178                                        )
3179                                        .detach_and_log_err(cx);
3180                                    })),
3181                            ))
3182                            .item(ContextMenuItem::Entry(
3183                                ContextMenuEntry::new("Close Right")
3184                                    .action(Box::new(close_items_to_the_right_action.clone()))
3185                                    .disabled(!has_items_to_right)
3186                                    .handler(window.handler_for(&pane, move |pane, window, cx| {
3187                                        pane.close_items_to_the_right_by_id(
3188                                            Some(item_id),
3189                                            &close_items_to_the_right_action,
3190                                            window,
3191                                            cx,
3192                                        )
3193                                        .detach_and_log_err(cx);
3194                                    })),
3195                            ))
3196                            .separator()
3197                            .item(ContextMenuItem::Entry(
3198                                ContextMenuEntry::new("Close Clean")
3199                                    .action(Box::new(close_clean_items_action.clone()))
3200                                    .disabled(!has_clean_items)
3201                                    .handler(window.handler_for(&pane, move |pane, window, cx| {
3202                                        pane.close_clean_items(
3203                                            &close_clean_items_action,
3204                                            window,
3205                                            cx,
3206                                        )
3207                                        .detach_and_log_err(cx)
3208                                    })),
3209                            ))
3210                            .entry(
3211                                "Close All",
3212                                Some(Box::new(close_all_items_action.clone())),
3213                                window.handler_for(&pane, move |pane, window, cx| {
3214                                    pane.close_all_items(&close_all_items_action, window, cx)
3215                                        .detach_and_log_err(cx)
3216                                }),
3217                            );
3218
3219                        let pin_tab_entries = |menu: ContextMenu| {
3220                            menu.separator().map(|this| {
3221                                if is_pinned {
3222                                    this.entry(
3223                                        "Unpin Tab",
3224                                        Some(TogglePinTab.boxed_clone()),
3225                                        window.handler_for(&pane, move |pane, window, cx| {
3226                                            pane.unpin_tab_at(ix, window, cx);
3227                                        }),
3228                                    )
3229                                } else {
3230                                    this.entry(
3231                                        "Pin Tab",
3232                                        Some(TogglePinTab.boxed_clone()),
3233                                        window.handler_for(&pane, move |pane, window, cx| {
3234                                            pane.pin_tab_at(ix, window, cx);
3235                                        }),
3236                                    )
3237                                }
3238                            })
3239                        };
3240
3241                        if capability != Capability::ReadOnly {
3242                            let read_only_label = if capability.editable() {
3243                                "Make File Read-Only"
3244                            } else {
3245                                "Make File Editable"
3246                            };
3247                            menu = menu.separator().entry(
3248                                read_only_label,
3249                                None,
3250                                window.handler_for(&pane, move |pane, window, cx| {
3251                                    if let Some(item) = pane.item_for_index(ix) {
3252                                        item.toggle_read_only(window, cx);
3253                                    }
3254                                }),
3255                            );
3256                        }
3257
3258                        if let Some(entry) = single_entry_to_resolve {
3259                            let project_path = pane
3260                                .read(cx)
3261                                .item_for_entry(entry, cx)
3262                                .and_then(|item| item.project_path(cx));
3263                            let worktree = project_path.as_ref().and_then(|project_path| {
3264                                pane.read(cx)
3265                                    .project
3266                                    .upgrade()?
3267                                    .read(cx)
3268                                    .worktree_for_id(project_path.worktree_id, cx)
3269                            });
3270                            let has_relative_path = worktree.as_ref().is_some_and(|worktree| {
3271                                worktree
3272                                    .read(cx)
3273                                    .root_entry()
3274                                    .is_some_and(|entry| entry.is_dir())
3275                            });
3276
3277                            let entry_abs_path = pane.read(cx).entry_abs_path(entry, cx);
3278                            let reveal_path = entry_abs_path.clone();
3279                            let parent_abs_path = entry_abs_path
3280                                .as_deref()
3281                                .and_then(|abs_path| Some(abs_path.parent()?.to_path_buf()));
3282                            let relative_path = project_path
3283                                .map(|project_path| project_path.path)
3284                                .filter(|_| has_relative_path);
3285
3286                            let visible_in_project_panel = relative_path.is_some()
3287                                && worktree.is_some_and(|worktree| worktree.read(cx).is_visible());
3288                            let is_local = pane.read(cx).project.upgrade().is_some_and(|project| {
3289                                let project = project.read(cx);
3290                                project.is_local() || project.is_via_wsl_with_host_interop(cx)
3291                            });
3292                            let is_remote = pane
3293                                .read(cx)
3294                                .project
3295                                .upgrade()
3296                                .is_some_and(|project| project.read(cx).is_remote());
3297
3298                            let entry_id = entry.to_proto();
3299
3300                            menu = menu
3301                                .separator()
3302                                .when_some(entry_abs_path, |menu, abs_path| {
3303                                    menu.entry(
3304                                        "Copy Path",
3305                                        Some(Box::new(zed_actions::workspace::CopyPath)),
3306                                        window.handler_for(&pane, move |_, _, cx| {
3307                                            cx.write_to_clipboard(ClipboardItem::new_string(
3308                                                abs_path.to_string_lossy().into_owned(),
3309                                            ));
3310                                        }),
3311                                    )
3312                                })
3313                                .when_some(relative_path, |menu, relative_path| {
3314                                    menu.entry(
3315                                        "Copy Relative Path",
3316                                        Some(Box::new(zed_actions::workspace::CopyRelativePath)),
3317                                        window.handler_for(&pane, move |this, _, cx| {
3318                                            let Some(project) = this.project.upgrade() else {
3319                                                return;
3320                                            };
3321                                            let path_style = project
3322                                                .update(cx, |project, cx| project.path_style(cx));
3323                                            cx.write_to_clipboard(ClipboardItem::new_string(
3324                                                relative_path.display(path_style).to_string(),
3325                                            ));
3326                                        }),
3327                                    )
3328                                })
3329                                .when(is_local, |menu| {
3330                                    menu.when_some(reveal_path, |menu, reveal_path| {
3331                                        menu.separator().entry(
3332                                            ui::utils::reveal_in_file_manager_label(is_remote),
3333                                            Some(Box::new(
3334                                                zed_actions::editor::RevealInFileManager,
3335                                            )),
3336                                            window.handler_for(&pane, move |pane, _, cx| {
3337                                                if let Some(project) = pane.project.upgrade() {
3338                                                    project.update(cx, |project, cx| {
3339                                                        project.reveal_path(&reveal_path, cx);
3340                                                    });
3341                                                } else {
3342                                                    cx.reveal_path(&reveal_path);
3343                                                }
3344                                            }),
3345                                        )
3346                                    })
3347                                })
3348                                .map(pin_tab_entries)
3349                                .when(visible_in_project_panel, |menu| {
3350                                    menu.entry(
3351                                        "Reveal In Project Panel",
3352                                        Some(Box::new(RevealInProjectPanel::default())),
3353                                        window.handler_for(&pane, move |pane, _, cx| {
3354                                            pane.project
3355                                                .update(cx, |_, cx| {
3356                                                    cx.emit(project::Event::RevealInProjectPanel(
3357                                                        ProjectEntryId::from_proto(entry_id),
3358                                                    ))
3359                                                })
3360                                                .ok();
3361                                        }),
3362                                    )
3363                                })
3364                                .when_some(parent_abs_path, |menu, parent_abs_path| {
3365                                    menu.entry(
3366                                        "Open in Terminal",
3367                                        Some(Box::new(OpenInTerminal)),
3368                                        window.handler_for(&pane, move |_, window, cx| {
3369                                            window.dispatch_action(
3370                                                OpenTerminal {
3371                                                    working_directory: parent_abs_path.clone(),
3372                                                    local: false,
3373                                                }
3374                                                .boxed_clone(),
3375                                                cx,
3376                                            );
3377                                        }),
3378                                    )
3379                                });
3380                        } else {
3381                            menu = menu.map(pin_tab_entries);
3382                        }
3383                    };
3384
3385                    // Add custom item-specific actions
3386                    if !extra_actions.is_empty() {
3387                        menu = menu.separator();
3388                        for (label, action) in extra_actions {
3389                            menu = menu.action(label, action);
3390                        }
3391                    }
3392
3393                    menu.context(menu_context)
3394                })
3395            })
3396    }
3397
3398    fn render_tab_bar(&mut self, window: &mut Window, cx: &mut Context<Pane>) -> AnyElement {
3399        if self.workspace.upgrade().is_none() {
3400            return gpui::Empty.into_any();
3401        }
3402
3403        let focus_handle = self.focus_handle.clone();
3404
3405        let navigate_backward = IconButton::new("navigate_backward", IconName::ArrowLeft)
3406            .icon_size(IconSize::Small)
3407            .on_click({
3408                let entity = cx.entity();
3409                move |_, window, cx| {
3410                    entity.update(cx, |pane, cx| {
3411                        pane.navigate_backward(&Default::default(), window, cx)
3412                    })
3413                }
3414            })
3415            .disabled(!self.can_navigate_backward())
3416            .tooltip({
3417                let focus_handle = focus_handle.clone();
3418                move |window, cx| {
3419                    Tooltip::for_action_in(
3420                        "Go Back",
3421                        &GoBack,
3422                        &window.focused(cx).unwrap_or_else(|| focus_handle.clone()),
3423                        cx,
3424                    )
3425                }
3426            });
3427
3428        let navigate_forward = IconButton::new("navigate_forward", IconName::ArrowRight)
3429            .icon_size(IconSize::Small)
3430            .on_click({
3431                let entity = cx.entity();
3432                move |_, window, cx| {
3433                    entity.update(cx, |pane, cx| {
3434                        pane.navigate_forward(&Default::default(), window, cx)
3435                    })
3436                }
3437            })
3438            .disabled(!self.can_navigate_forward())
3439            .tooltip({
3440                let focus_handle = focus_handle.clone();
3441                move |window, cx| {
3442                    Tooltip::for_action_in(
3443                        "Go Forward",
3444                        &GoForward,
3445                        &window.focused(cx).unwrap_or_else(|| focus_handle.clone()),
3446                        cx,
3447                    )
3448                }
3449            });
3450
3451        let mut tab_items = self
3452            .items
3453            .iter()
3454            .enumerate()
3455            .zip(tab_details(&self.items, window, cx))
3456            .map(|((ix, item), detail)| {
3457                self.render_tab(ix, &**item, detail, &focus_handle, window, cx)
3458                    .into_any_element()
3459            })
3460            .collect::<Vec<_>>();
3461        let tab_count = tab_items.len();
3462        if self.is_tab_pinned(tab_count) {
3463            log::warn!(
3464                "Pinned tab count ({}) exceeds actual tab count ({}). \
3465                This should not happen. If possible, add reproduction steps, \
3466                in a comment, to https://github.com/OpenAgentsInc/omega/issues",
3467                self.pinned_tab_count,
3468                tab_count
3469            );
3470            self.pinned_tab_count = tab_count;
3471        }
3472        let unpinned_tabs = tab_items.split_off(self.pinned_tab_count);
3473        let pinned_tabs = tab_items;
3474
3475        let tab_bar_settings = TabBarSettings::get_global(cx);
3476        let use_separate_rows = tab_bar_settings.show_pinned_tabs_in_separate_row;
3477
3478        if use_separate_rows && !pinned_tabs.is_empty() && !unpinned_tabs.is_empty() {
3479            self.render_two_row_tab_bar(
3480                pinned_tabs,
3481                unpinned_tabs,
3482                tab_count,
3483                navigate_backward,
3484                navigate_forward,
3485                window,
3486                cx,
3487            )
3488        } else {
3489            self.render_single_row_tab_bar(
3490                pinned_tabs,
3491                unpinned_tabs,
3492                tab_count,
3493                navigate_backward,
3494                navigate_forward,
3495                window,
3496                cx,
3497            )
3498        }
3499    }
3500
3501    fn configure_tab_bar_start(
3502        &mut self,
3503        tab_bar: TabBar,
3504        navigate_backward: IconButton,
3505        navigate_forward: IconButton,
3506        window: &mut Window,
3507        cx: &mut Context<Pane>,
3508    ) -> TabBar {
3509        tab_bar
3510            .when(
3511                self.display_nav_history_buttons.unwrap_or_default(),
3512                |tab_bar| {
3513                    tab_bar
3514                        .start_child(navigate_backward)
3515                        .start_child(navigate_forward)
3516                },
3517            )
3518            .map(|tab_bar| {
3519                if self.show_tab_bar_buttons {
3520                    let render_tab_buttons = self.render_tab_bar_buttons.clone();
3521                    let (left_children, right_children) = render_tab_buttons(self, window, cx);
3522                    tab_bar
3523                        .start_children(left_children)
3524                        .end_children(right_children)
3525                } else {
3526                    tab_bar
3527                }
3528            })
3529    }
3530
3531    fn render_single_row_tab_bar(
3532        &mut self,
3533        pinned_tabs: Vec<AnyElement>,
3534        unpinned_tabs: Vec<AnyElement>,
3535        tab_count: usize,
3536        navigate_backward: IconButton,
3537        navigate_forward: IconButton,
3538        window: &mut Window,
3539        cx: &mut Context<Pane>,
3540    ) -> AnyElement {
3541        let tab_bar = self
3542            .configure_tab_bar_start(
3543                TabBar::new("tab_bar"),
3544                navigate_backward,
3545                navigate_forward,
3546                window,
3547                cx,
3548            )
3549            .children(pinned_tabs.len().ne(&0).then(|| {
3550                let max_scroll = self.tab_bar_scroll_handle.max_offset().x;
3551                // We need to check both because offset returns delta values even when the scroll handle is not scrollable
3552                let is_scrolled = self.tab_bar_scroll_handle.offset().x < px(0.);
3553                // Avoid flickering when max_offset is very small (< 2px).
3554                // The border adds 1-2px which can push max_offset back to 0, creating a loop.
3555                let is_scrollable = max_scroll > px(2.0);
3556                let has_active_unpinned_tab = self.active_item_index >= self.pinned_tab_count;
3557                h_flex()
3558                    .children(pinned_tabs)
3559                    .when(is_scrollable && is_scrolled, |this| {
3560                        this.when(has_active_unpinned_tab, |this| this.border_r_2())
3561                            .when(!has_active_unpinned_tab, |this| this.border_r_1())
3562                            .border_color(cx.theme().colors().border)
3563                    })
3564            }))
3565            .child(self.render_unpinned_tabs_container(unpinned_tabs, tab_count, cx));
3566        tab_bar.into_any_element()
3567    }
3568
3569    fn render_two_row_tab_bar(
3570        &mut self,
3571        pinned_tabs: Vec<AnyElement>,
3572        unpinned_tabs: Vec<AnyElement>,
3573        tab_count: usize,
3574        navigate_backward: IconButton,
3575        navigate_forward: IconButton,
3576        window: &mut Window,
3577        cx: &mut Context<Pane>,
3578    ) -> AnyElement {
3579        let pinned_tab_bar = self
3580            .configure_tab_bar_start(
3581                TabBar::new("pinned_tab_bar"),
3582                navigate_backward,
3583                navigate_forward,
3584                window,
3585                cx,
3586            )
3587            .child(
3588                h_flex()
3589                    .id("pinned_tabs_row")
3590                    .debug_selector(|| "pinned_tabs_row".into())
3591                    .overflow_x_scroll()
3592                    .w_full()
3593                    .children(pinned_tabs)
3594                    .child(self.render_pinned_tab_bar_drop_target(cx)),
3595            );
3596        v_flex()
3597            .w_full()
3598            .flex_none()
3599            .child(pinned_tab_bar)
3600            .child(
3601                TabBar::new("unpinned_tab_bar").child(self.render_unpinned_tabs_container(
3602                    unpinned_tabs,
3603                    tab_count,
3604                    cx,
3605                )),
3606            )
3607            .into_any_element()
3608    }
3609
3610    fn render_unpinned_tabs_container(
3611        &mut self,
3612        unpinned_tabs: Vec<AnyElement>,
3613        tab_count: usize,
3614        cx: &mut Context<Pane>,
3615    ) -> impl IntoElement {
3616        h_flex()
3617            .id("unpinned tabs")
3618            .overflow_x_scroll()
3619            .w_full()
3620            .track_scroll(&self.tab_bar_scroll_handle)
3621            .on_scroll_wheel(cx.listener(|this, _, _, _| {
3622                this.suppress_scroll = true;
3623            }))
3624            .children(unpinned_tabs)
3625            .child(self.render_tab_bar_drop_target(tab_count, cx))
3626    }
3627
3628    fn render_tab_bar_drop_target(
3629        &self,
3630        tab_count: usize,
3631        cx: &mut Context<Pane>,
3632    ) -> impl IntoElement {
3633        div()
3634            .id("tab_bar_drop_target")
3635            .min_w_6()
3636            .h(Tab::container_height(cx))
3637            .flex_grow_1()
3638            // HACK: This empty child is currently necessary to force the drop target to appear
3639            // despite us setting a min width above.
3640            .child("")
3641            .drag_over::<DraggedTab>(|bar, _, _, cx| {
3642                bar.bg(cx.theme().colors().drop_target_background)
3643            })
3644            .drag_over::<DraggedSelection>(|bar, _, _, cx| {
3645                bar.bg(cx.theme().colors().drop_target_background)
3646            })
3647            .on_drop(
3648                cx.listener(move |this, dragged_tab: &DraggedTab, window, cx| {
3649                    this.drag_split_direction = None;
3650                    this.handle_tab_drop(dragged_tab, this.items.len(), false, window, cx)
3651                }),
3652            )
3653            .on_drop(
3654                cx.listener(move |this, selection: &DraggedSelection, window, cx| {
3655                    this.drag_split_direction = None;
3656                    this.handle_project_entry_drop(
3657                        &selection.active_selection.entry_id,
3658                        Some(tab_count),
3659                        window,
3660                        cx,
3661                    )
3662                }),
3663            )
3664            .on_drop(cx.listener(move |this, paths, window, cx| {
3665                this.drag_split_direction = None;
3666                this.handle_external_paths_drop(paths, window, cx)
3667            }))
3668            .on_click(cx.listener(move |this, event: &ClickEvent, window, cx| {
3669                if event.click_count() == 2 {
3670                    window.dispatch_action(this.double_click_dispatch_action.boxed_clone(), cx);
3671                }
3672            }))
3673    }
3674
3675    fn render_pinned_tab_bar_drop_target(&self, cx: &mut Context<Pane>) -> impl IntoElement {
3676        div()
3677            .id("pinned_tabs_border")
3678            .debug_selector(|| "pinned_tabs_border".into())
3679            .min_w_6()
3680            .h(Tab::container_height(cx))
3681            .flex_grow_1()
3682            .border_l_1()
3683            .border_color(cx.theme().colors().border)
3684            // HACK: This empty child is currently necessary to force the drop target to appear
3685            // despite us setting a min width above.
3686            .child("")
3687            .drag_over::<DraggedTab>(|bar, _, _, cx| {
3688                bar.bg(cx.theme().colors().drop_target_background)
3689            })
3690            .drag_over::<DraggedSelection>(|bar, _, _, cx| {
3691                bar.bg(cx.theme().colors().drop_target_background)
3692            })
3693            .on_drop(
3694                cx.listener(move |this, dragged_tab: &DraggedTab, window, cx| {
3695                    this.drag_split_direction = None;
3696                    this.handle_pinned_tab_bar_drop(dragged_tab, window, cx)
3697                }),
3698            )
3699            .on_drop(
3700                cx.listener(move |this, selection: &DraggedSelection, window, cx| {
3701                    this.drag_split_direction = None;
3702                    this.handle_project_entry_drop(
3703                        &selection.active_selection.entry_id,
3704                        Some(this.pinned_tab_count),
3705                        window,
3706                        cx,
3707                    )
3708                }),
3709            )
3710            .on_drop(cx.listener(move |this, paths, window, cx| {
3711                this.drag_split_direction = None;
3712                this.handle_external_paths_drop(paths, window, cx)
3713            }))
3714            .on_click(cx.listener(move |this, event: &ClickEvent, window, cx| {
3715                if event.click_count() == 2 {
3716                    window.dispatch_action(this.double_click_dispatch_action.boxed_clone(), cx);
3717                }
3718            }))
3719    }
3720
3721    pub fn render_menu_overlay(menu: &Entity<ContextMenu>) -> Div {
3722        div().absolute().bottom_0().right_0().size_0().child(
3723            deferred(anchored().anchor(Anchor::TopRight).child(menu.clone())).with_priority(1),
3724        )
3725    }
3726
3727    pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut Context<Self>) {
3728        self.zoomed = zoomed;
3729        cx.notify();
3730    }
3731
3732    pub fn is_zoomed(&self) -> bool {
3733        self.zoomed
3734    }
3735
3736    fn handle_drag_move<T: 'static>(
3737        &mut self,
3738        event: &DragMoveEvent<T>,
3739        window: &mut Window,
3740        cx: &mut Context<Self>,
3741    ) {
3742        let can_split_predicate = self.can_split_predicate.take();
3743        let can_split = match &can_split_predicate {
3744            Some(can_split_predicate) => {
3745                can_split_predicate(self, event.dragged_item(), window, cx)
3746            }
3747            None => false,
3748        };
3749        self.can_split_predicate = can_split_predicate;
3750        if !can_split {
3751            return;
3752        }
3753
3754        let rect = event.bounds.size;
3755
3756        let size = event.bounds.size.width.min(event.bounds.size.height)
3757            * WorkspaceSettings::get_global(cx).drop_target_size;
3758
3759        let relative_cursor = Point::new(
3760            event.event.position.x - event.bounds.left(),
3761            event.event.position.y - event.bounds.top(),
3762        );
3763
3764        let direction = if relative_cursor.x < size
3765            || relative_cursor.x > rect.width - size
3766            || relative_cursor.y < size
3767            || relative_cursor.y > rect.height - size
3768        {
3769            [
3770                SplitDirection::Up,
3771                SplitDirection::Right,
3772                SplitDirection::Down,
3773                SplitDirection::Left,
3774            ]
3775            .iter()
3776            .min_by_key(|side| match side {
3777                SplitDirection::Up => relative_cursor.y,
3778                SplitDirection::Right => rect.width - relative_cursor.x,
3779                SplitDirection::Down => rect.height - relative_cursor.y,
3780                SplitDirection::Left => relative_cursor.x,
3781            })
3782            .cloned()
3783        } else {
3784            None
3785        };
3786
3787        if direction != self.drag_split_direction {
3788            self.drag_split_direction = direction;
3789        }
3790    }
3791
3792    pub fn handle_tab_drop(
3793        &mut self,
3794        dragged_tab: &DraggedTab,
3795        ix: usize,
3796        is_pane_target: bool,
3797        window: &mut Window,
3798        cx: &mut Context<Self>,
3799    ) {
3800        if is_pane_target
3801            && ix == self.active_item_index
3802            && let Some(active_item) = self.active_item()
3803            && active_item.handle_drop(self, dragged_tab, window, cx)
3804        {
3805            return;
3806        }
3807
3808        let mut to_pane = cx.entity();
3809        let split_direction = self.drag_split_direction;
3810        let item_id = dragged_tab.item.item_id();
3811        self.unpreview_item_if_preview(item_id);
3812
3813        let is_clone = cfg!(target_os = "macos") && window.modifiers().alt
3814            || cfg!(not(target_os = "macos")) && window.modifiers().control;
3815
3816        let from_pane = dragged_tab.pane.clone();
3817
3818        self.workspace
3819            .update(cx, |_, cx| {
3820                cx.defer_in(window, move |workspace, window, cx| {
3821                    if let Some(split_direction) = split_direction {
3822                        to_pane = workspace.split_pane(to_pane, split_direction, window, cx);
3823                    }
3824                    let database_id = workspace.database_id();
3825                    let was_pinned_in_from_pane = from_pane.read_with(cx, |pane, _| {
3826                        pane.index_for_item_id(item_id)
3827                            .is_some_and(|ix| pane.is_tab_pinned(ix))
3828                    });
3829                    let to_pane_old_length = to_pane.read(cx).items.len();
3830                    if is_clone {
3831                        let Some(item) = from_pane
3832                            .read(cx)
3833                            .items()
3834                            .find(|item| item.item_id() == item_id)
3835                            .cloned()
3836                        else {
3837                            return;
3838                        };
3839                        if item.can_split(cx) {
3840                            let task = item.clone_on_split(database_id, window, cx);
3841                            let to_pane = to_pane.downgrade();
3842                            cx.spawn_in(window, async move |_, cx| {
3843                                if let Some(item) = task.await {
3844                                    to_pane
3845                                        .update_in(cx, |pane, window, cx| {
3846                                            pane.add_item(item, true, true, None, window, cx)
3847                                        })
3848                                        .ok();
3849                                }
3850                            })
3851                            .detach();
3852                        } else {
3853                            move_item(&from_pane, &to_pane, item_id, ix, true, window, cx);
3854                        }
3855                    } else {
3856                        move_item(&from_pane, &to_pane, item_id, ix, true, window, cx);
3857                    }
3858                    to_pane.update(cx, |this, _| {
3859                        if to_pane == from_pane {
3860                            let actual_ix = this
3861                                .items
3862                                .iter()
3863                                .position(|item| item.item_id() == item_id)
3864                                .unwrap_or(0);
3865
3866                            let is_pinned_in_to_pane = this.is_tab_pinned(actual_ix);
3867
3868                            if !was_pinned_in_from_pane && is_pinned_in_to_pane {
3869                                this.pinned_tab_count += 1;
3870                            } else if was_pinned_in_from_pane && !is_pinned_in_to_pane {
3871                                this.pinned_tab_count -= 1;
3872                            }
3873                        } else if this.items.len() >= to_pane_old_length {
3874                            let is_pinned_in_to_pane = this.is_tab_pinned(ix);
3875                            let item_created_pane = to_pane_old_length == 0;
3876                            let is_first_position = ix == 0;
3877                            let was_dropped_at_beginning = item_created_pane || is_first_position;
3878                            let should_remain_pinned = is_pinned_in_to_pane
3879                                || (was_pinned_in_from_pane && was_dropped_at_beginning);
3880
3881                            if should_remain_pinned {
3882                                this.pinned_tab_count += 1;
3883                            }
3884                        }
3885                    });
3886                });
3887            })
3888            .log_err();
3889    }
3890
3891    fn handle_pinned_tab_bar_drop(
3892        &mut self,
3893        dragged_tab: &DraggedTab,
3894        window: &mut Window,
3895        cx: &mut Context<Self>,
3896    ) {
3897        let item_id = dragged_tab.item.item_id();
3898        let pinned_count = self.pinned_tab_count;
3899
3900        self.handle_tab_drop(dragged_tab, pinned_count, false, window, cx);
3901
3902        let to_pane = cx.entity();
3903
3904        self.workspace
3905            .update(cx, |_, cx| {
3906                cx.defer_in(window, move |_, _, cx| {
3907                    to_pane.update(cx, |this, cx| {
3908                        if let Some(actual_ix) = this.index_for_item_id(item_id) {
3909                            // If the tab ended up at or after pinned_tab_count, it's not pinned
3910                            // so we pin it now
3911                            if actual_ix >= this.pinned_tab_count {
3912                                let was_active = this.active_item_index == actual_ix;
3913                                let destination_ix = this.pinned_tab_count;
3914
3915                                // Move item to pinned area if needed
3916                                if actual_ix != destination_ix {
3917                                    let item = this.items.remove(actual_ix);
3918                                    this.items.insert(destination_ix, item);
3919
3920                                    // Update active_item_index to follow the moved item
3921                                    if was_active {
3922                                        this.active_item_index = destination_ix;
3923                                    } else if this.active_item_index > actual_ix
3924                                        && this.active_item_index <= destination_ix
3925                                    {
3926                                        // Item moved left past the active item
3927                                        this.active_item_index -= 1;
3928                                    } else if this.active_item_index >= destination_ix
3929                                        && this.active_item_index < actual_ix
3930                                    {
3931                                        // Item moved right past the active item
3932                                        this.active_item_index += 1;
3933                                    }
3934                                }
3935                                this.pinned_tab_count += 1;
3936                                cx.notify();
3937                            }
3938                        }
3939                    });
3940                });
3941            })
3942            .log_err();
3943    }
3944
3945    fn handle_dragged_selection_drop(
3946        &mut self,
3947        dragged_selection: &DraggedSelection,
3948        dragged_onto: Option<usize>,
3949        window: &mut Window,
3950        cx: &mut Context<Self>,
3951    ) {
3952        if let Some(active_item) = self.active_item()
3953            && active_item.handle_drop(self, dragged_selection, window, cx)
3954        {
3955            return;
3956        }
3957
3958        self.handle_project_entry_drop(
3959            &dragged_selection.active_selection.entry_id,
3960            dragged_onto,
3961            window,
3962            cx,
3963        );
3964    }
3965
3966    fn handle_project_entry_drop(
3967        &mut self,
3968        project_entry_id: &ProjectEntryId,
3969        target: Option<usize>,
3970        window: &mut Window,
3971        cx: &mut Context<Self>,
3972    ) {
3973        if let Some(active_item) = self.active_item()
3974            && active_item.handle_drop(self, project_entry_id, window, cx)
3975        {
3976            return;
3977        }
3978
3979        let mut to_pane = cx.entity();
3980        let split_direction = self.drag_split_direction;
3981        let project_entry_id = *project_entry_id;
3982        self.workspace
3983            .update(cx, |_, cx| {
3984                cx.defer_in(window, move |workspace, window, cx| {
3985                    if let Some(project_path) = workspace
3986                        .project()
3987                        .read(cx)
3988                        .path_for_entry(project_entry_id, cx)
3989                    {
3990                        let load_path_task = workspace.load_path(project_path.clone(), window, cx);
3991                        cx.spawn_in(window, async move |workspace, mut cx| {
3992                            if let Some((project_entry_id, build_item)) = load_path_task
3993                                .await
3994                                .notify_workspace_async_err(workspace.clone(), &mut cx)
3995                            {
3996                                let (to_pane, new_item_handle) = workspace
3997                                    .update_in(cx, |workspace, window, cx| {
3998                                        if let Some(split_direction) = split_direction {
3999                                            to_pane = workspace.split_pane(
4000                                                to_pane,
4001                                                split_direction,
4002                                                window,
4003                                                cx,
4004                                            );
4005                                        }
4006                                        let new_item_handle = to_pane.update(cx, |pane, cx| {
4007                                            pane.open_item(
4008                                                project_entry_id,
4009                                                project_path,
4010                                                true,
4011                                                false,
4012                                                true,
4013                                                target,
4014                                                window,
4015                                                cx,
4016                                                build_item,
4017                                            )
4018                                        });
4019                                        (to_pane, new_item_handle)
4020                                    })
4021                                    .log_err()?;
4022                                to_pane
4023                                    .update_in(cx, |this, window, cx| {
4024                                        let Some(index) = this.index_for_item(&*new_item_handle)
4025                                        else {
4026                                            return;
4027                                        };
4028
4029                                        if target.is_some_and(|target| this.is_tab_pinned(target)) {
4030                                            this.pin_tab_at(index, window, cx);
4031                                        }
4032                                    })
4033                                    .ok()?
4034                            }
4035                            Some(())
4036                        })
4037                        .detach();
4038                    };
4039                });
4040            })
4041            .log_err();
4042    }
4043
4044    fn handle_external_paths_drop(
4045        &mut self,
4046        paths: &ExternalPaths,
4047        window: &mut Window,
4048        cx: &mut Context<Self>,
4049    ) {
4050        if let Some(active_item) = self.active_item()
4051            && active_item.handle_drop(self, paths, window, cx)
4052        {
4053            return;
4054        }
4055
4056        let mut to_pane = cx.entity();
4057        let mut split_direction = self.drag_split_direction;
4058        let paths = paths.paths().to_vec();
4059        let is_remote = self
4060            .workspace
4061            .update(cx, |workspace, cx| {
4062                if workspace.project().read(cx).is_via_collab() {
4063                    workspace.show_error("Cannot drop files on a remote project", cx);
4064                    true
4065                } else {
4066                    false
4067                }
4068            })
4069            .unwrap_or(true);
4070        if is_remote {
4071            return;
4072        }
4073
4074        self.workspace
4075            .update(cx, |workspace, cx| {
4076                let fs = Arc::clone(workspace.project().read(cx).fs());
4077                cx.spawn_in(window, async move |workspace, cx| {
4078                    let mut is_file_checks = FuturesUnordered::new();
4079                    for path in &paths {
4080                        is_file_checks.push(fs.is_file(path))
4081                    }
4082                    let mut has_files_to_open = false;
4083                    while let Some(is_file) = is_file_checks.next().await {
4084                        if is_file {
4085                            has_files_to_open = true;
4086                            break;
4087                        }
4088                    }
4089                    drop(is_file_checks);
4090                    if !has_files_to_open {
4091                        split_direction = None;
4092                    }
4093
4094                    if let Ok((open_task, to_pane)) =
4095                        workspace.update_in(cx, |workspace, window, cx| {
4096                            if let Some(split_direction) = split_direction {
4097                                to_pane =
4098                                    workspace.split_pane(to_pane, split_direction, window, cx);
4099                            }
4100                            (
4101                                workspace.open_paths(
4102                                    paths,
4103                                    OpenOptions {
4104                                        visible: Some(OpenVisible::OnlyDirectories),
4105                                        ..Default::default()
4106                                    },
4107                                    Some(to_pane.downgrade()),
4108                                    window,
4109                                    cx,
4110                                ),
4111                                to_pane,
4112                            )
4113                        })
4114                    {
4115                        let opened_items: Vec<_> = open_task.await;
4116                        _ = workspace.update_in(cx, |workspace, window, cx| {
4117                            for item in opened_items.into_iter().flatten() {
4118                                if let Err(e) = item {
4119                                    workspace.show_error(format!("Error: {e}"), cx);
4120                                }
4121                            }
4122                            if to_pane.read(cx).items_len() == 0 {
4123                                workspace.remove_pane(to_pane, None, window, cx);
4124                            }
4125                        });
4126                    }
4127                })
4128                .detach();
4129            })
4130            .log_err();
4131    }
4132
4133    pub fn display_nav_history_buttons(&mut self, display: Option<bool>) {
4134        self.display_nav_history_buttons = display;
4135    }
4136
4137    fn pinned_item_ids(&self) -> Vec<EntityId> {
4138        self.items
4139            .iter()
4140            .enumerate()
4141            .filter_map(|(index, item)| {
4142                if self.is_tab_pinned(index) {
4143                    return Some(item.item_id());
4144                }
4145
4146                None
4147            })
4148            .collect()
4149    }
4150
4151    fn clean_item_ids(&self, cx: &mut Context<Pane>) -> Vec<EntityId> {
4152        self.items()
4153            .filter_map(|item| {
4154                if !item.is_dirty(cx) {
4155                    return Some(item.item_id());
4156                }
4157
4158                None
4159            })
4160            .collect()
4161    }
4162
4163    fn to_the_side_item_ids(&self, item_id: EntityId, side: Side) -> Vec<EntityId> {
4164        match side {
4165            Side::Left => self
4166                .items()
4167                .take_while(|item| item.item_id() != item_id)
4168                .map(|item| item.item_id())
4169                .collect(),
4170            Side::Right => self
4171                .items()
4172                .rev()
4173                .take_while(|item| item.item_id() != item_id)
4174                .map(|item| item.item_id())
4175                .collect(),
4176        }
4177    }
4178
4179    fn multibuffer_item_ids(&self, cx: &mut Context<Pane>) -> Vec<EntityId> {
4180        self.items()
4181            .filter(|item| item.buffer_kind(cx) == ItemBufferKind::Multibuffer)
4182            .map(|item| item.item_id())
4183            .collect()
4184    }
4185
4186    pub fn drag_split_direction(&self) -> Option<SplitDirection> {
4187        self.drag_split_direction
4188    }
4189
4190    pub fn set_zoom_out_on_close(&mut self, zoom_out_on_close: bool) {
4191        self.zoom_out_on_close = zoom_out_on_close;
4192    }
4193}
4194
4195fn default_render_tab_bar_buttons(
4196    pane: &mut Pane,
4197    window: &mut Window,
4198    cx: &mut Context<Pane>,
4199) -> (Option<AnyElement>, Option<AnyElement>) {
4200    if !pane.has_focus(window, cx) && !pane.context_menu_focused(window, cx) {
4201        return (None, None);
4202    }
4203    let (can_clone, can_split_move) = match pane.active_item() {
4204        Some(active_item) if active_item.can_split(cx) => (true, false),
4205        Some(_) => (false, pane.items_len() > 1),
4206        None => (false, false),
4207    };
4208    // Ideally we would return a vec of elements here to pass directly to the [TabBar]'s
4209    // `end_slot`, but due to needing a view here that isn't possible.
4210    let right_children = h_flex()
4211        // Instead we need to replicate the spacing from the [TabBar]'s `end_slot` here.
4212        .gap(DynamicSpacing::Base04.rems(cx))
4213        .child(
4214            PopoverMenu::new("pane-tab-bar-popover-menu")
4215                .trigger_with_tooltip(
4216                    IconButton::new("plus", IconName::Plus).icon_size(IconSize::Small),
4217                    Tooltip::text("New…"),
4218                )
4219                .anchor(Anchor::TopRight)
4220                .with_handle(pane.new_item_context_menu_handle.clone())
4221                .menu(move |window, cx| {
4222                    Some(ContextMenu::build(window, cx, |menu, _, _| {
4223                        menu.action("New File", NewFile.boxed_clone())
4224                            .action("Open File", ToggleFileFinder::default().boxed_clone())
4225                            .separator()
4226                            .action("Search Project", DeploySearch::default().boxed_clone())
4227                            .action("Search Symbols", ToggleProjectSymbols.boxed_clone())
4228                            .separator()
4229                            .action("New Terminal", NewTerminal::default().boxed_clone())
4230                            .action(
4231                                "New Center Terminal",
4232                                NewCenterTerminal::default().boxed_clone(),
4233                            )
4234                    }))
4235                }),
4236        )
4237        .child(
4238            PopoverMenu::new("pane-tab-bar-split")
4239                .trigger_with_tooltip(
4240                    IconButton::new("split", IconName::Split)
4241                        .icon_size(IconSize::Small)
4242                        .disabled(!can_clone && !can_split_move),
4243                    Tooltip::text("Split Pane"),
4244                )
4245                .anchor(Anchor::TopRight)
4246                .with_handle(pane.split_item_context_menu_handle.clone())
4247                .menu(move |window, cx| {
4248                    ContextMenu::build(window, cx, |menu, _, _| {
4249                        let mode = SplitMode::MovePane;
4250                        if can_split_move {
4251                            menu.action("Split Right", SplitRight { mode }.boxed_clone())
4252                                .action("Split Left", SplitLeft { mode }.boxed_clone())
4253                                .action("Split Up", SplitUp { mode }.boxed_clone())
4254                                .action("Split Down", SplitDown { mode }.boxed_clone())
4255                        } else {
4256                            menu.action("Split Right", SplitRight::default().boxed_clone())
4257                                .action("Split Left", SplitLeft::default().boxed_clone())
4258                                .action("Split Up", SplitUp::default().boxed_clone())
4259                                .action("Split Down", SplitDown::default().boxed_clone())
4260                        }
4261                    })
4262                    .into()
4263                }),
4264        )
4265        .child({
4266            let zoomed = pane.is_zoomed();
4267            IconButton::new("toggle_zoom", IconName::Maximize)
4268                .icon_size(IconSize::Small)
4269                .toggle_state(zoomed)
4270                .selected_icon(IconName::Minimize)
4271                .on_click(cx.listener(|pane, _, window, cx| {
4272                    pane.toggle_zoom(&crate::ToggleZoom, window, cx);
4273                }))
4274                .tooltip(move |_window, cx| {
4275                    Tooltip::for_action(
4276                        if zoomed { "Zoom Out" } else { "Zoom In" },
4277                        &ToggleZoom,
4278                        cx,
4279                    )
4280                })
4281        })
4282        .into_any_element()
4283        .into();
4284    (None, right_children)
4285}
4286
4287impl Focusable for Pane {
4288    fn focus_handle(&self, _cx: &App) -> FocusHandle {
4289        self.focus_handle.clone()
4290    }
4291}
4292
4293impl Render for Pane {
4294    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
4295        let mut key_context = KeyContext::new_with_defaults();
4296        key_context.add("Pane");
4297        if self.active_item().is_none() {
4298            key_context.add("EmptyPane");
4299        }
4300
4301        self.toolbar
4302            .read(cx)
4303            .contribute_context(&mut key_context, cx);
4304
4305        let should_display_tab_bar = self.should_display_tab_bar.clone();
4306        let display_tab_bar = should_display_tab_bar(window, cx);
4307        let Some(project) = self.project.upgrade() else {
4308            return div().track_focus(&self.focus_handle(cx));
4309        };
4310        let is_local = project.read(cx).is_local();
4311
4312        v_flex()
4313            .key_context(key_context)
4314            .track_focus(&self.focus_handle(cx))
4315            .size_full()
4316            .flex_none()
4317            .overflow_hidden()
4318            .on_action(cx.listener(|pane, split: &SplitLeft, window, cx| {
4319                pane.split(SplitDirection::Left, split.mode, window, cx)
4320            }))
4321            .on_action(cx.listener(|pane, split: &SplitUp, window, cx| {
4322                pane.split(SplitDirection::Up, split.mode, window, cx)
4323            }))
4324            .on_action(cx.listener(|pane, split: &SplitHorizontal, window, cx| {
4325                pane.split(SplitDirection::horizontal(cx), split.mode, window, cx)
4326            }))
4327            .on_action(cx.listener(|pane, split: &SplitVertical, window, cx| {
4328                pane.split(SplitDirection::vertical(cx), split.mode, window, cx)
4329            }))
4330            .on_action(cx.listener(|pane, split: &SplitRight, window, cx| {
4331                pane.split(SplitDirection::Right, split.mode, window, cx)
4332            }))
4333            .on_action(cx.listener(|pane, split: &SplitDown, window, cx| {
4334                pane.split(SplitDirection::Down, split.mode, window, cx)
4335            }))
4336            .on_action(cx.listener(|pane, _: &SplitAndMoveUp, window, cx| {
4337                pane.split(SplitDirection::Up, SplitMode::MovePane, window, cx)
4338            }))
4339            .on_action(cx.listener(|pane, _: &SplitAndMoveDown, window, cx| {
4340                pane.split(SplitDirection::Down, SplitMode::MovePane, window, cx)
4341            }))
4342            .on_action(cx.listener(|pane, _: &SplitAndMoveLeft, window, cx| {
4343                pane.split(SplitDirection::Left, SplitMode::MovePane, window, cx)
4344            }))
4345            .on_action(cx.listener(|pane, _: &SplitAndMoveRight, window, cx| {
4346                pane.split(SplitDirection::Right, SplitMode::MovePane, window, cx)
4347            }))
4348            .on_action(cx.listener(|_, _: &JoinIntoNext, _, cx| {
4349                cx.emit(Event::JoinIntoNext);
4350            }))
4351            .on_action(cx.listener(|_, _: &JoinAll, _, cx| {
4352                cx.emit(Event::JoinAll);
4353            }))
4354            .on_action(cx.listener(Pane::toggle_zoom))
4355            .on_action(cx.listener(Pane::zoom_in))
4356            .on_action(cx.listener(Pane::zoom_out))
4357            .on_action(cx.listener(Self::navigate_backward))
4358            .on_action(cx.listener(Self::navigate_forward))
4359            .on_action(cx.listener(Self::go_to_older_tag))
4360            .on_action(cx.listener(Self::go_to_newer_tag))
4361            .on_action(
4362                cx.listener(|pane: &mut Pane, action: &ActivateItem, window, cx| {
4363                    pane.activate_item(
4364                        action.0.min(pane.items.len().saturating_sub(1)),
4365                        true,
4366                        true,
4367                        window,
4368                        cx,
4369                    );
4370                }),
4371            )
4372            .on_action(cx.listener(Self::alternate_file))
4373            .on_action(cx.listener(Self::activate_last_item))
4374            .on_action(cx.listener(Self::activate_previous_item))
4375            .on_action(cx.listener(Self::activate_next_item))
4376            .on_action(cx.listener(Self::swap_item_left))
4377            .on_action(cx.listener(Self::swap_item_right))
4378            .on_action(cx.listener(Self::toggle_pin_tab))
4379            .on_action(cx.listener(Self::unpin_all_tabs))
4380            .when(PreviewTabsSettings::get_global(cx).enabled, |this| {
4381                this.on_action(
4382                    cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, window, cx| {
4383                        if let Some(active_item_id) = pane.active_item().map(|i| i.item_id()) {
4384                            if pane.is_active_preview_item(active_item_id) {
4385                                pane.unpreview_item_if_preview(active_item_id);
4386                            } else {
4387                                pane.replace_preview_item_id(active_item_id, window, cx);
4388                            }
4389                        }
4390                    }),
4391                )
4392            })
4393            .on_action(
4394                cx.listener(|pane: &mut Self, action: &CloseActiveItem, window, cx| {
4395                    pane.close_active_item(action, window, cx)
4396                        .detach_and_log_err(cx)
4397                }),
4398            )
4399            .on_action(
4400                cx.listener(|pane: &mut Self, action: &CloseOtherItems, window, cx| {
4401                    pane.close_other_items(action, None, window, cx)
4402                        .detach_and_log_err(cx);
4403                }),
4404            )
4405            .on_action(
4406                cx.listener(|pane: &mut Self, action: &CloseCleanItems, window, cx| {
4407                    pane.close_clean_items(action, window, cx)
4408                        .detach_and_log_err(cx)
4409                }),
4410            )
4411            .on_action(cx.listener(
4412                |pane: &mut Self, action: &CloseItemsToTheLeft, window, cx| {
4413                    pane.close_items_to_the_left_by_id(None, action, window, cx)
4414                        .detach_and_log_err(cx)
4415                },
4416            ))
4417            .on_action(cx.listener(
4418                |pane: &mut Self, action: &CloseItemsToTheRight, window, cx| {
4419                    pane.close_items_to_the_right_by_id(None, action, window, cx)
4420                        .detach_and_log_err(cx)
4421                },
4422            ))
4423            .on_action(
4424                cx.listener(|pane: &mut Self, action: &CloseAllItems, window, cx| {
4425                    pane.close_all_items(action, window, cx)
4426                        .detach_and_log_err(cx)
4427                }),
4428            )
4429            .on_action(cx.listener(
4430                |pane: &mut Self, action: &CloseMultibufferItems, window, cx| {
4431                    pane.close_multibuffer_items(action, window, cx)
4432                        .detach_and_log_err(cx)
4433                },
4434            ))
4435            .on_action(cx.listener(
4436                |pane: &mut Self, action: &RevealInProjectPanel, _window, cx| {
4437                    let active_item = pane.active_item();
4438                    let entry_id = active_item.as_ref().and_then(|item| {
4439                        action
4440                            .entry_id
4441                            .map(ProjectEntryId::from_proto)
4442                            .or_else(|| item.project_entry_ids(cx).first().copied())
4443                    });
4444
4445                    pane.project
4446                        .update(cx, |project, cx| {
4447                            if let Some(entry_id) = entry_id
4448                                && project
4449                                    .worktree_for_entry(entry_id, cx)
4450                                    .is_some_and(|worktree| worktree.read(cx).is_visible())
4451                            {
4452                                return cx.emit(project::Event::RevealInProjectPanel(entry_id));
4453                            }
4454
4455                            // When no entry is found, which is the case when
4456                            // working with an unsaved buffer, or the worktree
4457                            // is not visible, for example, a file that doesn't
4458                            // belong to an open project, we can't reveal the
4459                            // entry but we still want to activate the project
4460                            // panel.
4461                            cx.emit(project::Event::ActivateProjectPanel);
4462                        })
4463                        .log_err();
4464                },
4465            ))
4466            .on_action(cx.listener(|_, _: &menu::Cancel, window, cx| {
4467                if cx.stop_active_drag(window) {
4468                } else {
4469                    cx.propagate();
4470                }
4471            }))
4472            .when(self.active_item().is_some() && display_tab_bar, |pane| {
4473                pane.child((self.render_tab_bar.clone())(self, window, cx))
4474            })
4475            .child({
4476                let has_worktrees = project.read(cx).visible_worktrees(cx).next().is_some();
4477                // main content
4478                div()
4479                    .flex_1()
4480                    .relative()
4481                    .group("")
4482                    .overflow_hidden()
4483                    .on_drag_move::<DraggedTab>(cx.listener(Self::handle_drag_move))
4484                    .on_drag_move::<DraggedSelection>(cx.listener(Self::handle_drag_move))
4485                    .when(is_local, |div| {
4486                        div.on_drag_move::<ExternalPaths>(cx.listener(Self::handle_drag_move))
4487                    })
4488                    .map(|div| {
4489                        if let Some(item) = self.active_item() {
4490                            div.id("pane_placeholder")
4491                                .v_flex()
4492                                .size_full()
4493                                .overflow_hidden()
4494                                .child(self.toolbar.clone())
4495                                .child(item.to_any_view())
4496                        } else {
4497                            let placeholder = div
4498                                .id("pane_placeholder")
4499                                .h_flex()
4500                                .size_full()
4501                                .justify_center()
4502                                .on_click(cx.listener(
4503                                    move |this, event: &ClickEvent, window, cx| {
4504                                        if event.click_count() == 2 {
4505                                            window.dispatch_action(
4506                                                this.double_click_dispatch_action.boxed_clone(),
4507                                                cx,
4508                                            );
4509                                        }
4510                                    },
4511                                ));
4512                            if has_worktrees || !self.should_display_welcome_page {
4513                                placeholder
4514                            } else {
4515                                if self.welcome_page.is_none() {
4516                                    let workspace = self.workspace.clone();
4517                                    self.welcome_page = Some(cx.new(|cx| {
4518                                        crate::welcome::WelcomePage::new(
4519                                            workspace, true, window, cx,
4520                                        )
4521                                    }));
4522                                }
4523                                placeholder.child(self.welcome_page.clone().unwrap())
4524                            }
4525                        }
4526                        .focus_follows_mouse(self.focus_follows_mouse, cx)
4527                    })
4528                    .child(
4529                        // drag target
4530                        div()
4531                            .invisible()
4532                            .absolute()
4533                            .bg(cx.theme().colors().drop_target_background)
4534                            .group_drag_over::<DraggedTab>("", |style| style.visible())
4535                            .group_drag_over::<DraggedSelection>("", |style| style.visible())
4536                            .when(is_local, |div| {
4537                                div.group_drag_over::<ExternalPaths>("", |style| style.visible())
4538                            })
4539                            .when_some(self.can_drop_predicate.clone(), |this, p| {
4540                                this.can_drop(move |a, window, cx| p(a, window, cx))
4541                            })
4542                            .on_drop(cx.listener(move |this, dragged_tab, window, cx| {
4543                                this.handle_tab_drop(
4544                                    dragged_tab,
4545                                    this.active_item_index(),
4546                                    true,
4547                                    window,
4548                                    cx,
4549                                )
4550                            }))
4551                            .on_drop(cx.listener(
4552                                move |this, selection: &DraggedSelection, window, cx| {
4553                                    this.handle_dragged_selection_drop(selection, None, window, cx)
4554                                },
4555                            ))
4556                            .on_drop(cx.listener(move |this, paths, window, cx| {
4557                                this.handle_external_paths_drop(paths, window, cx)
4558                            }))
4559                            .map(|div| {
4560                                let size = DefiniteLength::Fraction(0.5);
4561                                match self.drag_split_direction {
4562                                    None => div.top_0().right_0().bottom_0().left_0(),
4563                                    Some(SplitDirection::Up) => {
4564                                        div.top_0().left_0().right_0().h(size)
4565                                    }
4566                                    Some(SplitDirection::Down) => {
4567                                        div.left_0().bottom_0().right_0().h(size)
4568                                    }
4569                                    Some(SplitDirection::Left) => {
4570                                        div.top_0().left_0().bottom_0().w(size)
4571                                    }
4572                                    Some(SplitDirection::Right) => {
4573                                        div.top_0().bottom_0().right_0().w(size)
4574                                    }
4575                                }
4576                            }),
4577                    )
4578            })
4579            .on_mouse_down(
4580                MouseButton::Navigate(NavigationDirection::Back),
4581                cx.listener(|pane, _, window, cx| {
4582                    if let Some(workspace) = pane.workspace.upgrade() {
4583                        let pane = cx.entity().downgrade();
4584                        window.defer(cx, move |window, cx| {
4585                            workspace.update(cx, |workspace, cx| {
4586                                workspace.go_back(pane, window, cx).detach_and_log_err(cx)
4587                            })
4588                        })
4589                    }
4590                }),
4591            )
4592            .on_mouse_down(
4593                MouseButton::Navigate(NavigationDirection::Forward),
4594                cx.listener(|pane, _, window, cx| {
4595                    if let Some(workspace) = pane.workspace.upgrade() {
4596                        let pane = cx.entity().downgrade();
4597                        window.defer(cx, move |window, cx| {
4598                            workspace.update(cx, |workspace, cx| {
4599                                workspace
4600                                    .go_forward(pane, window, cx)
4601                                    .detach_and_log_err(cx)
4602                            })
4603                        })
4604                    }
4605                }),
4606            )
4607    }
4608}
4609
4610impl ItemNavHistory {
4611    pub fn push<D: 'static + Any + Send + Sync>(
4612        &mut self,
4613        data: Option<D>,
4614        row: Option<u32>,
4615        cx: &mut App,
4616    ) {
4617        if self
4618            .item
4619            .upgrade()
4620            .is_some_and(|item| item.include_in_nav_history())
4621        {
4622            let is_preview_item = self.history.0.lock().preview_item_id == Some(self.item.id());
4623            self.history
4624                .push(data, self.item.clone(), is_preview_item, row, cx);
4625        }
4626    }
4627
4628    pub fn navigation_entry(&self, data: Option<Arc<dyn Any + Send + Sync>>) -> NavigationEntry {
4629        let is_preview_item = self.history.0.lock().preview_item_id == Some(self.item.id());
4630        NavigationEntry {
4631            item: self.item.clone(),
4632            data,
4633            timestamp: 0,
4634            is_preview: is_preview_item,
4635            row: None,
4636        }
4637    }
4638
4639    pub fn push_tag(&mut self, origin: Option<NavigationEntry>, target: Option<NavigationEntry>) {
4640        if let (Some(origin_entry), Some(target_entry)) = (origin, target) {
4641            self.history.push_tag(origin_entry, target_entry);
4642        }
4643    }
4644
4645    pub fn pop_backward(&mut self, cx: &mut App) -> Option<NavigationEntry> {
4646        self.history.pop(NavigationMode::GoingBack, cx)
4647    }
4648
4649    pub fn pop_forward(&mut self, cx: &mut App) -> Option<NavigationEntry> {
4650        self.history.pop(NavigationMode::GoingForward, cx)
4651    }
4652}
4653
4654impl NavHistory {
4655    pub fn for_each_entry(
4656        &self,
4657        cx: &App,
4658        f: &mut dyn FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
4659    ) {
4660        let borrowed_history = self.0.lock();
4661        borrowed_history
4662            .forward_stack
4663            .iter()
4664            .chain(borrowed_history.backward_stack.iter())
4665            .chain(borrowed_history.closed_stack.iter())
4666            .for_each(|entry| {
4667                if let Some(project_and_abs_path) =
4668                    borrowed_history.paths_by_item.get(&entry.item.id())
4669                {
4670                    f(entry, project_and_abs_path.clone());
4671                } else if let Some(item) = entry.item.upgrade()
4672                    && let Some(path) = item.project_path(cx)
4673                {
4674                    f(entry, (path, None));
4675                }
4676            })
4677    }
4678
4679    pub fn set_mode(&mut self, mode: NavigationMode) {
4680        self.0.lock().mode = mode;
4681    }
4682
4683    pub fn mode(&self) -> NavigationMode {
4684        self.0.lock().mode
4685    }
4686
4687    pub fn disable(&mut self) {
4688        self.0.lock().mode = NavigationMode::Disabled;
4689    }
4690
4691    pub fn enable(&mut self) {
4692        self.0.lock().mode = NavigationMode::Normal;
4693    }
4694
4695    pub fn clear(&mut self, cx: &mut App) {
4696        let mut state = self.0.lock();
4697
4698        if state.backward_stack.is_empty()
4699            && state.forward_stack.is_empty()
4700            && state.closed_stack.is_empty()
4701            && state.paths_by_item.is_empty()
4702            && state.tag_stack.is_empty()
4703        {
4704            return;
4705        }
4706
4707        state.mode = NavigationMode::Normal;
4708        state.backward_stack.clear();
4709        state.forward_stack.clear();
4710        state.closed_stack.clear();
4711        state.paths_by_item.clear();
4712        state.tag_stack.clear();
4713        state.tag_stack_pos = 0;
4714        state.did_update(cx);
4715    }
4716
4717    pub fn pop(&mut self, mode: NavigationMode, cx: &mut App) -> Option<NavigationEntry> {
4718        let mut state = self.0.lock();
4719        let entry = match mode {
4720            NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
4721                return None;
4722            }
4723            NavigationMode::GoingBack => &mut state.backward_stack,
4724            NavigationMode::GoingForward => &mut state.forward_stack,
4725            NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
4726        }
4727        .pop_back();
4728        if entry.is_some() {
4729            state.did_update(cx);
4730        }
4731        entry
4732    }
4733
4734    pub fn push<D: 'static + Any + Send + Sync>(
4735        &mut self,
4736        data: Option<D>,
4737        item: Arc<dyn WeakItemHandle + Send + Sync>,
4738        is_preview: bool,
4739        row: Option<u32>,
4740        cx: &mut App,
4741    ) {
4742        let state = &mut *self.0.lock();
4743        let new_item_id = item.id();
4744
4745        let is_same_location =
4746            |entry: &NavigationEntry| entry.item.id() == new_item_id && entry.row == row;
4747
4748        match state.mode {
4749            NavigationMode::Disabled => {}
4750            NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
4751                state
4752                    .backward_stack
4753                    .retain(|entry| !is_same_location(entry));
4754
4755                if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
4756                    state.backward_stack.pop_front();
4757                }
4758                state.backward_stack.push_back(NavigationEntry {
4759                    item,
4760                    data: data.map(|data| Arc::new(data) as Arc<dyn Any + Send + Sync>),
4761                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
4762                    is_preview,
4763                    row,
4764                });
4765                state.forward_stack.clear();
4766            }
4767            NavigationMode::GoingBack => {
4768                state.forward_stack.retain(|entry| !is_same_location(entry));
4769
4770                if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
4771                    state.forward_stack.pop_front();
4772                }
4773                state.forward_stack.push_back(NavigationEntry {
4774                    item,
4775                    data: data.map(|data| Arc::new(data) as Arc<dyn Any + Send + Sync>),
4776                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
4777                    is_preview,
4778                    row,
4779                });
4780            }
4781            NavigationMode::GoingForward => {
4782                state
4783                    .backward_stack
4784                    .retain(|entry| !is_same_location(entry));
4785
4786                if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
4787                    state.backward_stack.pop_front();
4788                }
4789                state.backward_stack.push_back(NavigationEntry {
4790                    item,
4791                    data: data.map(|data| Arc::new(data) as Arc<dyn Any + Send + Sync>),
4792                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
4793                    is_preview,
4794                    row,
4795                });
4796            }
4797            NavigationMode::ClosingItem if is_preview => return,
4798            NavigationMode::ClosingItem => {
4799                if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
4800                    state.closed_stack.pop_front();
4801                }
4802                state.closed_stack.push_back(NavigationEntry {
4803                    item,
4804                    data: data.map(|data| Arc::new(data) as Arc<dyn Any + Send + Sync>),
4805                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
4806                    is_preview,
4807                    row,
4808                });
4809            }
4810        }
4811        state.did_update(cx);
4812    }
4813
4814    pub fn remove_item(&mut self, item_id: EntityId) {
4815        let mut state = self.0.lock();
4816        state.paths_by_item.remove(&item_id);
4817        state
4818            .backward_stack
4819            .retain(|entry| entry.item.id() != item_id);
4820        state
4821            .forward_stack
4822            .retain(|entry| entry.item.id() != item_id);
4823        state
4824            .closed_stack
4825            .retain(|entry| entry.item.id() != item_id);
4826        state
4827            .tag_stack
4828            .retain(|entry| entry.origin.item.id() != item_id && entry.target.item.id() != item_id);
4829    }
4830
4831    pub fn rename_item(
4832        &mut self,
4833        item_id: EntityId,
4834        project_path: ProjectPath,
4835        abs_path: Option<PathBuf>,
4836    ) {
4837        let mut state = self.0.lock();
4838        let path_for_item = state.paths_by_item.get_mut(&item_id);
4839        if let Some(path_for_item) = path_for_item {
4840            path_for_item.0 = project_path;
4841            path_for_item.1 = abs_path;
4842        }
4843    }
4844
4845    pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option<PathBuf>)> {
4846        self.0.lock().paths_by_item.get(&item_id).cloned()
4847    }
4848
4849    pub fn push_tag(&mut self, origin: NavigationEntry, target: NavigationEntry) {
4850        let mut state = self.0.lock();
4851        let truncate_to = state.tag_stack_pos;
4852        state.tag_stack.truncate(truncate_to);
4853        state.tag_stack.push_back(TagStackEntry { origin, target });
4854        state.tag_stack_pos = state.tag_stack.len();
4855    }
4856
4857    pub fn pop_tag(&mut self, mode: TagNavigationMode) -> Option<NavigationEntry> {
4858        let mut state = self.0.lock();
4859        match mode {
4860            TagNavigationMode::Older => {
4861                if state.tag_stack_pos > 0 {
4862                    state.tag_stack_pos -= 1;
4863                    state
4864                        .tag_stack
4865                        .get(state.tag_stack_pos)
4866                        .map(|e| e.origin.clone())
4867                } else {
4868                    None
4869                }
4870            }
4871            TagNavigationMode::Newer => {
4872                let entry = state
4873                    .tag_stack
4874                    .get(state.tag_stack_pos)
4875                    .map(|e| e.target.clone());
4876                if state.tag_stack_pos < state.tag_stack.len() {
4877                    state.tag_stack_pos += 1;
4878                }
4879                entry
4880            }
4881        }
4882    }
4883}
4884
4885impl NavHistoryState {
4886    pub fn did_update(&self, cx: &mut App) {
4887        if let Some(pane) = self.pane.upgrade() {
4888            cx.defer(move |cx| {
4889                pane.update(cx, |pane, cx| pane.history_updated(cx));
4890            });
4891        }
4892    }
4893}
4894
4895fn dirty_message_for(buffer_path: Option<ProjectPath>, path_style: PathStyle) -> String {
4896    let path = buffer_path.as_ref().and_then(|p| {
4897        let path = p.path.display(path_style);
4898        if path.is_empty() { None } else { Some(path) }
4899    });
4900    match path {
4901        Some(path) => {
4902            let path = truncate_and_remove_front(&path, 80);
4903            format!(
4904                "{} contains unsaved edits. Do you want to save it?",
4905                MarkdownInlineCode(path.as_str())
4906            )
4907        }
4908        None => "This buffer contains unsaved edits. Do you want to save it?".to_string(),
4909    }
4910}
4911
4912pub fn tab_details(items: &[Box<dyn ItemHandle>], _window: &Window, cx: &App) -> Vec<usize> {
4913    util::disambiguate::compute_disambiguation_details(items, |item, detail| {
4914        item.tab_content_text(detail, cx)
4915    })
4916}
4917
4918pub fn render_item_indicator(item: Box<dyn ItemHandle>, cx: &App) -> Option<Indicator> {
4919    maybe!({
4920        let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
4921            (true, _) => Color::Warning,
4922            (_, true) => Color::Accent,
4923            (false, false) => return None,
4924        };
4925
4926        Some(Indicator::dot().color(indicator_color))
4927    })
4928}
4929
4930impl Render for DraggedTab {
4931    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
4932        let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
4933        let label = self.item.tab_content(
4934            TabContentParams {
4935                detail: Some(self.detail),
4936                selected: false,
4937                preview: false,
4938                deemphasized: false,
4939                max_title_len: None,
4940                truncate_title_middle: false,
4941            },
4942            window,
4943            cx,
4944        );
4945        let icon =
4946            self.pane
4947                .read(cx)
4948                .tab_icon_element(self.item.as_ref(), self.is_active, window, cx);
4949        Tab::new("")
4950            .toggle_state(self.is_active)
4951            .children(icon)
4952            .child(label)
4953            .render(window, cx)
4954            .font(ui_font)
4955    }
4956}
4957
4958#[cfg(test)]
4959mod tests {
4960    use std::{cell::Cell, iter::zip, num::NonZero, rc::Rc};
4961
4962    use super::*;
4963    use crate::{
4964        Member,
4965        item::test::{TestItem, TestProjectItem},
4966    };
4967    use gpui::{
4968        AppContext, Axis, Modifiers, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
4969        TestAppContext, VisualTestContext, size,
4970    };
4971    use project::FakeFs;
4972    use settings::SettingsStore;
4973    use theme::LoadThemes;
4974    use util::TryFutureExt;
4975
4976    // drop_call_count is a Cell here because `handle_drop` takes &self, not &mut self.
4977    struct CustomDropHandlingItem {
4978        focus_handle: gpui::FocusHandle,
4979        drop_call_count: Cell<usize>,
4980    }
4981
4982    impl CustomDropHandlingItem {
4983        fn new(cx: &mut Context<Self>) -> Self {
4984            Self {
4985                focus_handle: cx.focus_handle(),
4986                drop_call_count: Cell::new(0),
4987            }
4988        }
4989
4990        fn drop_call_count(&self) -> usize {
4991            self.drop_call_count.get()
4992        }
4993    }
4994
4995    impl EventEmitter<()> for CustomDropHandlingItem {}
4996
4997    impl Focusable for CustomDropHandlingItem {
4998        fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
4999            self.focus_handle.clone()
5000        }
5001    }
5002
5003    impl Render for CustomDropHandlingItem {
5004        fn render(
5005            &mut self,
5006            _window: &mut Window,
5007            _cx: &mut Context<Self>,
5008        ) -> impl gpui::IntoElement {
5009            gpui::Empty
5010        }
5011    }
5012
5013    impl Item for CustomDropHandlingItem {
5014        type Event = ();
5015
5016        fn tab_content_text(&self, _detail: usize, _cx: &App) -> gpui::SharedString {
5017            "custom_drop_handling_item".into()
5018        }
5019
5020        fn handle_drop(
5021            &self,
5022            _active_pane: &Pane,
5023            dropped: &dyn std::any::Any,
5024            _window: &mut Window,
5025            _cx: &mut App,
5026        ) -> bool {
5027            let is_dragged_tab = dropped.downcast_ref::<DraggedTab>().is_some();
5028            if is_dragged_tab {
5029                self.drop_call_count.set(self.drop_call_count.get() + 1);
5030            }
5031            is_dragged_tab
5032        }
5033    }
5034
5035    #[gpui::test]
5036    async fn test_add_item_capped_to_max_tabs(cx: &mut TestAppContext) {
5037        init_test(cx);
5038        let fs = FakeFs::new(cx.executor());
5039
5040        let project = Project::test(fs, None, cx).await;
5041        let (workspace, cx) =
5042            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5043        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5044
5045        for i in 0..7 {
5046            add_labeled_item(&pane, format!("{}", i).as_str(), false, cx);
5047        }
5048
5049        set_max_tabs(cx, Some(5));
5050        add_labeled_item(&pane, "7", false, cx);
5051        // Remove items to respect the max tab cap.
5052        assert_item_labels(&pane, ["3", "4", "5", "6", "7*"], cx);
5053        pane.update_in(cx, |pane, window, cx| {
5054            pane.activate_item(0, false, false, window, cx);
5055        });
5056        add_labeled_item(&pane, "X", false, cx);
5057        // Respect activation order.
5058        assert_item_labels(&pane, ["3", "X*", "5", "6", "7"], cx);
5059
5060        for i in 0..7 {
5061            add_labeled_item(&pane, format!("D{}", i).as_str(), true, cx);
5062        }
5063        // Keeps dirty items, even over max tab cap.
5064        assert_item_labels(
5065            &pane,
5066            ["D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6*^"],
5067            cx,
5068        );
5069
5070        set_max_tabs(cx, None);
5071        for i in 0..7 {
5072            add_labeled_item(&pane, format!("N{}", i).as_str(), false, cx);
5073        }
5074        // No cap when max tabs is None.
5075        assert_item_labels(
5076            &pane,
5077            [
5078                "D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6^", "N0", "N1", "N2", "N3", "N4",
5079                "N5", "N6*",
5080            ],
5081            cx,
5082        );
5083    }
5084
5085    #[gpui::test]
5086    async fn test_reduce_max_tabs_closes_existing_items(cx: &mut TestAppContext) {
5087        init_test(cx);
5088        let fs = FakeFs::new(cx.executor());
5089
5090        let project = Project::test(fs, None, cx).await;
5091        let (workspace, cx) =
5092            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5093        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5094
5095        add_labeled_item(&pane, "A", false, cx);
5096        add_labeled_item(&pane, "B", false, cx);
5097        let item_c = add_labeled_item(&pane, "C", false, cx);
5098        let item_d = add_labeled_item(&pane, "D", false, cx);
5099        add_labeled_item(&pane, "E", false, cx);
5100        add_labeled_item(&pane, "Settings", false, cx);
5101        assert_item_labels(&pane, ["A", "B", "C", "D", "E", "Settings*"], cx);
5102
5103        set_max_tabs(cx, Some(5));
5104        assert_item_labels(&pane, ["B", "C", "D", "E", "Settings*"], cx);
5105
5106        set_max_tabs(cx, Some(4));
5107        assert_item_labels(&pane, ["C", "D", "E", "Settings*"], cx);
5108
5109        pane.update_in(cx, |pane, window, cx| {
5110            let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
5111            pane.pin_tab_at(ix, window, cx);
5112
5113            let ix = pane.index_for_item_id(item_d.item_id()).unwrap();
5114            pane.pin_tab_at(ix, window, cx);
5115        });
5116        assert_item_labels(&pane, ["C!", "D!", "E", "Settings*"], cx);
5117
5118        set_max_tabs(cx, Some(2));
5119        assert_item_labels(&pane, ["C!", "D!", "Settings*"], cx);
5120    }
5121
5122    #[gpui::test]
5123    async fn test_allow_pinning_dirty_item_at_max_tabs(cx: &mut TestAppContext) {
5124        init_test(cx);
5125        let fs = FakeFs::new(cx.executor());
5126
5127        let project = Project::test(fs, None, cx).await;
5128        let (workspace, cx) =
5129            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5130        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5131
5132        set_max_tabs(cx, Some(1));
5133        let item_a = add_labeled_item(&pane, "A", true, cx);
5134
5135        pane.update_in(cx, |pane, window, cx| {
5136            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
5137            pane.pin_tab_at(ix, window, cx);
5138        });
5139        assert_item_labels(&pane, ["A*^!"], cx);
5140    }
5141
5142    #[gpui::test]
5143    async fn test_allow_pinning_non_dirty_item_at_max_tabs(cx: &mut TestAppContext) {
5144        init_test(cx);
5145        let fs = FakeFs::new(cx.executor());
5146
5147        let project = Project::test(fs, None, cx).await;
5148        let (workspace, cx) =
5149            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5150        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5151
5152        set_max_tabs(cx, Some(1));
5153        let item_a = add_labeled_item(&pane, "A", false, cx);
5154
5155        pane.update_in(cx, |pane, window, cx| {
5156            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
5157            pane.pin_tab_at(ix, window, cx);
5158        });
5159        assert_item_labels(&pane, ["A*!"], cx);
5160    }
5161
5162    #[gpui::test]
5163    async fn test_pin_tabs_incrementally_at_max_capacity(cx: &mut TestAppContext) {
5164        init_test(cx);
5165        let fs = FakeFs::new(cx.executor());
5166
5167        let project = Project::test(fs, None, cx).await;
5168        let (workspace, cx) =
5169            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5170        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5171
5172        set_max_tabs(cx, Some(3));
5173
5174        let item_a = add_labeled_item(&pane, "A", false, cx);
5175        assert_item_labels(&pane, ["A*"], cx);
5176
5177        pane.update_in(cx, |pane, window, cx| {
5178            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
5179            pane.pin_tab_at(ix, window, cx);
5180        });
5181        assert_item_labels(&pane, ["A*!"], cx);
5182
5183        let item_b = add_labeled_item(&pane, "B", false, cx);
5184        assert_item_labels(&pane, ["A!", "B*"], cx);
5185
5186        pane.update_in(cx, |pane, window, cx| {
5187            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
5188            pane.pin_tab_at(ix, window, cx);
5189        });
5190        assert_item_labels(&pane, ["A!", "B*!"], cx);
5191
5192        let item_c = add_labeled_item(&pane, "C", false, cx);
5193        assert_item_labels(&pane, ["A!", "B!", "C*"], cx);
5194
5195        pane.update_in(cx, |pane, window, cx| {
5196            let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
5197            pane.pin_tab_at(ix, window, cx);
5198        });
5199        assert_item_labels(&pane, ["A!", "B!", "C*!"], cx);
5200    }
5201
5202    #[gpui::test]
5203    async fn test_pin_tabs_left_to_right_after_opening_at_max_capacity(cx: &mut TestAppContext) {
5204        init_test(cx);
5205        let fs = FakeFs::new(cx.executor());
5206
5207        let project = Project::test(fs, None, cx).await;
5208        let (workspace, cx) =
5209            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5210        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5211
5212        set_max_tabs(cx, Some(3));
5213
5214        let item_a = add_labeled_item(&pane, "A", false, cx);
5215        assert_item_labels(&pane, ["A*"], cx);
5216
5217        let item_b = add_labeled_item(&pane, "B", false, cx);
5218        assert_item_labels(&pane, ["A", "B*"], cx);
5219
5220        let item_c = add_labeled_item(&pane, "C", false, cx);
5221        assert_item_labels(&pane, ["A", "B", "C*"], cx);
5222
5223        pane.update_in(cx, |pane, window, cx| {
5224            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
5225            pane.pin_tab_at(ix, window, cx);
5226        });
5227        assert_item_labels(&pane, ["A!", "B", "C*"], cx);
5228
5229        pane.update_in(cx, |pane, window, cx| {
5230            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
5231            pane.pin_tab_at(ix, window, cx);
5232        });
5233        assert_item_labels(&pane, ["A!", "B!", "C*"], cx);
5234
5235        pane.update_in(cx, |pane, window, cx| {
5236            let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
5237            pane.pin_tab_at(ix, window, cx);
5238        });
5239        assert_item_labels(&pane, ["A!", "B!", "C*!"], cx);
5240    }
5241
5242    #[gpui::test]
5243    async fn test_pin_tabs_right_to_left_after_opening_at_max_capacity(cx: &mut TestAppContext) {
5244        init_test(cx);
5245        let fs = FakeFs::new(cx.executor());
5246
5247        let project = Project::test(fs, None, cx).await;
5248        let (workspace, cx) =
5249            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5250        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5251
5252        set_max_tabs(cx, Some(3));
5253
5254        let item_a = add_labeled_item(&pane, "A", false, cx);
5255        assert_item_labels(&pane, ["A*"], cx);
5256
5257        let item_b = add_labeled_item(&pane, "B", false, cx);
5258        assert_item_labels(&pane, ["A", "B*"], cx);
5259
5260        let item_c = add_labeled_item(&pane, "C", false, cx);
5261        assert_item_labels(&pane, ["A", "B", "C*"], cx);
5262
5263        pane.update_in(cx, |pane, window, cx| {
5264            let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
5265            pane.pin_tab_at(ix, window, cx);
5266        });
5267        assert_item_labels(&pane, ["C*!", "A", "B"], cx);
5268
5269        pane.update_in(cx, |pane, window, cx| {
5270            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
5271            pane.pin_tab_at(ix, window, cx);
5272        });
5273        assert_item_labels(&pane, ["C*!", "B!", "A"], cx);
5274
5275        pane.update_in(cx, |pane, window, cx| {
5276            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
5277            pane.pin_tab_at(ix, window, cx);
5278        });
5279        assert_item_labels(&pane, ["C*!", "B!", "A!"], cx);
5280    }
5281
5282    #[gpui::test]
5283    async fn test_pinned_tabs_never_closed_at_max_tabs(cx: &mut TestAppContext) {
5284        init_test(cx);
5285        let fs = FakeFs::new(cx.executor());
5286
5287        let project = Project::test(fs, None, cx).await;
5288        let (workspace, cx) =
5289            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5290        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5291
5292        let item_a = add_labeled_item(&pane, "A", false, cx);
5293        pane.update_in(cx, |pane, window, cx| {
5294            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
5295            pane.pin_tab_at(ix, window, cx);
5296        });
5297
5298        let item_b = add_labeled_item(&pane, "B", false, cx);
5299        pane.update_in(cx, |pane, window, cx| {
5300            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
5301            pane.pin_tab_at(ix, window, cx);
5302        });
5303
5304        add_labeled_item(&pane, "C", false, cx);
5305        add_labeled_item(&pane, "D", false, cx);
5306        add_labeled_item(&pane, "E", false, cx);
5307        assert_item_labels(&pane, ["A!", "B!", "C", "D", "E*"], cx);
5308
5309        set_max_tabs(cx, Some(3));
5310        add_labeled_item(&pane, "F", false, cx);
5311        assert_item_labels(&pane, ["A!", "B!", "F*"], cx);
5312
5313        add_labeled_item(&pane, "G", false, cx);
5314        assert_item_labels(&pane, ["A!", "B!", "G*"], cx);
5315
5316        add_labeled_item(&pane, "H", false, cx);
5317        assert_item_labels(&pane, ["A!", "B!", "H*"], cx);
5318    }
5319
5320    #[gpui::test]
5321    async fn test_always_allows_one_unpinned_item_over_max_tabs_regardless_of_pinned_count(
5322        cx: &mut TestAppContext,
5323    ) {
5324        init_test(cx);
5325        let fs = FakeFs::new(cx.executor());
5326
5327        let project = Project::test(fs, None, cx).await;
5328        let (workspace, cx) =
5329            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5330        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5331
5332        set_max_tabs(cx, Some(3));
5333
5334        let item_a = add_labeled_item(&pane, "A", false, cx);
5335        pane.update_in(cx, |pane, window, cx| {
5336            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
5337            pane.pin_tab_at(ix, window, cx);
5338        });
5339
5340        let item_b = add_labeled_item(&pane, "B", false, cx);
5341        pane.update_in(cx, |pane, window, cx| {
5342            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
5343            pane.pin_tab_at(ix, window, cx);
5344        });
5345
5346        let item_c = add_labeled_item(&pane, "C", false, cx);
5347        pane.update_in(cx, |pane, window, cx| {
5348            let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
5349            pane.pin_tab_at(ix, window, cx);
5350        });
5351
5352        assert_item_labels(&pane, ["A!", "B!", "C*!"], cx);
5353
5354        let item_d = add_labeled_item(&pane, "D", false, cx);
5355        assert_item_labels(&pane, ["A!", "B!", "C!", "D*"], cx);
5356
5357        pane.update_in(cx, |pane, window, cx| {
5358            let ix = pane.index_for_item_id(item_d.item_id()).unwrap();
5359            pane.pin_tab_at(ix, window, cx);
5360        });
5361        assert_item_labels(&pane, ["A!", "B!", "C!", "D*!"], cx);
5362
5363        add_labeled_item(&pane, "E", false, cx);
5364        assert_item_labels(&pane, ["A!", "B!", "C!", "D!", "E*"], cx);
5365
5366        add_labeled_item(&pane, "F", false, cx);
5367        assert_item_labels(&pane, ["A!", "B!", "C!", "D!", "F*"], cx);
5368    }
5369
5370    #[gpui::test]
5371    async fn test_can_open_one_item_when_all_tabs_are_dirty_at_max(cx: &mut TestAppContext) {
5372        init_test(cx);
5373        let fs = FakeFs::new(cx.executor());
5374
5375        let project = Project::test(fs, None, cx).await;
5376        let (workspace, cx) =
5377            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5378        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5379
5380        set_max_tabs(cx, Some(3));
5381
5382        add_labeled_item(&pane, "A", true, cx);
5383        assert_item_labels(&pane, ["A*^"], cx);
5384
5385        add_labeled_item(&pane, "B", true, cx);
5386        assert_item_labels(&pane, ["A^", "B*^"], cx);
5387
5388        add_labeled_item(&pane, "C", true, cx);
5389        assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
5390
5391        add_labeled_item(&pane, "D", false, cx);
5392        assert_item_labels(&pane, ["A^", "B^", "C^", "D*"], cx);
5393
5394        add_labeled_item(&pane, "E", false, cx);
5395        assert_item_labels(&pane, ["A^", "B^", "C^", "E*"], cx);
5396
5397        add_labeled_item(&pane, "F", false, cx);
5398        assert_item_labels(&pane, ["A^", "B^", "C^", "F*"], cx);
5399
5400        add_labeled_item(&pane, "G", true, cx);
5401        assert_item_labels(&pane, ["A^", "B^", "C^", "G*^"], cx);
5402    }
5403
5404    #[gpui::test]
5405    async fn test_toggle_pin_tab(cx: &mut TestAppContext) {
5406        init_test(cx);
5407        let fs = FakeFs::new(cx.executor());
5408
5409        let project = Project::test(fs, None, cx).await;
5410        let (workspace, cx) =
5411            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5412        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5413
5414        set_labeled_items(&pane, ["A", "B*", "C"], cx);
5415        assert_item_labels(&pane, ["A", "B*", "C"], cx);
5416
5417        pane.update_in(cx, |pane, window, cx| {
5418            pane.toggle_pin_tab(&TogglePinTab, window, cx);
5419        });
5420        assert_item_labels(&pane, ["B*!", "A", "C"], cx);
5421
5422        pane.update_in(cx, |pane, window, cx| {
5423            pane.toggle_pin_tab(&TogglePinTab, window, cx);
5424        });
5425        assert_item_labels(&pane, ["B*", "A", "C"], cx);
5426    }
5427
5428    #[gpui::test]
5429    async fn test_unpin_all_tabs(cx: &mut TestAppContext) {
5430        init_test(cx);
5431        let fs = FakeFs::new(cx.executor());
5432
5433        let project = Project::test(fs, None, cx).await;
5434        let (workspace, cx) =
5435            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5436        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5437
5438        // Unpin all, in an empty pane
5439        pane.update_in(cx, |pane, window, cx| {
5440            pane.unpin_all_tabs(&UnpinAllTabs, window, cx);
5441        });
5442
5443        assert_item_labels(&pane, [], cx);
5444
5445        let item_a = add_labeled_item(&pane, "A", false, cx);
5446        let item_b = add_labeled_item(&pane, "B", false, cx);
5447        let item_c = add_labeled_item(&pane, "C", false, cx);
5448        assert_item_labels(&pane, ["A", "B", "C*"], cx);
5449
5450        // Unpin all, when no tabs are pinned
5451        pane.update_in(cx, |pane, window, cx| {
5452            pane.unpin_all_tabs(&UnpinAllTabs, window, cx);
5453        });
5454
5455        assert_item_labels(&pane, ["A", "B", "C*"], cx);
5456
5457        // Pin inactive tabs only
5458        pane.update_in(cx, |pane, window, cx| {
5459            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
5460            pane.pin_tab_at(ix, window, cx);
5461
5462            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
5463            pane.pin_tab_at(ix, window, cx);
5464        });
5465        assert_item_labels(&pane, ["A!", "B!", "C*"], cx);
5466
5467        pane.update_in(cx, |pane, window, cx| {
5468            pane.unpin_all_tabs(&UnpinAllTabs, window, cx);
5469        });
5470
5471        assert_item_labels(&pane, ["A", "B", "C*"], cx);
5472
5473        // Pin all tabs
5474        pane.update_in(cx, |pane, window, cx| {
5475            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
5476            pane.pin_tab_at(ix, window, cx);
5477
5478            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
5479            pane.pin_tab_at(ix, window, cx);
5480
5481            let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
5482            pane.pin_tab_at(ix, window, cx);
5483        });
5484        assert_item_labels(&pane, ["A!", "B!", "C*!"], cx);
5485
5486        // Activate middle tab
5487        pane.update_in(cx, |pane, window, cx| {
5488            pane.activate_item(1, false, false, window, cx);
5489        });
5490        assert_item_labels(&pane, ["A!", "B*!", "C!"], cx);
5491
5492        pane.update_in(cx, |pane, window, cx| {
5493            pane.unpin_all_tabs(&UnpinAllTabs, window, cx);
5494        });
5495
5496        // Order has not changed
5497        assert_item_labels(&pane, ["A", "B*", "C"], cx);
5498    }
5499
5500    #[gpui::test]
5501    async fn test_separate_pinned_row_disabled_by_default(cx: &mut TestAppContext) {
5502        init_test(cx);
5503        let fs = FakeFs::new(cx.executor());
5504
5505        let project = Project::test(fs, None, cx).await;
5506        let (workspace, cx) =
5507            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5508        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5509
5510        let item_a = add_labeled_item(&pane, "A", false, cx);
5511        add_labeled_item(&pane, "B", false, cx);
5512        add_labeled_item(&pane, "C", false, cx);
5513
5514        // Pin one tab
5515        pane.update_in(cx, |pane, window, cx| {
5516            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
5517            pane.pin_tab_at(ix, window, cx);
5518        });
5519        assert_item_labels(&pane, ["A!", "B", "C*"], cx);
5520
5521        // Verify setting is disabled by default
5522        let is_separate_row_enabled = pane.read_with(cx, |_, cx| {
5523            TabBarSettings::get_global(cx).show_pinned_tabs_in_separate_row
5524        });
5525        assert!(
5526            !is_separate_row_enabled,
5527            "Separate pinned row should be disabled by default"
5528        );
5529
5530        // Verify pinned_tabs_row element does NOT exist (single row layout)
5531        let pinned_row_bounds = cx.debug_bounds("pinned_tabs_row");
5532        assert!(
5533            pinned_row_bounds.is_none(),
5534            "pinned_tabs_row should not exist when setting is disabled"
5535        );
5536    }
5537
5538    #[gpui::test]
5539    async fn test_separate_pinned_row_two_rows_when_both_tab_types_exist(cx: &mut TestAppContext) {
5540        init_test(cx);
5541        let fs = FakeFs::new(cx.executor());
5542
5543        let project = Project::test(fs, None, cx).await;
5544        let (workspace, cx) =
5545            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5546        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5547
5548        // Enable separate row setting
5549        set_pinned_tabs_separate_row(cx, true);
5550
5551        let item_a = add_labeled_item(&pane, "A", false, cx);
5552        add_labeled_item(&pane, "B", false, cx);
5553        add_labeled_item(&pane, "C", false, cx);
5554
5555        // Pin one tab - now we have both pinned and unpinned tabs
5556        pane.update_in(cx, |pane, window, cx| {
5557            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
5558            pane.pin_tab_at(ix, window, cx);
5559        });
5560        assert_item_labels(&pane, ["A!", "B", "C*"], cx);
5561
5562        // Verify pinned_tabs_row element exists (two row layout)
5563        let pinned_row_bounds = cx.debug_bounds("pinned_tabs_row");
5564        assert!(
5565            pinned_row_bounds.is_some(),
5566            "pinned_tabs_row should exist when setting is enabled and both tab types exist"
5567        );
5568    }
5569
5570    #[gpui::test]
5571    async fn test_separate_pinned_row_single_row_when_only_pinned_tabs(cx: &mut TestAppContext) {
5572        init_test(cx);
5573        let fs = FakeFs::new(cx.executor());
5574
5575        let project = Project::test(fs, None, cx).await;
5576        let (workspace, cx) =
5577            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5578        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5579
5580        // Enable separate row setting
5581        set_pinned_tabs_separate_row(cx, true);
5582
5583        let item_a = add_labeled_item(&pane, "A", false, cx);
5584        let item_b = add_labeled_item(&pane, "B", false, cx);
5585
5586        // Pin all tabs - only pinned tabs exist
5587        pane.update_in(cx, |pane, window, cx| {
5588            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
5589            pane.pin_tab_at(ix, window, cx);
5590            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
5591            pane.pin_tab_at(ix, window, cx);
5592        });
5593        assert_item_labels(&pane, ["A!", "B*!"], cx);
5594
5595        // Verify pinned_tabs_row does NOT exist (single row layout for pinned-only)
5596        let pinned_row_bounds = cx.debug_bounds("pinned_tabs_row");
5597        assert!(
5598            pinned_row_bounds.is_none(),
5599            "pinned_tabs_row should not exist when only pinned tabs exist (uses single row)"
5600        );
5601    }
5602
5603    #[gpui::test]
5604    async fn test_separate_pinned_row_single_row_when_only_unpinned_tabs(cx: &mut TestAppContext) {
5605        init_test(cx);
5606        let fs = FakeFs::new(cx.executor());
5607
5608        let project = Project::test(fs, None, cx).await;
5609        let (workspace, cx) =
5610            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5611        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5612
5613        // Enable separate row setting
5614        set_pinned_tabs_separate_row(cx, true);
5615
5616        // Add only unpinned tabs
5617        add_labeled_item(&pane, "A", false, cx);
5618        add_labeled_item(&pane, "B", false, cx);
5619        add_labeled_item(&pane, "C", false, cx);
5620        assert_item_labels(&pane, ["A", "B", "C*"], cx);
5621
5622        // Verify pinned_tabs_row does NOT exist (single row layout for unpinned-only)
5623        let pinned_row_bounds = cx.debug_bounds("pinned_tabs_row");
5624        assert!(
5625            pinned_row_bounds.is_none(),
5626            "pinned_tabs_row should not exist when only unpinned tabs exist (uses single row)"
5627        );
5628    }
5629
5630    #[gpui::test]
5631    async fn test_separate_pinned_row_toggles_between_layouts(cx: &mut TestAppContext) {
5632        init_test(cx);
5633        let fs = FakeFs::new(cx.executor());
5634
5635        let project = Project::test(fs, None, cx).await;
5636        let (workspace, cx) =
5637            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5638        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5639
5640        let item_a = add_labeled_item(&pane, "A", false, cx);
5641        add_labeled_item(&pane, "B", false, cx);
5642
5643        // Pin one tab
5644        pane.update_in(cx, |pane, window, cx| {
5645            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
5646            pane.pin_tab_at(ix, window, cx);
5647        });
5648
5649        // Initially disabled - single row
5650        let pinned_row_bounds = cx.debug_bounds("pinned_tabs_row");
5651        assert!(
5652            pinned_row_bounds.is_none(),
5653            "Should be single row when disabled"
5654        );
5655
5656        // Enable - two rows
5657        set_pinned_tabs_separate_row(cx, true);
5658        cx.run_until_parked();
5659        let pinned_row_bounds = cx.debug_bounds("pinned_tabs_row");
5660        assert!(
5661            pinned_row_bounds.is_some(),
5662            "Should be two rows when enabled"
5663        );
5664
5665        // Disable again - back to single row
5666        set_pinned_tabs_separate_row(cx, false);
5667        cx.run_until_parked();
5668        let pinned_row_bounds = cx.debug_bounds("pinned_tabs_row");
5669        assert!(
5670            pinned_row_bounds.is_none(),
5671            "Should be single row when disabled again"
5672        );
5673    }
5674
5675    #[gpui::test]
5676    async fn test_separate_pinned_row_has_right_border(cx: &mut TestAppContext) {
5677        init_test(cx);
5678        let fs = FakeFs::new(cx.executor());
5679
5680        let project = Project::test(fs, None, cx).await;
5681        let (workspace, cx) =
5682            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5683        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5684
5685        // Enable separate row setting
5686        set_pinned_tabs_separate_row(cx, true);
5687
5688        let item_a = add_labeled_item(&pane, "A", false, cx);
5689        add_labeled_item(&pane, "B", false, cx);
5690        add_labeled_item(&pane, "C", false, cx);
5691
5692        // Pin one tab - now we have both pinned and unpinned tabs (two-row layout)
5693        pane.update_in(cx, |pane, window, cx| {
5694            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
5695            pane.pin_tab_at(ix, window, cx);
5696        });
5697        assert_item_labels(&pane, ["A!", "B", "C*"], cx);
5698        cx.run_until_parked();
5699
5700        // Verify two-row layout is active
5701        let pinned_row_bounds = cx.debug_bounds("pinned_tabs_row");
5702        assert!(
5703            pinned_row_bounds.is_some(),
5704            "Two-row layout should be active when both pinned and unpinned tabs exist"
5705        );
5706
5707        // Verify pinned_tabs_border element exists (the right border after pinned tabs)
5708        let border_bounds = cx.debug_bounds("pinned_tabs_border");
5709        assert!(
5710            border_bounds.is_some(),
5711            "pinned_tabs_border should exist in two-row layout to show right border"
5712        );
5713    }
5714
5715    #[gpui::test]
5716    async fn test_pinning_active_tab_without_position_change_maintains_focus(
5717        cx: &mut TestAppContext,
5718    ) {
5719        init_test(cx);
5720        let fs = FakeFs::new(cx.executor());
5721
5722        let project = Project::test(fs, None, cx).await;
5723        let (workspace, cx) =
5724            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5725        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5726
5727        // Add A
5728        let item_a = add_labeled_item(&pane, "A", false, cx);
5729        assert_item_labels(&pane, ["A*"], cx);
5730
5731        // Add B
5732        add_labeled_item(&pane, "B", false, cx);
5733        assert_item_labels(&pane, ["A", "B*"], cx);
5734
5735        // Activate A again
5736        pane.update_in(cx, |pane, window, cx| {
5737            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
5738            pane.activate_item(ix, true, true, window, cx);
5739        });
5740        assert_item_labels(&pane, ["A*", "B"], cx);
5741
5742        // Pin A - remains active
5743        pane.update_in(cx, |pane, window, cx| {
5744            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
5745            pane.pin_tab_at(ix, window, cx);
5746        });
5747        assert_item_labels(&pane, ["A*!", "B"], cx);
5748
5749        // Unpin A - remain active
5750        pane.update_in(cx, |pane, window, cx| {
5751            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
5752            pane.unpin_tab_at(ix, window, cx);
5753        });
5754        assert_item_labels(&pane, ["A*", "B"], cx);
5755    }
5756
5757    #[gpui::test]
5758    async fn test_pinning_active_tab_with_position_change_maintains_focus(cx: &mut TestAppContext) {
5759        init_test(cx);
5760        let fs = FakeFs::new(cx.executor());
5761
5762        let project = Project::test(fs, None, cx).await;
5763        let (workspace, cx) =
5764            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5765        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5766
5767        // Add A, B, C
5768        add_labeled_item(&pane, "A", false, cx);
5769        add_labeled_item(&pane, "B", false, cx);
5770        let item_c = add_labeled_item(&pane, "C", false, cx);
5771        assert_item_labels(&pane, ["A", "B", "C*"], cx);
5772
5773        // Pin C - moves to pinned area, remains active
5774        pane.update_in(cx, |pane, window, cx| {
5775            let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
5776            pane.pin_tab_at(ix, window, cx);
5777        });
5778        assert_item_labels(&pane, ["C*!", "A", "B"], cx);
5779
5780        // Unpin C - moves after pinned area, remains active
5781        pane.update_in(cx, |pane, window, cx| {
5782            let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
5783            pane.unpin_tab_at(ix, window, cx);
5784        });
5785        assert_item_labels(&pane, ["C*", "A", "B"], cx);
5786    }
5787
5788    #[gpui::test]
5789    async fn test_pinning_inactive_tab_without_position_change_preserves_existing_focus(
5790        cx: &mut TestAppContext,
5791    ) {
5792        init_test(cx);
5793        let fs = FakeFs::new(cx.executor());
5794
5795        let project = Project::test(fs, None, cx).await;
5796        let (workspace, cx) =
5797            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5798        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5799
5800        // Add A, B
5801        let item_a = add_labeled_item(&pane, "A", false, cx);
5802        add_labeled_item(&pane, "B", false, cx);
5803        assert_item_labels(&pane, ["A", "B*"], cx);
5804
5805        // Pin A - already in pinned area, B remains active
5806        pane.update_in(cx, |pane, window, cx| {
5807            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
5808            pane.pin_tab_at(ix, window, cx);
5809        });
5810        assert_item_labels(&pane, ["A!", "B*"], cx);
5811
5812        // Unpin A - stays in place, B remains active
5813        pane.update_in(cx, |pane, window, cx| {
5814            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
5815            pane.unpin_tab_at(ix, window, cx);
5816        });
5817        assert_item_labels(&pane, ["A", "B*"], cx);
5818    }
5819
5820    #[gpui::test]
5821    async fn test_pinning_inactive_tab_with_position_change_preserves_existing_focus(
5822        cx: &mut TestAppContext,
5823    ) {
5824        init_test(cx);
5825        let fs = FakeFs::new(cx.executor());
5826
5827        let project = Project::test(fs, None, cx).await;
5828        let (workspace, cx) =
5829            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5830        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5831
5832        // Add A, B, C
5833        add_labeled_item(&pane, "A", false, cx);
5834        let item_b = add_labeled_item(&pane, "B", false, cx);
5835        let item_c = add_labeled_item(&pane, "C", false, cx);
5836        assert_item_labels(&pane, ["A", "B", "C*"], cx);
5837
5838        // Activate B
5839        pane.update_in(cx, |pane, window, cx| {
5840            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
5841            pane.activate_item(ix, true, true, window, cx);
5842        });
5843        assert_item_labels(&pane, ["A", "B*", "C"], cx);
5844
5845        // Pin C - moves to pinned area, B remains active
5846        pane.update_in(cx, |pane, window, cx| {
5847            let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
5848            pane.pin_tab_at(ix, window, cx);
5849        });
5850        assert_item_labels(&pane, ["C!", "A", "B*"], cx);
5851
5852        // Unpin C - moves after pinned area, B remains active
5853        pane.update_in(cx, |pane, window, cx| {
5854            let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
5855            pane.unpin_tab_at(ix, window, cx);
5856        });
5857        assert_item_labels(&pane, ["C", "A", "B*"], cx);
5858    }
5859
5860    #[gpui::test]
5861    async fn test_handle_tab_drop_respects_is_pane_target(cx: &mut TestAppContext) {
5862        init_test(cx);
5863        let fs = FakeFs::new(cx.executor());
5864        let project = Project::test(fs, None, cx).await;
5865        let (workspace, cx) =
5866            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5867        let source_pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5868
5869        let item_a = add_labeled_item(&source_pane, "A", false, cx);
5870        let item_b = add_labeled_item(&source_pane, "B", false, cx);
5871
5872        let target_pane = workspace.update_in(cx, |workspace, window, cx| {
5873            workspace.split_pane(source_pane.clone(), SplitDirection::Right, window, cx)
5874        });
5875
5876        let custom_item = target_pane.update_in(cx, |pane, window, cx| {
5877            let custom_item = Box::new(cx.new(CustomDropHandlingItem::new));
5878            pane.add_item(custom_item.clone(), true, true, None, window, cx);
5879            custom_item
5880        });
5881
5882        let moved_item_id = item_a.item_id();
5883        let other_item_id = item_b.item_id();
5884        let custom_item_id = custom_item.item_id();
5885
5886        let pane_item_ids = |pane: &Entity<Pane>, cx: &mut VisualTestContext| {
5887            pane.read_with(cx, |pane, _| {
5888                pane.items().map(|item| item.item_id()).collect::<Vec<_>>()
5889            })
5890        };
5891
5892        let source_before_item_ids = pane_item_ids(&source_pane, cx);
5893        assert_eq!(source_before_item_ids, vec![moved_item_id, other_item_id]);
5894
5895        let target_before_item_ids = pane_item_ids(&target_pane, cx);
5896        assert_eq!(target_before_item_ids, vec![custom_item_id]);
5897
5898        let dragged_tab = DraggedTab {
5899            pane: source_pane.clone(),
5900            item: item_a.boxed_clone(),
5901            ix: 0,
5902            detail: 0,
5903            is_active: true,
5904        };
5905
5906        // Dropping item_a onto the target pane itself means the
5907        // custom item handles the drop and no tab move should occur
5908        target_pane.update_in(cx, |pane, window, cx| {
5909            pane.handle_tab_drop(&dragged_tab, pane.active_item_index(), true, window, cx);
5910        });
5911        cx.run_until_parked();
5912
5913        assert_eq!(
5914            custom_item.read_with(cx, |item, _| item.drop_call_count()),
5915            1
5916        );
5917        assert_eq!(pane_item_ids(&source_pane, cx), source_before_item_ids);
5918        assert_eq!(pane_item_ids(&target_pane, cx), target_before_item_ids);
5919
5920        // Dropping item_a onto the tab target means the custom handler
5921        // should be skipped and the pane's default tab drop behavior should run.
5922        target_pane.update_in(cx, |pane, window, cx| {
5923            pane.handle_tab_drop(&dragged_tab, pane.active_item_index(), false, window, cx);
5924        });
5925        cx.run_until_parked();
5926
5927        assert_eq!(
5928            custom_item.read_with(cx, |item, _| item.drop_call_count()),
5929            1
5930        );
5931        assert_eq!(pane_item_ids(&source_pane, cx), vec![other_item_id]);
5932
5933        let target_item_ids = pane_item_ids(&target_pane, cx);
5934        assert_eq!(target_item_ids, vec![moved_item_id, custom_item_id]);
5935    }
5936
5937    #[gpui::test]
5938    async fn test_drag_unpinned_tab_to_split_creates_pane_with_unpinned_tab(
5939        cx: &mut TestAppContext,
5940    ) {
5941        init_test(cx);
5942        let fs = FakeFs::new(cx.executor());
5943
5944        let project = Project::test(fs, None, cx).await;
5945        let (workspace, cx) =
5946            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5947        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5948
5949        // Add A, B. Pin B. Activate A
5950        let item_a = add_labeled_item(&pane_a, "A", false, cx);
5951        let item_b = add_labeled_item(&pane_a, "B", false, cx);
5952
5953        pane_a.update_in(cx, |pane, window, cx| {
5954            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
5955            pane.pin_tab_at(ix, window, cx);
5956
5957            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
5958            pane.activate_item(ix, true, true, window, cx);
5959        });
5960
5961        // Drag A to create new split
5962        pane_a.update_in(cx, |pane, window, cx| {
5963            pane.drag_split_direction = Some(SplitDirection::Right);
5964
5965            let dragged_tab = DraggedTab {
5966                pane: pane_a.clone(),
5967                item: item_a.boxed_clone(),
5968                ix: 0,
5969                detail: 0,
5970                is_active: true,
5971            };
5972            pane.handle_tab_drop(&dragged_tab, 0, true, window, cx);
5973        });
5974
5975        // A should be moved to new pane. B should remain pinned, A should not be pinned
5976        let (pane_a, pane_b) = workspace.read_with(cx, |workspace, _| {
5977            let panes = workspace.panes();
5978            (panes[0].clone(), panes[1].clone())
5979        });
5980        assert_item_labels(&pane_a, ["B*!"], cx);
5981        assert_item_labels(&pane_b, ["A*"], cx);
5982    }
5983
5984    #[gpui::test]
5985    async fn test_drag_pinned_tab_to_split_creates_pane_with_pinned_tab(cx: &mut TestAppContext) {
5986        init_test(cx);
5987        let fs = FakeFs::new(cx.executor());
5988
5989        let project = Project::test(fs, None, cx).await;
5990        let (workspace, cx) =
5991            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5992        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5993
5994        // Add A, B. Pin both. Activate A
5995        let item_a = add_labeled_item(&pane_a, "A", false, cx);
5996        let item_b = add_labeled_item(&pane_a, "B", false, cx);
5997
5998        pane_a.update_in(cx, |pane, window, cx| {
5999            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
6000            pane.pin_tab_at(ix, window, cx);
6001
6002            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
6003            pane.pin_tab_at(ix, window, cx);
6004
6005            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
6006            pane.activate_item(ix, true, true, window, cx);
6007        });
6008        assert_item_labels(&pane_a, ["A*!", "B!"], cx);
6009
6010        // Drag A to create new split
6011        pane_a.update_in(cx, |pane, window, cx| {
6012            pane.drag_split_direction = Some(SplitDirection::Right);
6013
6014            let dragged_tab = DraggedTab {
6015                pane: pane_a.clone(),
6016                item: item_a.boxed_clone(),
6017                ix: 0,
6018                detail: 0,
6019                is_active: true,
6020            };
6021            pane.handle_tab_drop(&dragged_tab, 0, true, window, cx);
6022        });
6023
6024        // A should be moved to new pane. Both A and B should still be pinned
6025        let (pane_a, pane_b) = workspace.read_with(cx, |workspace, _| {
6026            let panes = workspace.panes();
6027            (panes[0].clone(), panes[1].clone())
6028        });
6029        assert_item_labels(&pane_a, ["B*!"], cx);
6030        assert_item_labels(&pane_b, ["A*!"], cx);
6031    }
6032
6033    #[gpui::test]
6034    async fn test_drag_pinned_tab_into_existing_panes_pinned_region(cx: &mut TestAppContext) {
6035        init_test(cx);
6036        let fs = FakeFs::new(cx.executor());
6037
6038        let project = Project::test(fs, None, cx).await;
6039        let (workspace, cx) =
6040            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6041        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
6042
6043        // Add A to pane A and pin
6044        let item_a = add_labeled_item(&pane_a, "A", false, cx);
6045        pane_a.update_in(cx, |pane, window, cx| {
6046            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
6047            pane.pin_tab_at(ix, window, cx);
6048        });
6049        assert_item_labels(&pane_a, ["A*!"], cx);
6050
6051        // Add B to pane B and pin
6052        let pane_b = workspace.update_in(cx, |workspace, window, cx| {
6053            workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx)
6054        });
6055        let item_b = add_labeled_item(&pane_b, "B", false, cx);
6056        pane_b.update_in(cx, |pane, window, cx| {
6057            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
6058            pane.pin_tab_at(ix, window, cx);
6059        });
6060        assert_item_labels(&pane_b, ["B*!"], cx);
6061
6062        // Move A from pane A to pane B's pinned region
6063        pane_b.update_in(cx, |pane, window, cx| {
6064            let dragged_tab = DraggedTab {
6065                pane: pane_a.clone(),
6066                item: item_a.boxed_clone(),
6067                ix: 0,
6068                detail: 0,
6069                is_active: true,
6070            };
6071            pane.handle_tab_drop(&dragged_tab, 0, false, window, cx);
6072        });
6073
6074        // A should stay pinned
6075        assert_item_labels(&pane_a, [], cx);
6076        assert_item_labels(&pane_b, ["A*!", "B!"], cx);
6077    }
6078
6079    #[gpui::test]
6080    async fn test_drag_pinned_tab_into_existing_panes_unpinned_region(cx: &mut TestAppContext) {
6081        init_test(cx);
6082        let fs = FakeFs::new(cx.executor());
6083
6084        let project = Project::test(fs, None, cx).await;
6085        let (workspace, cx) =
6086            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6087        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
6088
6089        // Add A to pane A and pin
6090        let item_a = add_labeled_item(&pane_a, "A", false, cx);
6091        pane_a.update_in(cx, |pane, window, cx| {
6092            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
6093            pane.pin_tab_at(ix, window, cx);
6094        });
6095        assert_item_labels(&pane_a, ["A*!"], cx);
6096
6097        // Create pane B with pinned item B
6098        let pane_b = workspace.update_in(cx, |workspace, window, cx| {
6099            workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx)
6100        });
6101        let item_b = add_labeled_item(&pane_b, "B", false, cx);
6102        assert_item_labels(&pane_b, ["B*"], cx);
6103
6104        pane_b.update_in(cx, |pane, window, cx| {
6105            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
6106            pane.pin_tab_at(ix, window, cx);
6107        });
6108        assert_item_labels(&pane_b, ["B*!"], cx);
6109
6110        // Move A from pane A to pane B's unpinned region
6111        pane_b.update_in(cx, |pane, window, cx| {
6112            let dragged_tab = DraggedTab {
6113                pane: pane_a.clone(),
6114                item: item_a.boxed_clone(),
6115                ix: 0,
6116                detail: 0,
6117                is_active: true,
6118            };
6119            pane.handle_tab_drop(&dragged_tab, 1, false, window, cx);
6120        });
6121
6122        // A should become pinned
6123        assert_item_labels(&pane_a, [], cx);
6124        assert_item_labels(&pane_b, ["B!", "A*"], cx);
6125    }
6126
6127    #[gpui::test]
6128    async fn test_drag_pinned_tab_into_existing_panes_first_position_with_no_pinned_tabs(
6129        cx: &mut TestAppContext,
6130    ) {
6131        init_test(cx);
6132        let fs = FakeFs::new(cx.executor());
6133
6134        let project = Project::test(fs, None, cx).await;
6135        let (workspace, cx) =
6136            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6137        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
6138
6139        // Add A to pane A and pin
6140        let item_a = add_labeled_item(&pane_a, "A", false, cx);
6141        pane_a.update_in(cx, |pane, window, cx| {
6142            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
6143            pane.pin_tab_at(ix, window, cx);
6144        });
6145        assert_item_labels(&pane_a, ["A*!"], cx);
6146
6147        // Add B to pane B
6148        let pane_b = workspace.update_in(cx, |workspace, window, cx| {
6149            workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx)
6150        });
6151        add_labeled_item(&pane_b, "B", false, cx);
6152        assert_item_labels(&pane_b, ["B*"], cx);
6153
6154        // Move A from pane A to position 0 in pane B, indicating it should stay pinned
6155        pane_b.update_in(cx, |pane, window, cx| {
6156            let dragged_tab = DraggedTab {
6157                pane: pane_a.clone(),
6158                item: item_a.boxed_clone(),
6159                ix: 0,
6160                detail: 0,
6161                is_active: true,
6162            };
6163            pane.handle_tab_drop(&dragged_tab, 0, false, window, cx);
6164        });
6165
6166        // A should stay pinned
6167        assert_item_labels(&pane_a, [], cx);
6168        assert_item_labels(&pane_b, ["A*!", "B"], cx);
6169    }
6170
6171    #[gpui::test]
6172    async fn test_drag_pinned_tab_into_existing_pane_at_max_capacity_closes_unpinned_tabs(
6173        cx: &mut TestAppContext,
6174    ) {
6175        init_test(cx);
6176        let fs = FakeFs::new(cx.executor());
6177
6178        let project = Project::test(fs, None, cx).await;
6179        let (workspace, cx) =
6180            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6181        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
6182        set_max_tabs(cx, Some(2));
6183
6184        // Add A, B to pane A. Pin both
6185        let item_a = add_labeled_item(&pane_a, "A", false, cx);
6186        let item_b = add_labeled_item(&pane_a, "B", false, cx);
6187        pane_a.update_in(cx, |pane, window, cx| {
6188            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
6189            pane.pin_tab_at(ix, window, cx);
6190
6191            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
6192            pane.pin_tab_at(ix, window, cx);
6193        });
6194        assert_item_labels(&pane_a, ["A!", "B*!"], cx);
6195
6196        // Add C, D to pane B. Pin both
6197        let pane_b = workspace.update_in(cx, |workspace, window, cx| {
6198            workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx)
6199        });
6200        let item_c = add_labeled_item(&pane_b, "C", false, cx);
6201        let item_d = add_labeled_item(&pane_b, "D", false, cx);
6202        pane_b.update_in(cx, |pane, window, cx| {
6203            let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
6204            pane.pin_tab_at(ix, window, cx);
6205
6206            let ix = pane.index_for_item_id(item_d.item_id()).unwrap();
6207            pane.pin_tab_at(ix, window, cx);
6208        });
6209        assert_item_labels(&pane_b, ["C!", "D*!"], cx);
6210
6211        // Add a third unpinned item to pane B (exceeds max tabs), but is allowed,
6212        // as we allow 1 tab over max if the others are pinned or dirty
6213        add_labeled_item(&pane_b, "E", false, cx);
6214        assert_item_labels(&pane_b, ["C!", "D!", "E*"], cx);
6215
6216        // Drag pinned A from pane A to position 0 in pane B
6217        pane_b.update_in(cx, |pane, window, cx| {
6218            let dragged_tab = DraggedTab {
6219                pane: pane_a.clone(),
6220                item: item_a.boxed_clone(),
6221                ix: 0,
6222                detail: 0,
6223                is_active: true,
6224            };
6225            pane.handle_tab_drop(&dragged_tab, 0, false, window, cx);
6226        });
6227
6228        // E (unpinned) should be closed, leaving 3 pinned items
6229        assert_item_labels(&pane_a, ["B*!"], cx);
6230        assert_item_labels(&pane_b, ["A*!", "C!", "D!"], cx);
6231    }
6232
6233    #[gpui::test]
6234    async fn test_drag_last_pinned_tab_to_same_position_stays_pinned(cx: &mut TestAppContext) {
6235        init_test(cx);
6236        let fs = FakeFs::new(cx.executor());
6237
6238        let project = Project::test(fs, None, cx).await;
6239        let (workspace, cx) =
6240            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6241        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
6242
6243        // Add A to pane A and pin it
6244        let item_a = add_labeled_item(&pane_a, "A", false, cx);
6245        pane_a.update_in(cx, |pane, window, cx| {
6246            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
6247            pane.pin_tab_at(ix, window, cx);
6248        });
6249        assert_item_labels(&pane_a, ["A*!"], cx);
6250
6251        // Drag pinned A to position 1 (directly to the right) in the same pane
6252        pane_a.update_in(cx, |pane, window, cx| {
6253            let dragged_tab = DraggedTab {
6254                pane: pane_a.clone(),
6255                item: item_a.boxed_clone(),
6256                ix: 0,
6257                detail: 0,
6258                is_active: true,
6259            };
6260            pane.handle_tab_drop(&dragged_tab, 1, false, window, cx);
6261        });
6262
6263        // A should still be pinned and active
6264        assert_item_labels(&pane_a, ["A*!"], cx);
6265    }
6266
6267    #[gpui::test]
6268    async fn test_drag_pinned_tab_beyond_last_pinned_tab_in_same_pane_stays_pinned(
6269        cx: &mut TestAppContext,
6270    ) {
6271        init_test(cx);
6272        let fs = FakeFs::new(cx.executor());
6273
6274        let project = Project::test(fs, None, cx).await;
6275        let (workspace, cx) =
6276            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6277        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
6278
6279        // Add A, B to pane A and pin both
6280        let item_a = add_labeled_item(&pane_a, "A", false, cx);
6281        let item_b = add_labeled_item(&pane_a, "B", false, cx);
6282        pane_a.update_in(cx, |pane, window, cx| {
6283            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
6284            pane.pin_tab_at(ix, window, cx);
6285
6286            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
6287            pane.pin_tab_at(ix, window, cx);
6288        });
6289        assert_item_labels(&pane_a, ["A!", "B*!"], cx);
6290
6291        // Drag pinned A right of B in the same pane
6292        pane_a.update_in(cx, |pane, window, cx| {
6293            let dragged_tab = DraggedTab {
6294                pane: pane_a.clone(),
6295                item: item_a.boxed_clone(),
6296                ix: 0,
6297                detail: 0,
6298                is_active: true,
6299            };
6300            pane.handle_tab_drop(&dragged_tab, 2, false, window, cx);
6301        });
6302
6303        // A stays pinned
6304        assert_item_labels(&pane_a, ["B!", "A*!"], cx);
6305    }
6306
6307    #[gpui::test]
6308    async fn test_dragging_pinned_tab_onto_unpinned_tab_reduces_unpinned_tab_count(
6309        cx: &mut TestAppContext,
6310    ) {
6311        init_test(cx);
6312        let fs = FakeFs::new(cx.executor());
6313
6314        let project = Project::test(fs, None, cx).await;
6315        let (workspace, cx) =
6316            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6317        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
6318
6319        // Add A, B to pane A and pin A
6320        let item_a = add_labeled_item(&pane_a, "A", false, cx);
6321        add_labeled_item(&pane_a, "B", false, cx);
6322        pane_a.update_in(cx, |pane, window, cx| {
6323            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
6324            pane.pin_tab_at(ix, window, cx);
6325        });
6326        assert_item_labels(&pane_a, ["A!", "B*"], cx);
6327
6328        // Drag pinned A on top of B in the same pane, which changes tab order to B, A
6329        pane_a.update_in(cx, |pane, window, cx| {
6330            let dragged_tab = DraggedTab {
6331                pane: pane_a.clone(),
6332                item: item_a.boxed_clone(),
6333                ix: 0,
6334                detail: 0,
6335                is_active: true,
6336            };
6337            pane.handle_tab_drop(&dragged_tab, 1, false, window, cx);
6338        });
6339
6340        // Neither are pinned
6341        assert_item_labels(&pane_a, ["B", "A*"], cx);
6342    }
6343
6344    #[gpui::test]
6345    async fn test_drag_pinned_tab_beyond_unpinned_tab_in_same_pane_becomes_unpinned(
6346        cx: &mut TestAppContext,
6347    ) {
6348        init_test(cx);
6349        let fs = FakeFs::new(cx.executor());
6350
6351        let project = Project::test(fs, None, cx).await;
6352        let (workspace, cx) =
6353            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6354        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
6355
6356        // Add A, B to pane A and pin A
6357        let item_a = add_labeled_item(&pane_a, "A", false, cx);
6358        add_labeled_item(&pane_a, "B", false, cx);
6359        pane_a.update_in(cx, |pane, window, cx| {
6360            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
6361            pane.pin_tab_at(ix, window, cx);
6362        });
6363        assert_item_labels(&pane_a, ["A!", "B*"], cx);
6364
6365        // Drag pinned A right of B in the same pane
6366        pane_a.update_in(cx, |pane, window, cx| {
6367            let dragged_tab = DraggedTab {
6368                pane: pane_a.clone(),
6369                item: item_a.boxed_clone(),
6370                ix: 0,
6371                detail: 0,
6372                is_active: true,
6373            };
6374            pane.handle_tab_drop(&dragged_tab, 2, false, window, cx);
6375        });
6376
6377        // A becomes unpinned
6378        assert_item_labels(&pane_a, ["B", "A*"], cx);
6379    }
6380
6381    #[gpui::test]
6382    async fn test_drag_unpinned_tab_in_front_of_pinned_tab_in_same_pane_becomes_pinned(
6383        cx: &mut TestAppContext,
6384    ) {
6385        init_test(cx);
6386        let fs = FakeFs::new(cx.executor());
6387
6388        let project = Project::test(fs, None, cx).await;
6389        let (workspace, cx) =
6390            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6391        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
6392
6393        // Add A, B to pane A and pin A
6394        let item_a = add_labeled_item(&pane_a, "A", false, cx);
6395        let item_b = add_labeled_item(&pane_a, "B", false, cx);
6396        pane_a.update_in(cx, |pane, window, cx| {
6397            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
6398            pane.pin_tab_at(ix, window, cx);
6399        });
6400        assert_item_labels(&pane_a, ["A!", "B*"], cx);
6401
6402        // Drag pinned B left of A in the same pane
6403        pane_a.update_in(cx, |pane, window, cx| {
6404            let dragged_tab = DraggedTab {
6405                pane: pane_a.clone(),
6406                item: item_b.boxed_clone(),
6407                ix: 1,
6408                detail: 0,
6409                is_active: true,
6410            };
6411            pane.handle_tab_drop(&dragged_tab, 0, false, window, cx);
6412        });
6413
6414        // A becomes unpinned
6415        assert_item_labels(&pane_a, ["B*!", "A!"], cx);
6416    }
6417
6418    #[gpui::test]
6419    async fn test_drag_unpinned_tab_to_the_pinned_region_stays_pinned(cx: &mut TestAppContext) {
6420        init_test(cx);
6421        let fs = FakeFs::new(cx.executor());
6422
6423        let project = Project::test(fs, None, cx).await;
6424        let (workspace, cx) =
6425            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6426        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
6427
6428        // Add A, B, C to pane A and pin A
6429        let item_a = add_labeled_item(&pane_a, "A", false, cx);
6430        add_labeled_item(&pane_a, "B", false, cx);
6431        let item_c = add_labeled_item(&pane_a, "C", false, cx);
6432        pane_a.update_in(cx, |pane, window, cx| {
6433            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
6434            pane.pin_tab_at(ix, window, cx);
6435        });
6436        assert_item_labels(&pane_a, ["A!", "B", "C*"], cx);
6437
6438        // Drag pinned C left of B in the same pane
6439        pane_a.update_in(cx, |pane, window, cx| {
6440            let dragged_tab = DraggedTab {
6441                pane: pane_a.clone(),
6442                item: item_c.boxed_clone(),
6443                ix: 2,
6444                detail: 0,
6445                is_active: true,
6446            };
6447            pane.handle_tab_drop(&dragged_tab, 1, false, window, cx);
6448        });
6449
6450        // A stays pinned, B and C remain unpinned
6451        assert_item_labels(&pane_a, ["A!", "C*", "B"], cx);
6452    }
6453
6454    #[gpui::test]
6455    async fn test_drag_unpinned_tab_into_existing_panes_pinned_region(cx: &mut TestAppContext) {
6456        init_test(cx);
6457        let fs = FakeFs::new(cx.executor());
6458
6459        let project = Project::test(fs, None, cx).await;
6460        let (workspace, cx) =
6461            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6462        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
6463
6464        // Add unpinned item A to pane A
6465        let item_a = add_labeled_item(&pane_a, "A", false, cx);
6466        assert_item_labels(&pane_a, ["A*"], cx);
6467
6468        // Create pane B with pinned item B
6469        let pane_b = workspace.update_in(cx, |workspace, window, cx| {
6470            workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx)
6471        });
6472        let item_b = add_labeled_item(&pane_b, "B", false, cx);
6473        pane_b.update_in(cx, |pane, window, cx| {
6474            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
6475            pane.pin_tab_at(ix, window, cx);
6476        });
6477        assert_item_labels(&pane_b, ["B*!"], cx);
6478
6479        // Move A from pane A to pane B's pinned region
6480        pane_b.update_in(cx, |pane, window, cx| {
6481            let dragged_tab = DraggedTab {
6482                pane: pane_a.clone(),
6483                item: item_a.boxed_clone(),
6484                ix: 0,
6485                detail: 0,
6486                is_active: true,
6487            };
6488            pane.handle_tab_drop(&dragged_tab, 0, false, window, cx);
6489        });
6490
6491        // A should become pinned since it was dropped in the pinned region
6492        assert_item_labels(&pane_a, [], cx);
6493        assert_item_labels(&pane_b, ["A*!", "B!"], cx);
6494    }
6495
6496    #[gpui::test]
6497    async fn test_drag_unpinned_tab_into_existing_panes_unpinned_region(cx: &mut TestAppContext) {
6498        init_test(cx);
6499        let fs = FakeFs::new(cx.executor());
6500
6501        let project = Project::test(fs, None, cx).await;
6502        let (workspace, cx) =
6503            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6504        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
6505
6506        // Add unpinned item A to pane A
6507        let item_a = add_labeled_item(&pane_a, "A", false, cx);
6508        assert_item_labels(&pane_a, ["A*"], cx);
6509
6510        // Create pane B with one pinned item B
6511        let pane_b = workspace.update_in(cx, |workspace, window, cx| {
6512            workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx)
6513        });
6514        let item_b = add_labeled_item(&pane_b, "B", false, cx);
6515        pane_b.update_in(cx, |pane, window, cx| {
6516            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
6517            pane.pin_tab_at(ix, window, cx);
6518        });
6519        assert_item_labels(&pane_b, ["B*!"], cx);
6520
6521        // Move A from pane A to pane B's unpinned region
6522        pane_b.update_in(cx, |pane, window, cx| {
6523            let dragged_tab = DraggedTab {
6524                pane: pane_a.clone(),
6525                item: item_a.boxed_clone(),
6526                ix: 0,
6527                detail: 0,
6528                is_active: true,
6529            };
6530            pane.handle_tab_drop(&dragged_tab, 1, true, window, cx);
6531        });
6532
6533        // A should remain unpinned since it was dropped outside the pinned region
6534        assert_item_labels(&pane_a, [], cx);
6535        assert_item_labels(&pane_b, ["B!", "A*"], cx);
6536    }
6537
6538    #[gpui::test]
6539    async fn test_drag_pinned_tab_throughout_entire_range_of_pinned_tabs_both_directions(
6540        cx: &mut TestAppContext,
6541    ) {
6542        init_test(cx);
6543        let fs = FakeFs::new(cx.executor());
6544
6545        let project = Project::test(fs, None, cx).await;
6546        let (workspace, cx) =
6547            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6548        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
6549
6550        // Add A, B, C and pin all
6551        let item_a = add_labeled_item(&pane_a, "A", false, cx);
6552        let item_b = add_labeled_item(&pane_a, "B", false, cx);
6553        let item_c = add_labeled_item(&pane_a, "C", false, cx);
6554        assert_item_labels(&pane_a, ["A", "B", "C*"], cx);
6555
6556        pane_a.update_in(cx, |pane, window, cx| {
6557            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
6558            pane.pin_tab_at(ix, window, cx);
6559
6560            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
6561            pane.pin_tab_at(ix, window, cx);
6562
6563            let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
6564            pane.pin_tab_at(ix, window, cx);
6565        });
6566        assert_item_labels(&pane_a, ["A!", "B!", "C*!"], cx);
6567
6568        // Move A to right of B
6569        pane_a.update_in(cx, |pane, window, cx| {
6570            let dragged_tab = DraggedTab {
6571                pane: pane_a.clone(),
6572                item: item_a.boxed_clone(),
6573                ix: 0,
6574                detail: 0,
6575                is_active: true,
6576            };
6577            pane.handle_tab_drop(&dragged_tab, 1, false, window, cx);
6578        });
6579
6580        // A should be after B and all are pinned
6581        assert_item_labels(&pane_a, ["B!", "A*!", "C!"], cx);
6582
6583        // Move A to right of C
6584        pane_a.update_in(cx, |pane, window, cx| {
6585            let dragged_tab = DraggedTab {
6586                pane: pane_a.clone(),
6587                item: item_a.boxed_clone(),
6588                ix: 1,
6589                detail: 0,
6590                is_active: true,
6591            };
6592            pane.handle_tab_drop(&dragged_tab, 2, false, window, cx);
6593        });
6594
6595        // A should be after C and all are pinned
6596        assert_item_labels(&pane_a, ["B!", "C!", "A*!"], cx);
6597
6598        // Move A to left of C
6599        pane_a.update_in(cx, |pane, window, cx| {
6600            let dragged_tab = DraggedTab {
6601                pane: pane_a.clone(),
6602                item: item_a.boxed_clone(),
6603                ix: 2,
6604                detail: 0,
6605                is_active: true,
6606            };
6607            pane.handle_tab_drop(&dragged_tab, 1, false, window, cx);
6608        });
6609
6610        // A should be before C and all are pinned
6611        assert_item_labels(&pane_a, ["B!", "A*!", "C!"], cx);
6612
6613        // Move A to left of B
6614        pane_a.update_in(cx, |pane, window, cx| {
6615            let dragged_tab = DraggedTab {
6616                pane: pane_a.clone(),
6617                item: item_a.boxed_clone(),
6618                ix: 1,
6619                detail: 0,
6620                is_active: true,
6621            };
6622            pane.handle_tab_drop(&dragged_tab, 0, false, window, cx);
6623        });
6624
6625        // A should be before B and all are pinned
6626        assert_item_labels(&pane_a, ["A*!", "B!", "C!"], cx);
6627    }
6628
6629    #[gpui::test]
6630    async fn test_drag_first_tab_to_last_position(cx: &mut TestAppContext) {
6631        init_test(cx);
6632        let fs = FakeFs::new(cx.executor());
6633
6634        let project = Project::test(fs, None, cx).await;
6635        let (workspace, cx) =
6636            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6637        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
6638
6639        // Add A, B, C
6640        let item_a = add_labeled_item(&pane_a, "A", false, cx);
6641        add_labeled_item(&pane_a, "B", false, cx);
6642        add_labeled_item(&pane_a, "C", false, cx);
6643        assert_item_labels(&pane_a, ["A", "B", "C*"], cx);
6644
6645        // Move A to the end
6646        pane_a.update_in(cx, |pane, window, cx| {
6647            let dragged_tab = DraggedTab {
6648                pane: pane_a.clone(),
6649                item: item_a.boxed_clone(),
6650                ix: 0,
6651                detail: 0,
6652                is_active: true,
6653            };
6654            pane.handle_tab_drop(&dragged_tab, 2, false, window, cx);
6655        });
6656
6657        // A should be at the end
6658        assert_item_labels(&pane_a, ["B", "C", "A*"], cx);
6659    }
6660
6661    #[gpui::test]
6662    async fn test_drag_last_tab_to_first_position(cx: &mut TestAppContext) {
6663        init_test(cx);
6664        let fs = FakeFs::new(cx.executor());
6665
6666        let project = Project::test(fs, None, cx).await;
6667        let (workspace, cx) =
6668            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6669        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
6670
6671        // Add A, B, C
6672        add_labeled_item(&pane_a, "A", false, cx);
6673        add_labeled_item(&pane_a, "B", false, cx);
6674        let item_c = add_labeled_item(&pane_a, "C", false, cx);
6675        assert_item_labels(&pane_a, ["A", "B", "C*"], cx);
6676
6677        // Move C to the beginning
6678        pane_a.update_in(cx, |pane, window, cx| {
6679            let dragged_tab = DraggedTab {
6680                pane: pane_a.clone(),
6681                item: item_c.boxed_clone(),
6682                ix: 2,
6683                detail: 0,
6684                is_active: true,
6685            };
6686            pane.handle_tab_drop(&dragged_tab, 0, false, window, cx);
6687        });
6688
6689        // C should be at the beginning
6690        assert_item_labels(&pane_a, ["C*", "A", "B"], cx);
6691    }
6692
6693    #[gpui::test]
6694    async fn test_drag_tab_to_middle_tab_with_mouse_events(cx: &mut TestAppContext) {
6695        init_test(cx);
6696        let fs = FakeFs::new(cx.executor());
6697
6698        let project = Project::test(fs, None, cx).await;
6699        let (workspace, cx) =
6700            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6701        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
6702
6703        add_labeled_item(&pane, "A", false, cx);
6704        add_labeled_item(&pane, "B", false, cx);
6705        add_labeled_item(&pane, "C", false, cx);
6706        add_labeled_item(&pane, "D", false, cx);
6707        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
6708        cx.run_until_parked();
6709
6710        let tab_a_bounds = cx
6711            .debug_bounds("TAB-0")
6712            .expect("Tab A (index 0) should have debug bounds");
6713        let tab_c_bounds = cx
6714            .debug_bounds("TAB-2")
6715            .expect("Tab C (index 2) should have debug bounds");
6716
6717        cx.simulate_event(MouseDownEvent {
6718            position: tab_a_bounds.center(),
6719            button: MouseButton::Left,
6720            modifiers: Modifiers::default(),
6721            click_count: 1,
6722            first_mouse: false,
6723        });
6724        cx.run_until_parked();
6725        cx.simulate_event(MouseMoveEvent {
6726            position: tab_c_bounds.center(),
6727            pressed_button: Some(MouseButton::Left),
6728            modifiers: Modifiers::default(),
6729        });
6730        cx.run_until_parked();
6731        cx.simulate_event(MouseUpEvent {
6732            position: tab_c_bounds.center(),
6733            button: MouseButton::Left,
6734            modifiers: Modifiers::default(),
6735            click_count: 1,
6736        });
6737        cx.run_until_parked();
6738
6739        assert_item_labels(&pane, ["B", "C", "A*", "D"], cx);
6740    }
6741
6742    #[gpui::test]
6743    async fn test_drag_pinned_tab_when_show_pinned_tabs_in_separate_row_enabled(
6744        cx: &mut TestAppContext,
6745    ) {
6746        init_test(cx);
6747        set_pinned_tabs_separate_row(cx, true);
6748        let fs = FakeFs::new(cx.executor());
6749
6750        let project = Project::test(fs, None, cx).await;
6751        let (workspace, cx) =
6752            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6753        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
6754
6755        let item_a = add_labeled_item(&pane, "A", false, cx);
6756        let item_b = add_labeled_item(&pane, "B", false, cx);
6757        let item_c = add_labeled_item(&pane, "C", false, cx);
6758        let item_d = add_labeled_item(&pane, "D", false, cx);
6759
6760        pane.update_in(cx, |pane, window, cx| {
6761            pane.pin_tab_at(
6762                pane.index_for_item_id(item_a.item_id()).unwrap(),
6763                window,
6764                cx,
6765            );
6766            pane.pin_tab_at(
6767                pane.index_for_item_id(item_b.item_id()).unwrap(),
6768                window,
6769                cx,
6770            );
6771            pane.pin_tab_at(
6772                pane.index_for_item_id(item_c.item_id()).unwrap(),
6773                window,
6774                cx,
6775            );
6776            pane.pin_tab_at(
6777                pane.index_for_item_id(item_d.item_id()).unwrap(),
6778                window,
6779                cx,
6780            );
6781        });
6782        assert_item_labels(&pane, ["A!", "B!", "C!", "D*!"], cx);
6783        cx.run_until_parked();
6784
6785        let tab_a_bounds = cx
6786            .debug_bounds("TAB-0")
6787            .expect("Tab A (index 0) should have debug bounds");
6788        let tab_c_bounds = cx
6789            .debug_bounds("TAB-2")
6790            .expect("Tab C (index 2) should have debug bounds");
6791
6792        cx.simulate_event(MouseDownEvent {
6793            position: tab_a_bounds.center(),
6794            button: MouseButton::Left,
6795            modifiers: Modifiers::default(),
6796            click_count: 1,
6797            first_mouse: false,
6798        });
6799        cx.run_until_parked();
6800        cx.simulate_event(MouseMoveEvent {
6801            position: tab_c_bounds.center(),
6802            pressed_button: Some(MouseButton::Left),
6803            modifiers: Modifiers::default(),
6804        });
6805        cx.run_until_parked();
6806        cx.simulate_event(MouseUpEvent {
6807            position: tab_c_bounds.center(),
6808            button: MouseButton::Left,
6809            modifiers: Modifiers::default(),
6810            click_count: 1,
6811        });
6812        cx.run_until_parked();
6813
6814        assert_item_labels(&pane, ["B!", "C!", "A*!", "D!"], cx);
6815    }
6816
6817    #[gpui::test]
6818    async fn test_drag_unpinned_tab_when_show_pinned_tabs_in_separate_row_enabled(
6819        cx: &mut TestAppContext,
6820    ) {
6821        init_test(cx);
6822        set_pinned_tabs_separate_row(cx, true);
6823        let fs = FakeFs::new(cx.executor());
6824
6825        let project = Project::test(fs, None, cx).await;
6826        let (workspace, cx) =
6827            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6828        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
6829
6830        add_labeled_item(&pane, "A", false, cx);
6831        add_labeled_item(&pane, "B", false, cx);
6832        add_labeled_item(&pane, "C", false, cx);
6833        add_labeled_item(&pane, "D", false, cx);
6834        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
6835        cx.run_until_parked();
6836
6837        let tab_a_bounds = cx
6838            .debug_bounds("TAB-0")
6839            .expect("Tab A (index 0) should have debug bounds");
6840        let tab_c_bounds = cx
6841            .debug_bounds("TAB-2")
6842            .expect("Tab C (index 2) should have debug bounds");
6843
6844        cx.simulate_event(MouseDownEvent {
6845            position: tab_a_bounds.center(),
6846            button: MouseButton::Left,
6847            modifiers: Modifiers::default(),
6848            click_count: 1,
6849            first_mouse: false,
6850        });
6851        cx.run_until_parked();
6852        cx.simulate_event(MouseMoveEvent {
6853            position: tab_c_bounds.center(),
6854            pressed_button: Some(MouseButton::Left),
6855            modifiers: Modifiers::default(),
6856        });
6857        cx.run_until_parked();
6858        cx.simulate_event(MouseUpEvent {
6859            position: tab_c_bounds.center(),
6860            button: MouseButton::Left,
6861            modifiers: Modifiers::default(),
6862            click_count: 1,
6863        });
6864        cx.run_until_parked();
6865
6866        assert_item_labels(&pane, ["B", "C", "A*", "D"], cx);
6867    }
6868
6869    #[gpui::test]
6870    async fn test_drag_mixed_tabs_when_show_pinned_tabs_in_separate_row_enabled(
6871        cx: &mut TestAppContext,
6872    ) {
6873        init_test(cx);
6874        set_pinned_tabs_separate_row(cx, true);
6875        let fs = FakeFs::new(cx.executor());
6876
6877        let project = Project::test(fs, None, cx).await;
6878        let (workspace, cx) =
6879            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6880        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
6881
6882        let item_a = add_labeled_item(&pane, "A", false, cx);
6883        let item_b = add_labeled_item(&pane, "B", false, cx);
6884        add_labeled_item(&pane, "C", false, cx);
6885        add_labeled_item(&pane, "D", false, cx);
6886        add_labeled_item(&pane, "E", false, cx);
6887        add_labeled_item(&pane, "F", false, cx);
6888
6889        pane.update_in(cx, |pane, window, cx| {
6890            pane.pin_tab_at(
6891                pane.index_for_item_id(item_a.item_id()).unwrap(),
6892                window,
6893                cx,
6894            );
6895            pane.pin_tab_at(
6896                pane.index_for_item_id(item_b.item_id()).unwrap(),
6897                window,
6898                cx,
6899            );
6900        });
6901        assert_item_labels(&pane, ["A!", "B!", "C", "D", "E", "F*"], cx);
6902        cx.run_until_parked();
6903
6904        let tab_c_bounds = cx
6905            .debug_bounds("TAB-2")
6906            .expect("Tab C (index 2) should have debug bounds");
6907        let tab_e_bounds = cx
6908            .debug_bounds("TAB-4")
6909            .expect("Tab E (index 4) should have debug bounds");
6910
6911        cx.simulate_event(MouseDownEvent {
6912            position: tab_c_bounds.center(),
6913            button: MouseButton::Left,
6914            modifiers: Modifiers::default(),
6915            click_count: 1,
6916            first_mouse: false,
6917        });
6918        cx.run_until_parked();
6919        cx.simulate_event(MouseMoveEvent {
6920            position: tab_e_bounds.center(),
6921            pressed_button: Some(MouseButton::Left),
6922            modifiers: Modifiers::default(),
6923        });
6924        cx.run_until_parked();
6925        cx.simulate_event(MouseUpEvent {
6926            position: tab_e_bounds.center(),
6927            button: MouseButton::Left,
6928            modifiers: Modifiers::default(),
6929            click_count: 1,
6930        });
6931        cx.run_until_parked();
6932
6933        assert_item_labels(&pane, ["A!", "B!", "D", "E", "C*", "F"], cx);
6934    }
6935
6936    #[gpui::test]
6937    async fn test_middle_click_pinned_tab_does_not_close(cx: &mut TestAppContext) {
6938        init_test(cx);
6939        let fs = FakeFs::new(cx.executor());
6940
6941        let project = Project::test(fs, None, cx).await;
6942        let (workspace, cx) =
6943            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6944        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
6945
6946        let item_a = add_labeled_item(&pane, "A", false, cx);
6947        add_labeled_item(&pane, "B", false, cx);
6948
6949        pane.update_in(cx, |pane, window, cx| {
6950            pane.pin_tab_at(
6951                pane.index_for_item_id(item_a.item_id()).unwrap(),
6952                window,
6953                cx,
6954            );
6955        });
6956        assert_item_labels(&pane, ["A!", "B*"], cx);
6957        cx.run_until_parked();
6958
6959        let tab_a_bounds = cx
6960            .debug_bounds("TAB-0")
6961            .expect("Tab A (index 1) should have debug bounds");
6962        let tab_b_bounds = cx
6963            .debug_bounds("TAB-1")
6964            .expect("Tab B (index 2) should have debug bounds");
6965
6966        cx.simulate_event(MouseDownEvent {
6967            position: tab_a_bounds.center(),
6968            button: MouseButton::Middle,
6969            modifiers: Modifiers::default(),
6970            click_count: 1,
6971            first_mouse: false,
6972        });
6973
6974        cx.run_until_parked();
6975
6976        cx.simulate_event(MouseUpEvent {
6977            position: tab_a_bounds.center(),
6978            button: MouseButton::Middle,
6979            modifiers: Modifiers::default(),
6980            click_count: 1,
6981        });
6982
6983        cx.run_until_parked();
6984
6985        cx.simulate_event(MouseDownEvent {
6986            position: tab_b_bounds.center(),
6987            button: MouseButton::Middle,
6988            modifiers: Modifiers::default(),
6989            click_count: 1,
6990            first_mouse: false,
6991        });
6992
6993        cx.run_until_parked();
6994
6995        cx.simulate_event(MouseUpEvent {
6996            position: tab_b_bounds.center(),
6997            button: MouseButton::Middle,
6998            modifiers: Modifiers::default(),
6999            click_count: 1,
7000        });
7001
7002        cx.run_until_parked();
7003
7004        assert_item_labels(&pane, ["A*!"], cx);
7005    }
7006
7007    #[gpui::test]
7008    async fn test_double_click_pinned_tab_bar_empty_space_creates_new_tab(cx: &mut TestAppContext) {
7009        init_test(cx);
7010        let fs = FakeFs::new(cx.executor());
7011
7012        let project = Project::test(fs, None, cx).await;
7013        let (workspace, cx) =
7014            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7015        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
7016
7017        // The real NewFile handler lives in editor::init, which isn't initialized
7018        // in workspace tests. Register a global action handler that sets a flag so
7019        // we can verify the action is dispatched without depending on the editor crate.
7020        // TODO: If editor::init is ever available in workspace tests, remove this
7021        // flag and assert the resulting tab bar state directly instead.
7022        let new_file_dispatched = Rc::new(Cell::new(false));
7023        cx.update(|_, cx| {
7024            let new_file_dispatched = new_file_dispatched.clone();
7025            cx.on_action(move |_: &NewFile, _cx| {
7026                new_file_dispatched.set(true);
7027            });
7028        });
7029
7030        set_pinned_tabs_separate_row(cx, true);
7031
7032        let item_a = add_labeled_item(&pane, "A", false, cx);
7033        add_labeled_item(&pane, "B", false, cx);
7034
7035        pane.update_in(cx, |pane, window, cx| {
7036            let ix = pane
7037                .index_for_item_id(item_a.item_id())
7038                .expect("item A should exist");
7039            pane.pin_tab_at(ix, window, cx);
7040        });
7041        assert_item_labels(&pane, ["A!", "B*"], cx);
7042        cx.run_until_parked();
7043
7044        let pinned_drop_target_bounds = cx
7045            .debug_bounds("pinned_tabs_border")
7046            .expect("pinned_tabs_border should have debug bounds");
7047
7048        cx.simulate_event(MouseDownEvent {
7049            position: pinned_drop_target_bounds.center(),
7050            button: MouseButton::Left,
7051            modifiers: Modifiers::default(),
7052            click_count: 2,
7053            first_mouse: false,
7054        });
7055
7056        cx.run_until_parked();
7057
7058        cx.simulate_event(MouseUpEvent {
7059            position: pinned_drop_target_bounds.center(),
7060            button: MouseButton::Left,
7061            modifiers: Modifiers::default(),
7062            click_count: 2,
7063        });
7064
7065        cx.run_until_parked();
7066
7067        // TODO: If editor::init is ever available in workspace tests, replace this
7068        // with an assert_item_labels check that verifies a new tab is actually created.
7069        assert!(
7070            new_file_dispatched.get(),
7071            "Double-clicking pinned tab bar empty space should dispatch the new file action"
7072        );
7073    }
7074
7075    #[gpui::test]
7076    async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
7077        init_test(cx);
7078        let fs = FakeFs::new(cx.executor());
7079
7080        let project = Project::test(fs, None, cx).await;
7081        let (workspace, cx) =
7082            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7083        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
7084
7085        // 1. Add with a destination index
7086        //   a. Add before the active item
7087        set_labeled_items(&pane, ["A", "B*", "C"], cx);
7088        pane.update_in(cx, |pane, window, cx| {
7089            pane.add_item(
7090                Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
7091                false,
7092                false,
7093                Some(0),
7094                window,
7095                cx,
7096            );
7097        });
7098        assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
7099
7100        //   b. Add after the active item
7101        set_labeled_items(&pane, ["A", "B*", "C"], cx);
7102        pane.update_in(cx, |pane, window, cx| {
7103            pane.add_item(
7104                Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
7105                false,
7106                false,
7107                Some(2),
7108                window,
7109                cx,
7110            );
7111        });
7112        assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
7113
7114        //   c. Add at the end of the item list (including off the length)
7115        set_labeled_items(&pane, ["A", "B*", "C"], cx);
7116        pane.update_in(cx, |pane, window, cx| {
7117            pane.add_item(
7118                Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
7119                false,
7120                false,
7121                Some(5),
7122                window,
7123                cx,
7124            );
7125        });
7126        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
7127
7128        // 2. Add without a destination index
7129        //   a. Add with active item at the start of the item list
7130        set_labeled_items(&pane, ["A*", "B", "C"], cx);
7131        pane.update_in(cx, |pane, window, cx| {
7132            pane.add_item(
7133                Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
7134                false,
7135                false,
7136                None,
7137                window,
7138                cx,
7139            );
7140        });
7141        set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
7142
7143        //   b. Add with active item at the end of the item list
7144        set_labeled_items(&pane, ["A", "B", "C*"], cx);
7145        pane.update_in(cx, |pane, window, cx| {
7146            pane.add_item(
7147                Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
7148                false,
7149                false,
7150                None,
7151                window,
7152                cx,
7153            );
7154        });
7155        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
7156    }
7157
7158    #[gpui::test]
7159    async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
7160        init_test(cx);
7161        let fs = FakeFs::new(cx.executor());
7162
7163        let project = Project::test(fs, None, cx).await;
7164        let (workspace, cx) =
7165            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7166        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
7167
7168        // 1. Add with a destination index
7169        //   1a. Add before the active item
7170        let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
7171        pane.update_in(cx, |pane, window, cx| {
7172            pane.add_item(d, false, false, Some(0), window, cx);
7173        });
7174        assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
7175
7176        //   1b. Add after the active item
7177        let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
7178        pane.update_in(cx, |pane, window, cx| {
7179            pane.add_item(d, false, false, Some(2), window, cx);
7180        });
7181        assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
7182
7183        //   1c. Add at the end of the item list (including off the length)
7184        let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
7185        pane.update_in(cx, |pane, window, cx| {
7186            pane.add_item(a, false, false, Some(5), window, cx);
7187        });
7188        assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
7189
7190        //   1d. Add same item to active index
7191        let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
7192        pane.update_in(cx, |pane, window, cx| {
7193            pane.add_item(b, false, false, Some(1), window, cx);
7194        });
7195        assert_item_labels(&pane, ["A", "B*", "C"], cx);
7196
7197        //   1e. Add item to index after same item in last position
7198        let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
7199        pane.update_in(cx, |pane, window, cx| {
7200            pane.add_item(c, false, false, Some(2), window, cx);
7201        });
7202        assert_item_labels(&pane, ["A", "B", "C*"], cx);
7203
7204        // 2. Add without a destination index
7205        //   2a. Add with active item at the start of the item list
7206        let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
7207        pane.update_in(cx, |pane, window, cx| {
7208            pane.add_item(d, false, false, None, window, cx);
7209        });
7210        assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
7211
7212        //   2b. Add with active item at the end of the item list
7213        let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
7214        pane.update_in(cx, |pane, window, cx| {
7215            pane.add_item(a, false, false, None, window, cx);
7216        });
7217        assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
7218
7219        //   2c. Add active item to active item at end of list
7220        let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
7221        pane.update_in(cx, |pane, window, cx| {
7222            pane.add_item(c, false, false, None, window, cx);
7223        });
7224        assert_item_labels(&pane, ["A", "B", "C*"], cx);
7225
7226        //   2d. Add active item to active item at start of list
7227        let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
7228        pane.update_in(cx, |pane, window, cx| {
7229            pane.add_item(a, false, false, None, window, cx);
7230        });
7231        assert_item_labels(&pane, ["A*", "B", "C"], cx);
7232    }
7233
7234    #[gpui::test]
7235    async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
7236        init_test(cx);
7237        let fs = FakeFs::new(cx.executor());
7238
7239        let project = Project::test(fs, None, cx).await;
7240        let (workspace, cx) =
7241            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7242        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
7243
7244        // singleton view
7245        pane.update_in(cx, |pane, window, cx| {
7246            pane.add_item(
7247                Box::new(cx.new(|cx| {
7248                    TestItem::new(cx)
7249                        .with_buffer_kind(ItemBufferKind::Singleton)
7250                        .with_label("buffer 1")
7251                        .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
7252                })),
7253                false,
7254                false,
7255                None,
7256                window,
7257                cx,
7258            );
7259        });
7260        assert_item_labels(&pane, ["buffer 1*"], cx);
7261
7262        // new singleton view with the same project entry
7263        pane.update_in(cx, |pane, window, cx| {
7264            pane.add_item(
7265                Box::new(cx.new(|cx| {
7266                    TestItem::new(cx)
7267                        .with_buffer_kind(ItemBufferKind::Singleton)
7268                        .with_label("buffer 1")
7269                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7270                })),
7271                false,
7272                false,
7273                None,
7274                window,
7275                cx,
7276            );
7277        });
7278        assert_item_labels(&pane, ["buffer 1*"], cx);
7279
7280        // new singleton view with different project entry
7281        pane.update_in(cx, |pane, window, cx| {
7282            pane.add_item(
7283                Box::new(cx.new(|cx| {
7284                    TestItem::new(cx)
7285                        .with_buffer_kind(ItemBufferKind::Singleton)
7286                        .with_label("buffer 2")
7287                        .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
7288                })),
7289                false,
7290                false,
7291                None,
7292                window,
7293                cx,
7294            );
7295        });
7296        assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
7297
7298        // new multibuffer view with the same project entry
7299        pane.update_in(cx, |pane, window, cx| {
7300            pane.add_item(
7301                Box::new(cx.new(|cx| {
7302                    TestItem::new(cx)
7303                        .with_buffer_kind(ItemBufferKind::Multibuffer)
7304                        .with_label("multibuffer 1")
7305                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7306                })),
7307                false,
7308                false,
7309                None,
7310                window,
7311                cx,
7312            );
7313        });
7314        assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
7315
7316        // another multibuffer view with the same project entry
7317        pane.update_in(cx, |pane, window, cx| {
7318            pane.add_item(
7319                Box::new(cx.new(|cx| {
7320                    TestItem::new(cx)
7321                        .with_buffer_kind(ItemBufferKind::Multibuffer)
7322                        .with_label("multibuffer 1b")
7323                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7324                })),
7325                false,
7326                false,
7327                None,
7328                window,
7329                cx,
7330            );
7331        });
7332        assert_item_labels(
7333            &pane,
7334            ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
7335            cx,
7336        );
7337    }
7338
7339    #[gpui::test]
7340    async fn test_remove_item_ordering_history(cx: &mut TestAppContext) {
7341        init_test(cx);
7342        let fs = FakeFs::new(cx.executor());
7343
7344        let project = Project::test(fs, None, cx).await;
7345        let (workspace, cx) =
7346            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7347        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
7348
7349        add_labeled_item(&pane, "A", false, cx);
7350        add_labeled_item(&pane, "B", false, cx);
7351        add_labeled_item(&pane, "C", false, cx);
7352        add_labeled_item(&pane, "D", false, cx);
7353        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
7354
7355        pane.update_in(cx, |pane, window, cx| {
7356            pane.activate_item(1, false, false, window, cx)
7357        });
7358        add_labeled_item(&pane, "1", false, cx);
7359        assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
7360
7361        pane.update_in(cx, |pane, window, cx| {
7362            pane.close_active_item(
7363                &CloseActiveItem {
7364                    save_intent: None,
7365                    close_pinned: false,
7366                },
7367                window,
7368                cx,
7369            )
7370        })
7371        .await
7372        .unwrap();
7373        assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
7374
7375        pane.update_in(cx, |pane, window, cx| {
7376            pane.activate_item(3, false, false, window, cx)
7377        });
7378        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
7379
7380        pane.update_in(cx, |pane, window, cx| {
7381            pane.close_active_item(
7382                &CloseActiveItem {
7383                    save_intent: None,
7384                    close_pinned: false,
7385                },
7386                window,
7387                cx,
7388            )
7389        })
7390        .await
7391        .unwrap();
7392        assert_item_labels(&pane, ["A", "B*", "C"], cx);
7393
7394        pane.update_in(cx, |pane, window, cx| {
7395            pane.close_active_item(
7396                &CloseActiveItem {
7397                    save_intent: None,
7398                    close_pinned: false,
7399                },
7400                window,
7401                cx,
7402            )
7403        })
7404        .await
7405        .unwrap();
7406        assert_item_labels(&pane, ["A", "C*"], cx);
7407
7408        pane.update_in(cx, |pane, window, cx| {
7409            pane.close_active_item(
7410                &CloseActiveItem {
7411                    save_intent: None,
7412                    close_pinned: false,
7413                },
7414                window,
7415                cx,
7416            )
7417        })
7418        .await
7419        .unwrap();
7420        assert_item_labels(&pane, ["A*"], cx);
7421    }
7422
7423    #[gpui::test]
7424    async fn test_remove_item_ordering_neighbour(cx: &mut TestAppContext) {
7425        init_test(cx);
7426        cx.update_global::<SettingsStore, ()>(|s, cx| {
7427            s.update_user_settings(cx, |s| {
7428                s.tabs.get_or_insert_default().activate_on_close = Some(ActivateOnClose::Neighbour);
7429            });
7430        });
7431        let fs = FakeFs::new(cx.executor());
7432
7433        let project = Project::test(fs, None, cx).await;
7434        let (workspace, cx) =
7435            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7436        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
7437
7438        add_labeled_item(&pane, "A", false, cx);
7439        add_labeled_item(&pane, "B", false, cx);
7440        add_labeled_item(&pane, "C", false, cx);
7441        add_labeled_item(&pane, "D", false, cx);
7442        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
7443
7444        pane.update_in(cx, |pane, window, cx| {
7445            pane.activate_item(1, false, false, window, cx)
7446        });
7447        add_labeled_item(&pane, "1", false, cx);
7448        assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
7449
7450        pane.update_in(cx, |pane, window, cx| {
7451            pane.close_active_item(
7452                &CloseActiveItem {
7453                    save_intent: None,
7454                    close_pinned: false,
7455                },
7456                window,
7457                cx,
7458            )
7459        })
7460        .await
7461        .unwrap();
7462        assert_item_labels(&pane, ["A", "B", "C*", "D"], cx);
7463
7464        pane.update_in(cx, |pane, window, cx| {
7465            pane.activate_item(3, false, false, window, cx)
7466        });
7467        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
7468
7469        pane.update_in(cx, |pane, window, cx| {
7470            pane.close_active_item(
7471                &CloseActiveItem {
7472                    save_intent: None,
7473                    close_pinned: false,
7474                },
7475                window,
7476                cx,
7477            )
7478        })
7479        .await
7480        .unwrap();
7481        assert_item_labels(&pane, ["A", "B", "C*"], cx);
7482
7483        pane.update_in(cx, |pane, window, cx| {
7484            pane.close_active_item(
7485                &CloseActiveItem {
7486                    save_intent: None,
7487                    close_pinned: false,
7488                },
7489                window,
7490                cx,
7491            )
7492        })
7493        .await
7494        .unwrap();
7495        assert_item_labels(&pane, ["A", "B*"], cx);
7496
7497        pane.update_in(cx, |pane, window, cx| {
7498            pane.close_active_item(
7499                &CloseActiveItem {
7500                    save_intent: None,
7501                    close_pinned: false,
7502                },
7503                window,
7504                cx,
7505            )
7506        })
7507        .await
7508        .unwrap();
7509        assert_item_labels(&pane, ["A*"], cx);
7510    }
7511
7512    #[gpui::test]
7513    async fn test_remove_item_ordering_left_neighbour(cx: &mut TestAppContext) {
7514        init_test(cx);
7515        cx.update_global::<SettingsStore, ()>(|s, cx| {
7516            s.update_user_settings(cx, |s| {
7517                s.tabs.get_or_insert_default().activate_on_close =
7518                    Some(ActivateOnClose::LeftNeighbour);
7519            });
7520        });
7521        let fs = FakeFs::new(cx.executor());
7522
7523        let project = Project::test(fs, None, cx).await;
7524        let (workspace, cx) =
7525            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7526        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
7527
7528        add_labeled_item(&pane, "A", false, cx);
7529        add_labeled_item(&pane, "B", false, cx);
7530        add_labeled_item(&pane, "C", false, cx);
7531        add_labeled_item(&pane, "D", false, cx);
7532        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
7533
7534        pane.update_in(cx, |pane, window, cx| {
7535            pane.activate_item(1, false, false, window, cx)
7536        });
7537        add_labeled_item(&pane, "1", false, cx);
7538        assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
7539
7540        pane.update_in(cx, |pane, window, cx| {
7541            pane.close_active_item(
7542                &CloseActiveItem {
7543                    save_intent: None,
7544                    close_pinned: false,
7545                },
7546                window,
7547                cx,
7548            )
7549        })
7550        .await
7551        .unwrap();
7552        assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
7553
7554        pane.update_in(cx, |pane, window, cx| {
7555            pane.activate_item(3, false, false, window, cx)
7556        });
7557        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
7558
7559        pane.update_in(cx, |pane, window, cx| {
7560            pane.close_active_item(
7561                &CloseActiveItem {
7562                    save_intent: None,
7563                    close_pinned: false,
7564                },
7565                window,
7566                cx,
7567            )
7568        })
7569        .await
7570        .unwrap();
7571        assert_item_labels(&pane, ["A", "B", "C*"], cx);
7572
7573        pane.update_in(cx, |pane, window, cx| {
7574            pane.activate_item(0, false, false, window, cx)
7575        });
7576        assert_item_labels(&pane, ["A*", "B", "C"], cx);
7577
7578        pane.update_in(cx, |pane, window, cx| {
7579            pane.close_active_item(
7580                &CloseActiveItem {
7581                    save_intent: None,
7582                    close_pinned: false,
7583                },
7584                window,
7585                cx,
7586            )
7587        })
7588        .await
7589        .unwrap();
7590        assert_item_labels(&pane, ["B*", "C"], cx);
7591
7592        pane.update_in(cx, |pane, window, cx| {
7593            pane.close_active_item(
7594                &CloseActiveItem {
7595                    save_intent: None,
7596                    close_pinned: false,
7597                },
7598                window,
7599                cx,
7600            )
7601        })
7602        .await
7603        .unwrap();
7604        assert_item_labels(&pane, ["C*"], cx);
7605    }
7606
7607    #[gpui::test]
7608    async fn test_close_inactive_items(cx: &mut TestAppContext) {
7609        init_test(cx);
7610        let fs = FakeFs::new(cx.executor());
7611
7612        let project = Project::test(fs, None, cx).await;
7613        let (workspace, cx) =
7614            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7615        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
7616
7617        let item_a = add_labeled_item(&pane, "A", false, cx);
7618        pane.update_in(cx, |pane, window, cx| {
7619            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
7620            pane.pin_tab_at(ix, window, cx);
7621        });
7622        assert_item_labels(&pane, ["A*!"], cx);
7623
7624        let item_b = add_labeled_item(&pane, "B", false, cx);
7625        pane.update_in(cx, |pane, window, cx| {
7626            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
7627            pane.pin_tab_at(ix, window, cx);
7628        });
7629        assert_item_labels(&pane, ["A!", "B*!"], cx);
7630
7631        add_labeled_item(&pane, "C", false, cx);
7632        assert_item_labels(&pane, ["A!", "B!", "C*"], cx);
7633
7634        add_labeled_item(&pane, "D", false, cx);
7635        add_labeled_item(&pane, "E", false, cx);
7636        assert_item_labels(&pane, ["A!", "B!", "C", "D", "E*"], cx);
7637
7638        pane.update_in(cx, |pane, window, cx| {
7639            pane.close_other_items(
7640                &CloseOtherItems {
7641                    save_intent: None,
7642                    close_pinned: false,
7643                },
7644                None,
7645                window,
7646                cx,
7647            )
7648        })
7649        .await
7650        .unwrap();
7651        assert_item_labels(&pane, ["A!", "B!", "E*"], cx);
7652    }
7653
7654    #[gpui::test]
7655    async fn test_running_close_inactive_items_via_an_inactive_item(cx: &mut TestAppContext) {
7656        init_test(cx);
7657        let fs = FakeFs::new(cx.executor());
7658
7659        let project = Project::test(fs, None, cx).await;
7660        let (workspace, cx) =
7661            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7662        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
7663
7664        add_labeled_item(&pane, "A", false, cx);
7665        assert_item_labels(&pane, ["A*"], cx);
7666
7667        let item_b = add_labeled_item(&pane, "B", false, cx);
7668        assert_item_labels(&pane, ["A", "B*"], cx);
7669
7670        add_labeled_item(&pane, "C", false, cx);
7671        add_labeled_item(&pane, "D", false, cx);
7672        add_labeled_item(&pane, "E", false, cx);
7673        assert_item_labels(&pane, ["A", "B", "C", "D", "E*"], cx);
7674
7675        pane.update_in(cx, |pane, window, cx| {
7676            pane.close_other_items(
7677                &CloseOtherItems {
7678                    save_intent: None,
7679                    close_pinned: false,
7680                },
7681                Some(item_b.item_id()),
7682                window,
7683                cx,
7684            )
7685        })
7686        .await
7687        .unwrap();
7688        assert_item_labels(&pane, ["B*"], cx);
7689    }
7690
7691    #[gpui::test]
7692    async fn test_close_other_items_unpreviews_active_item(cx: &mut TestAppContext) {
7693        init_test(cx);
7694        let fs = FakeFs::new(cx.executor());
7695
7696        let project = Project::test(fs, None, cx).await;
7697        let (workspace, cx) =
7698            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7699        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
7700
7701        add_labeled_item(&pane, "A", false, cx);
7702        add_labeled_item(&pane, "B", false, cx);
7703        let item_c = add_labeled_item(&pane, "C", false, cx);
7704        assert_item_labels(&pane, ["A", "B", "C*"], cx);
7705
7706        pane.update(cx, |pane, cx| {
7707            pane.set_preview_item_id(Some(item_c.item_id()), cx);
7708        });
7709        assert!(pane.read_with(cx, |pane, _| pane.preview_item_id()
7710            == Some(item_c.item_id())));
7711
7712        pane.update_in(cx, |pane, window, cx| {
7713            pane.close_other_items(
7714                &CloseOtherItems {
7715                    save_intent: None,
7716                    close_pinned: false,
7717                },
7718                Some(item_c.item_id()),
7719                window,
7720                cx,
7721            )
7722        })
7723        .await
7724        .unwrap();
7725
7726        assert!(pane.read_with(cx, |pane, _| pane.preview_item_id().is_none()));
7727        assert_item_labels(&pane, ["C*"], cx);
7728    }
7729
7730    #[gpui::test]
7731    async fn test_close_clean_items(cx: &mut TestAppContext) {
7732        init_test(cx);
7733        let fs = FakeFs::new(cx.executor());
7734
7735        let project = Project::test(fs, None, cx).await;
7736        let (workspace, cx) =
7737            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7738        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
7739
7740        add_labeled_item(&pane, "A", true, cx);
7741        add_labeled_item(&pane, "B", false, cx);
7742        add_labeled_item(&pane, "C", true, cx);
7743        add_labeled_item(&pane, "D", false, cx);
7744        add_labeled_item(&pane, "E", false, cx);
7745        assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
7746
7747        pane.update_in(cx, |pane, window, cx| {
7748            pane.close_clean_items(
7749                &CloseCleanItems {
7750                    close_pinned: false,
7751                },
7752                window,
7753                cx,
7754            )
7755        })
7756        .await
7757        .unwrap();
7758        assert_item_labels(&pane, ["A^", "C*^"], cx);
7759    }
7760
7761    #[gpui::test]
7762    async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
7763        init_test(cx);
7764        let fs = FakeFs::new(cx.executor());
7765
7766        let project = Project::test(fs, None, cx).await;
7767        let (workspace, cx) =
7768            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7769        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
7770
7771        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
7772
7773        pane.update_in(cx, |pane, window, cx| {
7774            pane.close_items_to_the_left_by_id(
7775                None,
7776                &CloseItemsToTheLeft {
7777                    close_pinned: false,
7778                },
7779                window,
7780                cx,
7781            )
7782        })
7783        .await
7784        .unwrap();
7785        assert_item_labels(&pane, ["C*", "D", "E"], cx);
7786    }
7787
7788    #[gpui::test]
7789    async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
7790        init_test(cx);
7791        let fs = FakeFs::new(cx.executor());
7792
7793        let project = Project::test(fs, None, cx).await;
7794        let (workspace, cx) =
7795            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7796        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
7797
7798        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
7799
7800        pane.update_in(cx, |pane, window, cx| {
7801            pane.close_items_to_the_right_by_id(
7802                None,
7803                &CloseItemsToTheRight {
7804                    close_pinned: false,
7805                },
7806                window,
7807                cx,
7808            )
7809        })
7810        .await
7811        .unwrap();
7812        assert_item_labels(&pane, ["A", "B", "C*"], cx);
7813    }
7814
7815    #[gpui::test]
7816    async fn test_close_all_items(cx: &mut TestAppContext) {
7817        init_test(cx);
7818        let fs = FakeFs::new(cx.executor());
7819
7820        let project = Project::test(fs, None, cx).await;
7821        let (workspace, cx) =
7822            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7823        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
7824
7825        let item_a = add_labeled_item(&pane, "A", false, cx);
7826        add_labeled_item(&pane, "B", false, cx);
7827        add_labeled_item(&pane, "C", false, cx);
7828        assert_item_labels(&pane, ["A", "B", "C*"], cx);
7829
7830        pane.update_in(cx, |pane, window, cx| {
7831            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
7832            pane.pin_tab_at(ix, window, cx);
7833            pane.close_all_items(
7834                &CloseAllItems {
7835                    save_intent: None,
7836                    close_pinned: false,
7837                },
7838                window,
7839                cx,
7840            )
7841        })
7842        .await
7843        .unwrap();
7844        assert_item_labels(&pane, ["A*!"], cx);
7845
7846        pane.update_in(cx, |pane, window, cx| {
7847            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
7848            pane.unpin_tab_at(ix, window, cx);
7849            pane.close_all_items(
7850                &CloseAllItems {
7851                    save_intent: None,
7852                    close_pinned: false,
7853                },
7854                window,
7855                cx,
7856            )
7857        })
7858        .await
7859        .unwrap();
7860
7861        assert_item_labels(&pane, [], cx);
7862
7863        add_labeled_item(&pane, "A", true, cx).update(cx, |item, cx| {
7864            item.project_items
7865                .push(TestProjectItem::new_dirty(1, "A.txt", cx))
7866        });
7867        add_labeled_item(&pane, "B", true, cx).update(cx, |item, cx| {
7868            item.project_items
7869                .push(TestProjectItem::new_dirty(2, "B.txt", cx))
7870        });
7871        add_labeled_item(&pane, "C", true, cx).update(cx, |item, cx| {
7872            item.project_items
7873                .push(TestProjectItem::new_dirty(3, "C.txt", cx))
7874        });
7875        assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
7876
7877        let save = pane.update_in(cx, |pane, window, cx| {
7878            pane.close_all_items(
7879                &CloseAllItems {
7880                    save_intent: None,
7881                    close_pinned: false,
7882                },
7883                window,
7884                cx,
7885            )
7886        });
7887
7888        cx.executor().run_until_parked();
7889        cx.simulate_prompt_answer("Save all");
7890        save.await.unwrap();
7891        assert_item_labels(&pane, [], cx);
7892
7893        add_labeled_item(&pane, "A", true, cx);
7894        add_labeled_item(&pane, "B", true, cx);
7895        add_labeled_item(&pane, "C", true, cx);
7896        assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
7897        let save = pane.update_in(cx, |pane, window, cx| {
7898            pane.close_all_items(
7899                &CloseAllItems {
7900                    save_intent: None,
7901                    close_pinned: false,
7902                },
7903                window,
7904                cx,
7905            )
7906        });
7907
7908        cx.executor().run_until_parked();
7909        cx.simulate_prompt_answer("Discard all");
7910        save.await.unwrap();
7911        assert_item_labels(&pane, [], cx);
7912
7913        add_labeled_item(&pane, "A", true, cx).update(cx, |item, cx| {
7914            item.project_items
7915                .push(TestProjectItem::new_dirty(1, "A.txt", cx))
7916        });
7917        add_labeled_item(&pane, "B", true, cx).update(cx, |item, cx| {
7918            item.project_items
7919                .push(TestProjectItem::new_dirty(2, "B.txt", cx))
7920        });
7921        add_labeled_item(&pane, "C", true, cx).update(cx, |item, cx| {
7922            item.project_items
7923                .push(TestProjectItem::new_dirty(3, "C.txt", cx))
7924        });
7925        assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
7926
7927        let close_task = pane.update_in(cx, |pane, window, cx| {
7928            pane.close_all_items(
7929                &CloseAllItems {
7930                    save_intent: None,
7931                    close_pinned: false,
7932                },
7933                window,
7934                cx,
7935            )
7936        });
7937
7938        cx.executor().run_until_parked();
7939        cx.simulate_prompt_answer("Discard all");
7940        close_task.await.unwrap();
7941        assert_item_labels(&pane, [], cx);
7942
7943        add_labeled_item(&pane, "Clean1", false, cx);
7944        add_labeled_item(&pane, "Dirty", true, cx).update(cx, |item, cx| {
7945            item.project_items
7946                .push(TestProjectItem::new_dirty(1, "Dirty.txt", cx))
7947        });
7948        add_labeled_item(&pane, "Clean2", false, cx);
7949        assert_item_labels(&pane, ["Clean1", "Dirty^", "Clean2*"], cx);
7950
7951        let close_task = pane.update_in(cx, |pane, window, cx| {
7952            pane.close_all_items(
7953                &CloseAllItems {
7954                    save_intent: None,
7955                    close_pinned: false,
7956                },
7957                window,
7958                cx,
7959            )
7960        });
7961
7962        cx.executor().run_until_parked();
7963        cx.simulate_prompt_answer("Cancel");
7964        close_task.await.unwrap();
7965        assert_item_labels(&pane, ["Dirty*^"], cx);
7966    }
7967
7968    #[gpui::test]
7969    async fn test_discard_all_reloads_from_disk(cx: &mut TestAppContext) {
7970        init_test(cx);
7971        let fs = FakeFs::new(cx.executor());
7972
7973        let project = Project::test(fs, None, cx).await;
7974        let (workspace, cx) =
7975            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7976        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
7977
7978        let item_a = add_labeled_item(&pane, "A", true, cx);
7979        item_a.update(cx, |item, cx| {
7980            item.project_items
7981                .push(TestProjectItem::new_dirty(1, "A.txt", cx))
7982        });
7983        let item_b = add_labeled_item(&pane, "B", true, cx);
7984        item_b.update(cx, |item, cx| {
7985            item.project_items
7986                .push(TestProjectItem::new_dirty(2, "B.txt", cx))
7987        });
7988        assert_item_labels(&pane, ["A^", "B*^"], cx);
7989
7990        let close_task = pane.update_in(cx, |pane, window, cx| {
7991            pane.close_all_items(
7992                &CloseAllItems {
7993                    save_intent: None,
7994                    close_pinned: false,
7995                },
7996                window,
7997                cx,
7998            )
7999        });
8000
8001        cx.executor().run_until_parked();
8002        cx.simulate_prompt_answer("Discard all");
8003        close_task.await.unwrap();
8004        assert_item_labels(&pane, [], cx);
8005
8006        item_a.read_with(cx, |item, _| {
8007            assert_eq!(item.reload_count, 1, "item A should have been reloaded");
8008            assert!(
8009                !item.is_dirty,
8010                "item A should no longer be dirty after reload"
8011            );
8012        });
8013        item_b.read_with(cx, |item, _| {
8014            assert_eq!(item.reload_count, 1, "item B should have been reloaded");
8015            assert!(
8016                !item.is_dirty,
8017                "item B should no longer be dirty after reload"
8018            );
8019        });
8020    }
8021
8022    #[gpui::test]
8023    async fn test_dont_save_single_file_reloads_from_disk(cx: &mut TestAppContext) {
8024        init_test(cx);
8025        let fs = FakeFs::new(cx.executor());
8026
8027        let project = Project::test(fs, None, cx).await;
8028        let (workspace, cx) =
8029            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
8030        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
8031
8032        let item = add_labeled_item(&pane, "Dirty", true, cx);
8033        item.update(cx, |item, cx| {
8034            item.project_items
8035                .push(TestProjectItem::new_dirty(1, "Dirty.txt", cx))
8036        });
8037        assert_item_labels(&pane, ["Dirty*^"], cx);
8038
8039        let close_task = pane.update_in(cx, |pane, window, cx| {
8040            pane.close_item_by_id(item.item_id(), SaveIntent::Close, window, cx)
8041        });
8042
8043        cx.executor().run_until_parked();
8044        cx.simulate_prompt_answer("Don't Save");
8045        close_task.await.unwrap();
8046        assert_item_labels(&pane, [], cx);
8047
8048        item.read_with(cx, |item, _| {
8049            assert_eq!(item.reload_count, 1, "item should have been reloaded");
8050            assert!(
8051                !item.is_dirty,
8052                "item should no longer be dirty after reload"
8053            );
8054        });
8055    }
8056
8057    #[gpui::test]
8058    async fn test_format_runs_on_first_save_of_new_file(cx: &mut TestAppContext) {
8059        init_test(cx);
8060        let fs = FakeFs::new(cx.executor());
8061
8062        let project = Project::test(fs, None, cx).await;
8063        let (workspace, cx) =
8064            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
8065        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
8066
8067        let item = add_labeled_item(&pane, "untitled", true, cx);
8068        item.update(cx, |item, cx| {
8069            item.project_items.push(TestProjectItem::new_untitled(cx));
8070        });
8071        assert_item_labels(&pane, ["untitled*^"], cx);
8072
8073        let close_task = pane.update_in(cx, |pane, window, cx| {
8074            pane.close_item_by_id(item.item_id(), SaveIntent::Save, window, cx)
8075        });
8076
8077        cx.executor().run_until_parked();
8078        cx.simulate_new_path_selection(|_| Some(Default::default()));
8079        close_task.await.unwrap();
8080
8081        item.read_with(cx, |item, _| {
8082            assert_eq!(item.save_as_count, 1);
8083            assert_eq!(
8084                item.save_count, 1,
8085                "formatter should run after the file is given a path on first save"
8086            );
8087        });
8088    }
8089
8090    #[gpui::test]
8091    async fn test_format_does_not_run_on_first_save_when_save_without_format(
8092        cx: &mut TestAppContext,
8093    ) {
8094        init_test(cx);
8095        let fs = FakeFs::new(cx.executor());
8096
8097        let project = Project::test(fs, None, cx).await;
8098        let (workspace, cx) =
8099            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
8100        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
8101
8102        let item = add_labeled_item(&pane, "untitled", true, cx);
8103        item.update(cx, |item, cx| {
8104            item.project_items.push(TestProjectItem::new_untitled(cx));
8105        });
8106        assert_item_labels(&pane, ["untitled*^"], cx);
8107
8108        let close_task = pane.update_in(cx, |pane, window, cx| {
8109            pane.close_item_by_id(item.item_id(), SaveIntent::SaveWithoutFormat, window, cx)
8110        });
8111
8112        cx.executor().run_until_parked();
8113        cx.simulate_new_path_selection(|_| Some(Default::default()));
8114        close_task.await.unwrap();
8115
8116        item.read_with(cx, |item, _| {
8117            assert_eq!(item.save_as_count, 1);
8118            assert_eq!(
8119                item.save_count, 0,
8120                "formatter should not run when SaveWithoutFormat is used"
8121            );
8122        });
8123    }
8124
8125    #[gpui::test]
8126    async fn test_discard_does_not_reload_multibuffer(cx: &mut TestAppContext) {
8127        init_test(cx);
8128        let fs = FakeFs::new(cx.executor());
8129
8130        let project = Project::test(fs, None, cx).await;
8131        let (workspace, cx) =
8132            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
8133        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
8134
8135        let singleton_item = pane.update_in(cx, |pane, window, cx| {
8136            let item = Box::new(cx.new(|cx| {
8137                TestItem::new(cx)
8138                    .with_label("Singleton")
8139                    .with_dirty(true)
8140                    .with_buffer_kind(ItemBufferKind::Singleton)
8141            }));
8142            pane.add_item(item.clone(), false, false, None, window, cx);
8143            item
8144        });
8145        singleton_item.update(cx, |item, cx| {
8146            item.project_items
8147                .push(TestProjectItem::new_dirty(1, "Singleton.txt", cx))
8148        });
8149
8150        let multi_item = pane.update_in(cx, |pane, window, cx| {
8151            let item = Box::new(cx.new(|cx| {
8152                TestItem::new(cx)
8153                    .with_label("Multi")
8154                    .with_dirty(true)
8155                    .with_buffer_kind(ItemBufferKind::Multibuffer)
8156            }));
8157            pane.add_item(item.clone(), false, false, None, window, cx);
8158            item
8159        });
8160        multi_item.update(cx, |item, cx| {
8161            item.project_items
8162                .push(TestProjectItem::new_dirty(2, "Multi.txt", cx))
8163        });
8164
8165        let close_task = pane.update_in(cx, |pane, window, cx| {
8166            pane.close_all_items(
8167                &CloseAllItems {
8168                    save_intent: None,
8169                    close_pinned: false,
8170                },
8171                window,
8172                cx,
8173            )
8174        });
8175
8176        cx.executor().run_until_parked();
8177        cx.simulate_prompt_answer("Discard all");
8178        close_task.await.unwrap();
8179        assert_item_labels(&pane, [], cx);
8180
8181        singleton_item.read_with(cx, |item, _| {
8182            assert_eq!(item.reload_count, 1, "singleton should have been reloaded");
8183            assert!(
8184                !item.is_dirty,
8185                "singleton should no longer be dirty after reload"
8186            );
8187        });
8188        multi_item.read_with(cx, |item, _| {
8189            assert_eq!(
8190                item.reload_count, 0,
8191                "multibuffer should not have been reloaded"
8192            );
8193        });
8194    }
8195
8196    #[gpui::test]
8197    async fn test_close_multibuffer_items(cx: &mut TestAppContext) {
8198        init_test(cx);
8199        let fs = FakeFs::new(cx.executor());
8200
8201        let project = Project::test(fs, None, cx).await;
8202        let (workspace, cx) =
8203            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
8204        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
8205
8206        let add_labeled_item = |pane: &Entity<Pane>,
8207                                label,
8208                                is_dirty,
8209                                kind: ItemBufferKind,
8210                                cx: &mut VisualTestContext| {
8211            pane.update_in(cx, |pane, window, cx| {
8212                let labeled_item = Box::new(cx.new(|cx| {
8213                    TestItem::new(cx)
8214                        .with_label(label)
8215                        .with_dirty(is_dirty)
8216                        .with_buffer_kind(kind)
8217                }));
8218                pane.add_item(labeled_item.clone(), false, false, None, window, cx);
8219                labeled_item
8220            })
8221        };
8222
8223        let item_a = add_labeled_item(&pane, "A", false, ItemBufferKind::Multibuffer, cx);
8224        add_labeled_item(&pane, "B", false, ItemBufferKind::Multibuffer, cx);
8225        add_labeled_item(&pane, "C", false, ItemBufferKind::Singleton, cx);
8226        assert_item_labels(&pane, ["A", "B", "C*"], cx);
8227
8228        pane.update_in(cx, |pane, window, cx| {
8229            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
8230            pane.pin_tab_at(ix, window, cx);
8231            pane.close_multibuffer_items(
8232                &CloseMultibufferItems {
8233                    save_intent: None,
8234                    close_pinned: false,
8235                },
8236                window,
8237                cx,
8238            )
8239        })
8240        .await
8241        .unwrap();
8242        assert_item_labels(&pane, ["A!", "C*"], cx);
8243
8244        pane.update_in(cx, |pane, window, cx| {
8245            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
8246            pane.unpin_tab_at(ix, window, cx);
8247            pane.close_multibuffer_items(
8248                &CloseMultibufferItems {
8249                    save_intent: None,
8250                    close_pinned: false,
8251                },
8252                window,
8253                cx,
8254            )
8255        })
8256        .await
8257        .unwrap();
8258
8259        assert_item_labels(&pane, ["C*"], cx);
8260
8261        add_labeled_item(&pane, "A", true, ItemBufferKind::Singleton, cx).update(cx, |item, cx| {
8262            item.project_items
8263                .push(TestProjectItem::new_dirty(1, "A.txt", cx))
8264        });
8265        add_labeled_item(&pane, "B", true, ItemBufferKind::Multibuffer, cx).update(
8266            cx,
8267            |item, cx| {
8268                item.project_items
8269                    .push(TestProjectItem::new_dirty(2, "B.txt", cx))
8270            },
8271        );
8272        add_labeled_item(&pane, "D", true, ItemBufferKind::Multibuffer, cx).update(
8273            cx,
8274            |item, cx| {
8275                item.project_items
8276                    .push(TestProjectItem::new_dirty(3, "D.txt", cx))
8277            },
8278        );
8279        assert_item_labels(&pane, ["C", "A^", "B^", "D*^"], cx);
8280
8281        let save = pane.update_in(cx, |pane, window, cx| {
8282            pane.close_multibuffer_items(
8283                &CloseMultibufferItems {
8284                    save_intent: None,
8285                    close_pinned: false,
8286                },
8287                window,
8288                cx,
8289            )
8290        });
8291
8292        cx.executor().run_until_parked();
8293        cx.simulate_prompt_answer("Save all");
8294        save.await.unwrap();
8295        assert_item_labels(&pane, ["C", "A*^"], cx);
8296
8297        add_labeled_item(&pane, "B", true, ItemBufferKind::Multibuffer, cx).update(
8298            cx,
8299            |item, cx| {
8300                item.project_items
8301                    .push(TestProjectItem::new_dirty(2, "B.txt", cx))
8302            },
8303        );
8304        add_labeled_item(&pane, "D", true, ItemBufferKind::Multibuffer, cx).update(
8305            cx,
8306            |item, cx| {
8307                item.project_items
8308                    .push(TestProjectItem::new_dirty(3, "D.txt", cx))
8309            },
8310        );
8311        assert_item_labels(&pane, ["C", "A^", "B^", "D*^"], cx);
8312        let save = pane.update_in(cx, |pane, window, cx| {
8313            pane.close_multibuffer_items(
8314                &CloseMultibufferItems {
8315                    save_intent: None,
8316                    close_pinned: false,
8317                },
8318                window,
8319                cx,
8320            )
8321        });
8322
8323        cx.executor().run_until_parked();
8324        cx.simulate_prompt_answer("Discard all");
8325        save.await.unwrap();
8326        assert_item_labels(&pane, ["C", "A*^"], cx);
8327    }
8328
8329    #[gpui::test]
8330    async fn test_close_with_save_intent(cx: &mut TestAppContext) {
8331        init_test(cx);
8332        let fs = FakeFs::new(cx.executor());
8333
8334        let project = Project::test(fs, None, cx).await;
8335        let (workspace, cx) =
8336            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8337        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
8338
8339        let a = cx.update(|_, cx| TestProjectItem::new_dirty(1, "A.txt", cx));
8340        let b = cx.update(|_, cx| TestProjectItem::new_dirty(1, "B.txt", cx));
8341        let c = cx.update(|_, cx| TestProjectItem::new_dirty(1, "C.txt", cx));
8342
8343        add_labeled_item(&pane, "AB", true, cx).update(cx, |item, _| {
8344            item.project_items.push(a.clone());
8345            item.project_items.push(b.clone());
8346        });
8347        add_labeled_item(&pane, "C", true, cx)
8348            .update(cx, |item, _| item.project_items.push(c.clone()));
8349        assert_item_labels(&pane, ["AB^", "C*^"], cx);
8350
8351        pane.update_in(cx, |pane, window, cx| {
8352            pane.close_all_items(
8353                &CloseAllItems {
8354                    save_intent: Some(SaveIntent::Save),
8355                    close_pinned: false,
8356                },
8357                window,
8358                cx,
8359            )
8360        })
8361        .await
8362        .unwrap();
8363
8364        assert_item_labels(&pane, [], cx);
8365        cx.update(|_, cx| {
8366            assert!(!a.read(cx).is_dirty);
8367            assert!(!b.read(cx).is_dirty);
8368            assert!(!c.read(cx).is_dirty);
8369        });
8370    }
8371
8372    #[gpui::test]
8373    async fn test_new_tab_scrolls_into_view_completely(cx: &mut TestAppContext) {
8374        // Arrange
8375        init_test(cx);
8376        let fs = FakeFs::new(cx.executor());
8377
8378        let project = Project::test(fs, None, cx).await;
8379        let (workspace, cx) =
8380            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8381        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
8382
8383        cx.simulate_resize(size(px(300.), px(300.)));
8384
8385        add_labeled_item(&pane, "untitled", false, cx);
8386        add_labeled_item(&pane, "untitled", false, cx);
8387        add_labeled_item(&pane, "untitled", false, cx);
8388        add_labeled_item(&pane, "untitled", false, cx);
8389        // Act: this should trigger a scroll
8390        add_labeled_item(&pane, "untitled", false, cx);
8391        // Assert
8392        let tab_bar_scroll_handle =
8393            pane.update_in(cx, |pane, _window, _cx| pane.tab_bar_scroll_handle.clone());
8394        assert_eq!(tab_bar_scroll_handle.children_count(), 6);
8395        let tab_bounds = cx.debug_bounds("TAB-4").unwrap();
8396        let new_tab_button_bounds = cx.debug_bounds("ICON-Plus").unwrap();
8397        let scroll_bounds = tab_bar_scroll_handle.bounds();
8398        let scroll_offset = tab_bar_scroll_handle.offset();
8399        assert!(scroll_offset.x < px(0.));
8400        assert!(scroll_offset.x >= -tab_bar_scroll_handle.max_offset().x);
8401        assert!(tab_bounds.left() >= scroll_bounds.left());
8402        assert!(tab_bounds.right() <= scroll_bounds.right());
8403        assert!(
8404            !tab_bounds.intersects(&new_tab_button_bounds),
8405            "Tab should not overlap with the new tab button, if this is failing check if there's been a redesign!"
8406        );
8407    }
8408
8409    #[gpui::test]
8410    async fn test_pinned_tabs_scroll_to_item_uses_correct_index(cx: &mut TestAppContext) {
8411        init_test(cx);
8412        let fs = FakeFs::new(cx.executor());
8413
8414        let project = Project::test(fs, None, cx).await;
8415        let (workspace, cx) =
8416            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8417        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
8418
8419        cx.simulate_resize(size(px(400.), px(300.)));
8420
8421        for label in ["A", "B", "C"] {
8422            add_labeled_item(&pane, label, false, cx);
8423        }
8424
8425        pane.update_in(cx, |pane, window, cx| {
8426            pane.pin_tab_at(0, window, cx);
8427            pane.pin_tab_at(1, window, cx);
8428            pane.pin_tab_at(2, window, cx);
8429        });
8430
8431        for label in ["D", "E", "F", "G", "H", "I", "J", "K"] {
8432            add_labeled_item(&pane, label, false, cx);
8433        }
8434
8435        assert_item_labels(
8436            &pane,
8437            ["A!", "B!", "C!", "D", "E", "F", "G", "H", "I", "J", "K*"],
8438            cx,
8439        );
8440
8441        cx.run_until_parked();
8442
8443        // Verify overflow exists (precondition for scroll test)
8444        let scroll_handle =
8445            pane.update_in(cx, |pane, _window, _cx| pane.tab_bar_scroll_handle.clone());
8446        assert!(
8447            scroll_handle.max_offset().x > px(0.),
8448            "Test requires tab overflow to verify scrolling. Increase tab count or reduce window width."
8449        );
8450
8451        // Activate a different tab first, then activate K
8452        // This ensures we're not just re-activating an already-active tab
8453        pane.update_in(cx, |pane, window, cx| {
8454            pane.activate_item(3, true, true, window, cx);
8455        });
8456        cx.run_until_parked();
8457
8458        pane.update_in(cx, |pane, window, cx| {
8459            pane.activate_item(10, true, true, window, cx);
8460        });
8461        cx.run_until_parked();
8462
8463        let scroll_handle =
8464            pane.update_in(cx, |pane, _window, _cx| pane.tab_bar_scroll_handle.clone());
8465        let k_tab_bounds = cx.debug_bounds("TAB-10").unwrap();
8466        let scroll_bounds = scroll_handle.bounds();
8467
8468        assert!(
8469            k_tab_bounds.left() >= scroll_bounds.left(),
8470            "Active tab K should be scrolled into view"
8471        );
8472    }
8473
8474    #[gpui::test]
8475    async fn test_close_all_items_including_pinned(cx: &mut TestAppContext) {
8476        init_test(cx);
8477        let fs = FakeFs::new(cx.executor());
8478
8479        let project = Project::test(fs, None, cx).await;
8480        let (workspace, cx) =
8481            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8482        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
8483
8484        let item_a = add_labeled_item(&pane, "A", false, cx);
8485        add_labeled_item(&pane, "B", false, cx);
8486        add_labeled_item(&pane, "C", false, cx);
8487        assert_item_labels(&pane, ["A", "B", "C*"], cx);
8488
8489        pane.update_in(cx, |pane, window, cx| {
8490            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
8491            pane.pin_tab_at(ix, window, cx);
8492            pane.close_all_items(
8493                &CloseAllItems {
8494                    save_intent: None,
8495                    close_pinned: true,
8496                },
8497                window,
8498                cx,
8499            )
8500        })
8501        .await
8502        .unwrap();
8503        assert_item_labels(&pane, [], cx);
8504    }
8505
8506    #[gpui::test]
8507    async fn test_close_pinned_tab_with_non_pinned_in_same_pane(cx: &mut TestAppContext) {
8508        init_test(cx);
8509        let fs = FakeFs::new(cx.executor());
8510        let project = Project::test(fs, None, cx).await;
8511        let (workspace, cx) =
8512            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8513
8514        // Non-pinned tabs in same pane
8515        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
8516        add_labeled_item(&pane, "A", false, cx);
8517        add_labeled_item(&pane, "B", false, cx);
8518        add_labeled_item(&pane, "C", false, cx);
8519        pane.update_in(cx, |pane, window, cx| {
8520            pane.pin_tab_at(0, window, cx);
8521        });
8522        set_labeled_items(&pane, ["A*", "B", "C"], cx);
8523        pane.update_in(cx, |pane, window, cx| {
8524            pane.close_active_item(
8525                &CloseActiveItem {
8526                    save_intent: None,
8527                    close_pinned: false,
8528                },
8529                window,
8530                cx,
8531            )
8532            .unwrap();
8533        });
8534        // Non-pinned tab should be active
8535        assert_item_labels(&pane, ["A!", "B*", "C"], cx);
8536    }
8537
8538    #[gpui::test]
8539    async fn test_close_pinned_tab_with_non_pinned_in_different_pane(cx: &mut TestAppContext) {
8540        init_test(cx);
8541        let fs = FakeFs::new(cx.executor());
8542        let project = Project::test(fs, None, cx).await;
8543        let (workspace, cx) =
8544            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8545
8546        // No non-pinned tabs in same pane, non-pinned tabs in another pane
8547        let pane1 = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
8548        let pane2 = workspace.update_in(cx, |workspace, window, cx| {
8549            workspace.split_pane(pane1.clone(), SplitDirection::Right, window, cx)
8550        });
8551        add_labeled_item(&pane1, "A", false, cx);
8552        pane1.update_in(cx, |pane, window, cx| {
8553            pane.pin_tab_at(0, window, cx);
8554        });
8555        set_labeled_items(&pane1, ["A*"], cx);
8556        add_labeled_item(&pane2, "B", false, cx);
8557        set_labeled_items(&pane2, ["B"], cx);
8558        pane1.update_in(cx, |pane, window, cx| {
8559            pane.close_active_item(
8560                &CloseActiveItem {
8561                    save_intent: None,
8562                    close_pinned: false,
8563                },
8564                window,
8565                cx,
8566            )
8567            .unwrap();
8568        });
8569        //  Non-pinned tab of other pane should be active
8570        assert_item_labels(&pane2, ["B*"], cx);
8571    }
8572
8573    #[gpui::test]
8574    async fn test_close_pinned_tab_while_workspace_is_leased(cx: &mut TestAppContext) {
8575        init_test(cx);
8576        let fs = FakeFs::new(cx.executor());
8577        let project = Project::test(fs, None, cx).await;
8578        let (workspace, cx) =
8579            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8580
8581        // No non-pinned tabs in same pane, non-pinned tabs in another pane
8582        let pane1 = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
8583        let pane2 = workspace.update_in(cx, |workspace, window, cx| {
8584            workspace.split_pane(pane1.clone(), SplitDirection::Right, window, cx)
8585        });
8586        add_labeled_item(&pane1, "A", false, cx);
8587        pane1.update_in(cx, |pane, window, cx| {
8588            pane.pin_tab_at(0, window, cx);
8589        });
8590        set_labeled_items(&pane1, ["A*"], cx);
8591        add_labeled_item(&pane2, "B", false, cx);
8592        set_labeled_items(&pane2, ["B"], cx);
8593
8594        // Close the active item while the workspace entity is already being
8595        // updated. This used to double-lease the workspace and panic, because
8596        // `close_active_item` synchronously reached back into the workspace to
8597        // find an unpinned tab in another pane.
8598        workspace.update_in(cx, |_workspace, window, cx| {
8599            pane1.update(cx, |pane, cx| {
8600                pane.close_active_item(
8601                    &CloseActiveItem {
8602                        save_intent: None,
8603                        close_pinned: false,
8604                    },
8605                    window,
8606                    cx,
8607                )
8608                .detach();
8609            });
8610        });
8611        cx.run_until_parked();
8612
8613        // The pinned tab must not be closed, and the non-pinned tab of the
8614        // other pane should have been activated.
8615        assert_item_labels(&pane1, ["A*!"], cx);
8616        assert_item_labels(&pane2, ["B*"], cx);
8617    }
8618
8619    #[gpui::test]
8620    async fn ensure_item_closing_actions_do_not_panic_when_no_items_exist(cx: &mut TestAppContext) {
8621        init_test(cx);
8622        let fs = FakeFs::new(cx.executor());
8623        let project = Project::test(fs, None, cx).await;
8624        let (workspace, cx) =
8625            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8626
8627        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
8628        assert_item_labels(&pane, [], cx);
8629
8630        pane.update_in(cx, |pane, window, cx| {
8631            pane.close_active_item(
8632                &CloseActiveItem {
8633                    save_intent: None,
8634                    close_pinned: false,
8635                },
8636                window,
8637                cx,
8638            )
8639        })
8640        .await
8641        .unwrap();
8642
8643        pane.update_in(cx, |pane, window, cx| {
8644            pane.close_other_items(
8645                &CloseOtherItems {
8646                    save_intent: None,
8647                    close_pinned: false,
8648                },
8649                None,
8650                window,
8651                cx,
8652            )
8653        })
8654        .await
8655        .unwrap();
8656
8657        pane.update_in(cx, |pane, window, cx| {
8658            pane.close_all_items(
8659                &CloseAllItems {
8660                    save_intent: None,
8661                    close_pinned: false,
8662                },
8663                window,
8664                cx,
8665            )
8666        })
8667        .await
8668        .unwrap();
8669
8670        pane.update_in(cx, |pane, window, cx| {
8671            pane.close_clean_items(
8672                &CloseCleanItems {
8673                    close_pinned: false,
8674                },
8675                window,
8676                cx,
8677            )
8678        })
8679        .await
8680        .unwrap();
8681
8682        pane.update_in(cx, |pane, window, cx| {
8683            pane.close_items_to_the_right_by_id(
8684                None,
8685                &CloseItemsToTheRight {
8686                    close_pinned: false,
8687                },
8688                window,
8689                cx,
8690            )
8691        })
8692        .await
8693        .unwrap();
8694
8695        pane.update_in(cx, |pane, window, cx| {
8696            pane.close_items_to_the_left_by_id(
8697                None,
8698                &CloseItemsToTheLeft {
8699                    close_pinned: false,
8700                },
8701                window,
8702                cx,
8703            )
8704        })
8705        .await
8706        .unwrap();
8707    }
8708
8709    #[gpui::test]
8710    async fn test_item_swapping_actions(cx: &mut TestAppContext) {
8711        init_test(cx);
8712        let fs = FakeFs::new(cx.executor());
8713        let project = Project::test(fs, None, cx).await;
8714        let (workspace, cx) =
8715            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8716
8717        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
8718        assert_item_labels(&pane, [], cx);
8719
8720        // Test that these actions do not panic
8721        pane.update_in(cx, |pane, window, cx| {
8722            pane.swap_item_right(&Default::default(), window, cx);
8723        });
8724
8725        pane.update_in(cx, |pane, window, cx| {
8726            pane.swap_item_left(&Default::default(), window, cx);
8727        });
8728
8729        add_labeled_item(&pane, "A", false, cx);
8730        add_labeled_item(&pane, "B", false, cx);
8731        add_labeled_item(&pane, "C", false, cx);
8732        assert_item_labels(&pane, ["A", "B", "C*"], cx);
8733
8734        pane.update_in(cx, |pane, window, cx| {
8735            pane.swap_item_right(&Default::default(), window, cx);
8736        });
8737        assert_item_labels(&pane, ["A", "B", "C*"], cx);
8738
8739        pane.update_in(cx, |pane, window, cx| {
8740            pane.swap_item_left(&Default::default(), window, cx);
8741        });
8742        assert_item_labels(&pane, ["A", "C*", "B"], cx);
8743
8744        pane.update_in(cx, |pane, window, cx| {
8745            pane.swap_item_left(&Default::default(), window, cx);
8746        });
8747        assert_item_labels(&pane, ["C*", "A", "B"], cx);
8748
8749        pane.update_in(cx, |pane, window, cx| {
8750            pane.swap_item_left(&Default::default(), window, cx);
8751        });
8752        assert_item_labels(&pane, ["C*", "A", "B"], cx);
8753
8754        pane.update_in(cx, |pane, window, cx| {
8755            pane.swap_item_right(&Default::default(), window, cx);
8756        });
8757        assert_item_labels(&pane, ["A", "C*", "B"], cx);
8758    }
8759
8760    #[gpui::test]
8761    async fn test_split_empty(cx: &mut TestAppContext) {
8762        for split_direction in SplitDirection::all() {
8763            test_single_pane_split(["A"], split_direction, SplitMode::EmptyPane, cx).await;
8764        }
8765    }
8766
8767    #[gpui::test]
8768    async fn test_split_clone(cx: &mut TestAppContext) {
8769        for split_direction in SplitDirection::all() {
8770            test_single_pane_split(["A"], split_direction, SplitMode::ClonePane, cx).await;
8771        }
8772    }
8773
8774    #[gpui::test]
8775    async fn test_split_move_right_on_single_pane(cx: &mut TestAppContext) {
8776        test_single_pane_split(["A"], SplitDirection::Right, SplitMode::MovePane, cx).await;
8777    }
8778
8779    #[gpui::test]
8780    async fn test_split_move(cx: &mut TestAppContext) {
8781        for split_direction in SplitDirection::all() {
8782            test_single_pane_split(["A", "B"], split_direction, SplitMode::MovePane, cx).await;
8783        }
8784    }
8785
8786    #[gpui::test]
8787    async fn test_reopening_closed_item_after_unpreview(cx: &mut TestAppContext) {
8788        init_test(cx);
8789
8790        cx.update_global::<SettingsStore, ()>(|store, cx| {
8791            store.update_user_settings(cx, |settings| {
8792                settings.preview_tabs.get_or_insert_default().enabled = Some(true);
8793            });
8794        });
8795
8796        let fs = FakeFs::new(cx.executor());
8797        let project = Project::test(fs, None, cx).await;
8798        let (workspace, cx) =
8799            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
8800        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
8801
8802        // Add an item as preview
8803        let item = pane.update_in(cx, |pane, window, cx| {
8804            let item = Box::new(cx.new(|cx| TestItem::new(cx).with_label("A")));
8805            pane.add_item(item.clone(), true, true, None, window, cx);
8806            pane.set_preview_item_id(Some(item.item_id()), cx);
8807            item
8808        });
8809
8810        // Verify item is preview
8811        pane.read_with(cx, |pane, _| {
8812            assert_eq!(pane.preview_item_id(), Some(item.item_id()));
8813        });
8814
8815        // Unpreview the item
8816        pane.update_in(cx, |pane, _window, _cx| {
8817            pane.unpreview_item_if_preview(item.item_id());
8818        });
8819
8820        // Verify item is no longer preview
8821        pane.read_with(cx, |pane, _| {
8822            assert_eq!(pane.preview_item_id(), None);
8823        });
8824
8825        // Close the item
8826        pane.update_in(cx, |pane, window, cx| {
8827            pane.close_item_by_id(item.item_id(), SaveIntent::Skip, window, cx)
8828                .detach_and_log_err(cx);
8829        });
8830
8831        cx.run_until_parked();
8832
8833        // The item should be in the closed_stack and reopenable
8834        let has_closed_items = pane.read_with(cx, |pane, _| {
8835            !pane.nav_history.0.lock().closed_stack.is_empty()
8836        });
8837        assert!(
8838            has_closed_items,
8839            "closed item should be in closed_stack and reopenable"
8840        );
8841    }
8842
8843    #[gpui::test]
8844    async fn test_activate_item_with_wrap_around(cx: &mut TestAppContext) {
8845        init_test(cx);
8846        let fs = FakeFs::new(cx.executor());
8847        let project = Project::test(fs, None, cx).await;
8848        let (workspace, cx) =
8849            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
8850        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
8851
8852        add_labeled_item(&pane, "A", false, cx);
8853        add_labeled_item(&pane, "B", false, cx);
8854        add_labeled_item(&pane, "C", false, cx);
8855        assert_item_labels(&pane, ["A", "B", "C*"], cx);
8856
8857        pane.update_in(cx, |pane, window, cx| {
8858            pane.activate_next_item(&ActivateNextItem { wrap_around: false }, window, cx);
8859        });
8860        assert_item_labels(&pane, ["A", "B", "C*"], cx);
8861
8862        pane.update_in(cx, |pane, window, cx| {
8863            pane.activate_next_item(&ActivateNextItem::default(), window, cx);
8864        });
8865        assert_item_labels(&pane, ["A*", "B", "C"], cx);
8866
8867        pane.update_in(cx, |pane, window, cx| {
8868            pane.activate_previous_item(&ActivatePreviousItem { wrap_around: false }, window, cx);
8869        });
8870        assert_item_labels(&pane, ["A*", "B", "C"], cx);
8871
8872        pane.update_in(cx, |pane, window, cx| {
8873            pane.activate_previous_item(&ActivatePreviousItem::default(), window, cx);
8874        });
8875        assert_item_labels(&pane, ["A", "B", "C*"], cx);
8876
8877        pane.update_in(cx, |pane, window, cx| {
8878            pane.activate_previous_item(&ActivatePreviousItem { wrap_around: false }, window, cx);
8879        });
8880        assert_item_labels(&pane, ["A", "B*", "C"], cx);
8881
8882        pane.update_in(cx, |pane, window, cx| {
8883            pane.activate_next_item(&ActivateNextItem { wrap_around: false }, window, cx);
8884        });
8885        assert_item_labels(&pane, ["A", "B", "C*"], cx);
8886    }
8887
8888    fn init_test(cx: &mut TestAppContext) {
8889        cx.update(|cx| {
8890            let settings_store = SettingsStore::test(cx);
8891            cx.set_global(settings_store);
8892            theme_settings::init(LoadThemes::JustBase, cx);
8893        });
8894    }
8895
8896    fn set_max_tabs(cx: &mut TestAppContext, value: Option<usize>) {
8897        cx.update_global(|store: &mut SettingsStore, cx| {
8898            store.update_user_settings(cx, |settings| {
8899                settings.workspace.max_tabs = value.map(|v| NonZero::new(v).unwrap())
8900            });
8901        });
8902    }
8903
8904    fn set_pinned_tabs_separate_row(cx: &mut TestAppContext, enabled: bool) {
8905        cx.update_global(|store: &mut SettingsStore, cx| {
8906            store.update_user_settings(cx, |settings| {
8907                settings
8908                    .tab_bar
8909                    .get_or_insert_default()
8910                    .show_pinned_tabs_in_separate_row = Some(enabled);
8911            });
8912        });
8913    }
8914
8915    fn add_labeled_item(
8916        pane: &Entity<Pane>,
8917        label: &str,
8918        is_dirty: bool,
8919        cx: &mut VisualTestContext,
8920    ) -> Box<Entity<TestItem>> {
8921        pane.update_in(cx, |pane, window, cx| {
8922            let labeled_item =
8923                Box::new(cx.new(|cx| TestItem::new(cx).with_label(label).with_dirty(is_dirty)));
8924            pane.add_item(labeled_item.clone(), false, false, None, window, cx);
8925            labeled_item
8926        })
8927    }
8928
8929    fn set_labeled_items<const COUNT: usize>(
8930        pane: &Entity<Pane>,
8931        labels: [&str; COUNT],
8932        cx: &mut VisualTestContext,
8933    ) -> [Box<Entity<TestItem>>; COUNT] {
8934        pane.update_in(cx, |pane, window, cx| {
8935            pane.items.clear();
8936            let mut active_item_index = 0;
8937
8938            let mut index = 0;
8939            let items = labels.map(|mut label| {
8940                if label.ends_with('*') {
8941                    label = label.trim_end_matches('*');
8942                    active_item_index = index;
8943                }
8944
8945                let labeled_item = Box::new(cx.new(|cx| TestItem::new(cx).with_label(label)));
8946                pane.add_item(labeled_item.clone(), false, false, None, window, cx);
8947                index += 1;
8948                labeled_item
8949            });
8950
8951            pane.activate_item(active_item_index, false, false, window, cx);
8952
8953            items
8954        })
8955    }
8956
8957    // Assert the item label, with the active item label suffixed with a '*'
8958    #[track_caller]
8959    fn assert_item_labels<const COUNT: usize>(
8960        pane: &Entity<Pane>,
8961        expected_states: [&str; COUNT],
8962        cx: &mut VisualTestContext,
8963    ) {
8964        let actual_states = pane.update(cx, |pane, cx| {
8965            pane.items
8966                .iter()
8967                .enumerate()
8968                .map(|(ix, item)| {
8969                    let mut state = item
8970                        .to_any_view()
8971                        .downcast::<TestItem>()
8972                        .unwrap()
8973                        .read(cx)
8974                        .label
8975                        .clone();
8976                    if ix == pane.active_item_index {
8977                        state.push('*');
8978                    }
8979                    if item.is_dirty(cx) {
8980                        state.push('^');
8981                    }
8982                    if pane.is_tab_pinned(ix) {
8983                        state.push('!');
8984                    }
8985                    state
8986                })
8987                .collect::<Vec<_>>()
8988        });
8989        assert_eq!(
8990            actual_states, expected_states,
8991            "pane items do not match expectation"
8992        );
8993    }
8994
8995    // Assert the item label, with the active item label expected active index
8996    #[track_caller]
8997    fn assert_item_labels_active_index(
8998        pane: &Entity<Pane>,
8999        expected_states: &[&str],
9000        expected_active_idx: usize,
9001        cx: &mut VisualTestContext,
9002    ) {
9003        let actual_states = pane.update(cx, |pane, cx| {
9004            pane.items
9005                .iter()
9006                .enumerate()
9007                .map(|(ix, item)| {
9008                    let mut state = item
9009                        .to_any_view()
9010                        .downcast::<TestItem>()
9011                        .unwrap()
9012                        .read(cx)
9013                        .label
9014                        .clone();
9015                    if ix == pane.active_item_index {
9016                        assert_eq!(ix, expected_active_idx);
9017                    }
9018                    if item.is_dirty(cx) {
9019                        state.push('^');
9020                    }
9021                    if pane.is_tab_pinned(ix) {
9022                        state.push('!');
9023                    }
9024                    state
9025                })
9026                .collect::<Vec<_>>()
9027        });
9028        assert_eq!(
9029            actual_states, expected_states,
9030            "pane items do not match expectation"
9031        );
9032    }
9033
9034    #[track_caller]
9035    fn assert_pane_ids_on_axis<const COUNT: usize>(
9036        workspace: &Entity<Workspace>,
9037        expected_ids: [&EntityId; COUNT],
9038        expected_axis: Axis,
9039        cx: &mut VisualTestContext,
9040    ) {
9041        workspace.read_with(cx, |workspace, _| match &workspace.center.root {
9042            Member::Axis(axis) => {
9043                assert_eq!(axis.axis, expected_axis);
9044                assert_eq!(axis.members.len(), expected_ids.len());
9045                assert!(
9046                    zip(expected_ids, &axis.members).all(|(e, a)| {
9047                        if let Member::Pane(p) = a {
9048                            p.entity_id() == *e
9049                        } else {
9050                            false
9051                        }
9052                    }),
9053                    "pane ids do not match expectation: {expected_ids:?} != {actual_ids:?}",
9054                    actual_ids = axis.members
9055                );
9056            }
9057            Member::Pane(_) => panic!("expected axis"),
9058        });
9059    }
9060
9061    async fn test_single_pane_split<const COUNT: usize>(
9062        pane_labels: [&str; COUNT],
9063        direction: SplitDirection,
9064        operation: SplitMode,
9065        cx: &mut TestAppContext,
9066    ) {
9067        init_test(cx);
9068        let fs = FakeFs::new(cx.executor());
9069        let project = Project::test(fs, None, cx).await;
9070        let (workspace, cx) =
9071            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
9072
9073        let mut pane_before =
9074            workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
9075        for label in pane_labels {
9076            add_labeled_item(&pane_before, label, false, cx);
9077        }
9078        pane_before.update_in(cx, |pane, window, cx| {
9079            pane.split(direction, operation, window, cx)
9080        });
9081        cx.executor().run_until_parked();
9082        let pane_after = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
9083
9084        let num_labels = pane_labels.len();
9085        let last_as_active = format!("{}*", String::from(pane_labels[num_labels - 1]));
9086
9087        // check labels for all split operations
9088        match operation {
9089            SplitMode::EmptyPane => {
9090                assert_item_labels_active_index(&pane_before, &pane_labels, num_labels - 1, cx);
9091                assert_item_labels(&pane_after, [], cx);
9092            }
9093            SplitMode::ClonePane => {
9094                assert_item_labels_active_index(&pane_before, &pane_labels, num_labels - 1, cx);
9095                assert_item_labels(&pane_after, [&last_as_active], cx);
9096            }
9097            SplitMode::MovePane => {
9098                let head = &pane_labels[..(num_labels - 1)];
9099                if num_labels == 1 {
9100                    // We special-case this behavior and actually execute an empty pane command
9101                    // followed by a refocus of the old pane for this case.
9102                    pane_before = workspace.read_with(cx, |workspace, _cx| {
9103                        workspace
9104                            .panes()
9105                            .into_iter()
9106                            .find(|pane| *pane != &pane_after)
9107                            .unwrap()
9108                            .clone()
9109                    });
9110                };
9111
9112                assert_item_labels_active_index(
9113                    &pane_before,
9114                    &head,
9115                    head.len().saturating_sub(1),
9116                    cx,
9117                );
9118                assert_item_labels(&pane_after, [&last_as_active], cx);
9119                pane_after.update_in(cx, |pane, window, cx| {
9120                    window.focused(cx).is_some_and(|focus_handle| {
9121                        focus_handle == pane.active_item().unwrap().item_focus_handle(cx)
9122                    })
9123                });
9124            }
9125        }
9126
9127        // expected axis depends on split direction
9128        let expected_axis = match direction {
9129            SplitDirection::Right | SplitDirection::Left => Axis::Horizontal,
9130            SplitDirection::Up | SplitDirection::Down => Axis::Vertical,
9131        };
9132
9133        // expected ids depends on split direction
9134        let expected_ids = match direction {
9135            SplitDirection::Right | SplitDirection::Down => {
9136                [&pane_before.entity_id(), &pane_after.entity_id()]
9137            }
9138            SplitDirection::Left | SplitDirection::Up => {
9139                [&pane_after.entity_id(), &pane_before.entity_id()]
9140            }
9141        };
9142
9143        // check pane axes for all operations
9144        match operation {
9145            SplitMode::EmptyPane | SplitMode::ClonePane => {
9146                assert_pane_ids_on_axis(&workspace, expected_ids, expected_axis, cx);
9147            }
9148            SplitMode::MovePane => {
9149                assert_pane_ids_on_axis(&workspace, expected_ids, expected_axis, cx);
9150            }
9151        }
9152    }
9153
9154    #[test]
9155    fn test_dirty_message_for_escapes_markdown_in_path() {
9156        let project_path = ProjectPath {
9157            worktree_id: WorktreeId::from_usize(0),
9158            path: util::rel_path::rel_path("dir/__init__.py").into(),
9159        };
9160        assert_eq!(
9161            dirty_message_for(Some(project_path), PathStyle::Unix),
9162            "`dir/__init__.py` contains unsaved edits. Do you want to save it?"
9163        );
9164        assert_eq!(
9165            dirty_message_for(None, PathStyle::Unix),
9166            "This buffer contains unsaved edits. Do you want to save it?"
9167        );
9168    }
9169
9170    mod property_test {
9171        use super::*;
9172        use proptest::prelude::*;
9173        use serde_json::json;
9174        use std::{collections::HashSet, sync::Arc};
9175        use util::{
9176            path,
9177            rel_path::{RelPath, rel_path},
9178        };
9179
9180        struct TestFileItem {
9181            project_path: ProjectPath,
9182        }
9183
9184        impl project::ProjectItem for TestFileItem {
9185            fn try_open(
9186                _project: &Entity<Project>,
9187                path: &ProjectPath,
9188                cx: &mut App,
9189            ) -> Option<Task<anyhow::Result<Entity<Self>>>> {
9190                let project_path = path.clone();
9191                Some(cx.spawn(async move |cx| Ok(cx.new(|_| Self { project_path }))))
9192            }
9193
9194            fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
9195                None
9196            }
9197
9198            fn project_path(&self, _: &App) -> Option<ProjectPath> {
9199                Some(self.project_path.clone())
9200            }
9201
9202            fn is_dirty(&self) -> bool {
9203                false
9204            }
9205        }
9206
9207        struct TestItemView {
9208            focus_handle: FocusHandle,
9209            project_item: Entity<TestFileItem>,
9210            nav_history: Option<ItemNavHistory>,
9211        }
9212
9213        impl EventEmitter<()> for TestItemView {}
9214
9215        impl Focusable for TestItemView {
9216            fn focus_handle(&self, _cx: &App) -> FocusHandle {
9217                self.focus_handle.clone()
9218            }
9219        }
9220
9221        impl Render for TestItemView {
9222            fn render(
9223                &mut self,
9224                _window: &mut Window,
9225                _cx: &mut Context<Self>,
9226            ) -> impl IntoElement {
9227                gpui::Empty
9228            }
9229        }
9230
9231        impl Item for TestItemView {
9232            type Event = ();
9233
9234            fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
9235                "".into()
9236            }
9237
9238            fn for_each_project_item(
9239                &self,
9240                cx: &App,
9241                f: &mut dyn FnMut(EntityId, &dyn project::ProjectItem),
9242            ) {
9243                f(self.project_item.entity_id(), self.project_item.read(cx))
9244            }
9245
9246            fn buffer_kind(&self, _: &App) -> ItemBufferKind {
9247                ItemBufferKind::Singleton
9248            }
9249
9250            fn set_nav_history(
9251                &mut self,
9252                history: ItemNavHistory,
9253                _window: &mut Window,
9254                _: &mut Context<Self>,
9255            ) {
9256                self.nav_history = Some(history);
9257            }
9258
9259            fn deactivated(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
9260                if let Some(nav_history) = self.nav_history.as_mut() {
9261                    nav_history.push::<()>(None, None, cx);
9262                }
9263            }
9264        }
9265
9266        impl crate::ProjectItem for TestItemView {
9267            type Item = TestFileItem;
9268
9269            fn for_project_item(
9270                _project: Entity<Project>,
9271                _pane: Option<&Pane>,
9272                item: Entity<Self::Item>,
9273                _: &mut Window,
9274                cx: &mut Context<Self>,
9275            ) -> Self
9276            where
9277                Self: Sized,
9278            {
9279                Self {
9280                    focus_handle: cx.focus_handle(),
9281                    project_item: item,
9282                    nav_history: None,
9283                }
9284            }
9285        }
9286
9287        fn arbitrary_path() -> impl Strategy<Value = Arc<RelPath>> {
9288            prop_oneof![
9289                Just(rel_path("1.txt").into()),
9290                Just(rel_path("2.js").into()),
9291                Just(rel_path("3.rs").into()),
9292            ]
9293        }
9294
9295        #[derive(Debug, Clone, proptest_derive::Arbitrary)]
9296        enum Operation {
9297            Open {
9298                #[proptest(strategy = "arbitrary_path()")]
9299                path: Arc<RelPath>,
9300                allow_preview: bool,
9301            },
9302            GoBack,
9303            GoForward,
9304        }
9305
9306        struct Oracle {
9307            /// The active item's path, if known.
9308            current: Option<Arc<RelPath>>,
9309            /// The path that the back button would navigate to, if known.
9310            previous: Option<Arc<RelPath>>,
9311            /// The path that the forward button would navigate to, if known.
9312            next: Option<Arc<RelPath>>,
9313        }
9314
9315        impl Oracle {
9316            fn new() -> Self {
9317                Self {
9318                    current: None,
9319                    previous: None,
9320                    next: None,
9321                }
9322            }
9323
9324            fn apply(&mut self, operation: Operation) {
9325                match operation {
9326                    Operation::Open { path, .. } => {
9327                        if self.current.as_ref() != Some(&path) {
9328                            self.previous = self.current.replace(path);
9329                            self.next = None;
9330                        }
9331                    }
9332                    Operation::GoBack => {
9333                        if let Some(previous) = self.previous.take() {
9334                            self.next = self.current.replace(previous);
9335                        } else {
9336                            // `previous` isn't set, so backward navigation may not have been
9337                            // possible, hence we don't know which item a following forward
9338                            // navigation will lead to
9339                            self.next = None;
9340                            self.current = None;
9341                        }
9342                    }
9343                    Operation::GoForward => {
9344                        if let Some(next) = self.next.take() {
9345                            self.previous = self.current.replace(next);
9346                        } else {
9347                            self.previous = None;
9348                            self.current = None;
9349                        }
9350                    }
9351                }
9352            }
9353        }
9354
9355        struct PaneHarness {
9356            cx: VisualTestContext,
9357            workspace: Entity<Workspace>,
9358            pane: Entity<Pane>,
9359            worktree_id: WorktreeId,
9360        }
9361
9362        impl PaneHarness {
9363            async fn new(cx: &mut TestAppContext) -> Self {
9364                init_test(cx);
9365                cx.update(|cx| crate::register_project_item::<TestItemView>(cx));
9366
9367                let fs = FakeFs::new(cx.executor());
9368                fs.insert_tree(
9369                    path!("/root"),
9370                    json!({
9371                        "1.txt": "one",
9372                        "2.js": "two",
9373                        "3.rs": "three",
9374                    }),
9375                )
9376                .await;
9377                let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
9378                let worktree_id = project.update(cx, |project, cx| {
9379                    project.worktrees(cx).next().unwrap().read(cx).id()
9380                });
9381                let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
9382                let workspace = window.root(cx).unwrap();
9383                let cx = VisualTestContext::from_window(*window, cx);
9384                let pane = workspace.read_with(&cx, |workspace, _| workspace.active_pane().clone());
9385
9386                Self {
9387                    cx,
9388                    workspace,
9389                    pane,
9390                    worktree_id,
9391                }
9392            }
9393
9394            async fn apply(&mut self, operation: Operation) {
9395                match operation {
9396                    Operation::Open {
9397                        path,
9398                        allow_preview,
9399                    } => {
9400                        self.workspace
9401                            .update_in(&mut self.cx, |workspace, window, cx| {
9402                                workspace.open_path_preview(
9403                                    ProjectPath {
9404                                        worktree_id: self.worktree_id,
9405                                        path,
9406                                    },
9407                                    None,
9408                                    true,
9409                                    allow_preview,
9410                                    true,
9411                                    window,
9412                                    cx,
9413                                )
9414                            })
9415                            .await
9416                            .unwrap();
9417                    }
9418                    Operation::GoBack => {
9419                        self.workspace
9420                            .update_in(&mut self.cx, |workspace, window, cx| {
9421                                workspace.go_back(self.pane.downgrade(), window, cx)
9422                            })
9423                            .await
9424                            .unwrap();
9425                    }
9426                    Operation::GoForward => {
9427                        self.workspace
9428                            .update_in(&mut self.cx, |workspace, window, cx| {
9429                                workspace.go_forward(self.pane.downgrade(), window, cx)
9430                            })
9431                            .await
9432                            .unwrap();
9433                    }
9434                }
9435            }
9436
9437            fn check_invariants(&self, expected_path: &Option<Arc<RelPath>>) {
9438                self.pane.read_with(&self.cx, |pane, cx| {
9439                    let open_paths = pane
9440                        .items()
9441                        .map(|item| item.project_path(cx).unwrap())
9442                        .collect::<Vec<_>>();
9443                    let active_path = pane
9444                        .active_item()
9445                        .map(|item| item.project_path(cx).unwrap());
9446                    let preview_path = pane
9447                        .preview_item()
9448                        .map(|item| item.project_path(cx).unwrap());
9449
9450                    let unique_paths = open_paths.iter().collect::<HashSet<_>>();
9451                    assert_eq!(
9452                        unique_paths.len(),
9453                        open_paths.len(),
9454                        "pane should not contain duplicate open paths"
9455                    );
9456
9457                    assert_eq!(
9458                        active_path.is_none(),
9459                        open_paths.is_empty(),
9460                        "pane should have an active item iff it has open paths"
9461                    );
9462                    assert!(
9463                        active_path
9464                            .as_ref()
9465                            .is_none_or(|path| open_paths.contains(path)),
9466                        "active path should be open"
9467                    );
9468                    assert!(
9469                        preview_path
9470                            .as_ref()
9471                            .is_none_or(|path| open_paths.contains(path)),
9472                        "preview path should be open"
9473                    );
9474
9475                    if let Some(expected_active_path) = expected_path {
9476                        assert_eq!(
9477                            &active_path.as_ref().unwrap().path,
9478                            expected_active_path,
9479                            "active path should match the oracle"
9480                        );
9481                    }
9482                });
9483            }
9484        }
9485
9486        #[gpui::property_test]
9487        async fn single_pane_navigation(
9488            #[strategy = proptest::collection::vec(any::<Operation>(), 1..32)] operations: Vec<
9489                Operation,
9490            >,
9491            cx: &mut TestAppContext,
9492        ) {
9493            let mut harness = PaneHarness::new(cx).await;
9494            let mut oracle = Oracle::new();
9495
9496            for operation in operations {
9497                oracle.apply(operation.clone());
9498                harness.apply(operation).await;
9499                harness.check_invariants(&oracle.current);
9500            }
9501        }
9502    }
9503}
9504
Served at tenant.openagents/omega Member data and write actions are omitted.