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