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