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