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
2928 if !was_pinned_in_from_pane && is_pinned_in_to_pane {
2929 this.pinned_tab_count += 1;
2930 } else if was_pinned_in_from_pane && !is_pinned_in_to_pane {
2931 this.pinned_tab_count -= 1;
2932 }
2933 } else if this.items.len() >= to_pane_old_length {
2934 let is_pinned_in_to_pane = this.is_tab_pinned(ix);
2935 let item_created_pane = to_pane_old_length == 0;
2936 let is_first_position = ix == 0;
2937 let was_dropped_at_beginning = item_created_pane || is_first_position;
2938 let should_remain_pinned = is_pinned_in_to_pane
2939 || (was_pinned_in_from_pane && was_dropped_at_beginning);
2940
2941 if should_remain_pinned {
2942 this.pinned_tab_count += 1;
2943 }
2944 }
2945 });
2946 });
2947 })
2948 .log_err();
2949 }
2950
2951 fn handle_dragged_selection_drop(
2952 &mut self,
2953 dragged_selection: &DraggedSelection,
2954 dragged_onto: Option<usize>,
2955 window: &mut Window,
2956 cx: &mut Context<Self>,
2957 ) {
2958 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2959 if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_selection, window, cx)
2960 {
2961 return;
2962 }
2963 }
2964 self.handle_project_entry_drop(
2965 &dragged_selection.active_selection.entry_id,
2966 dragged_onto,
2967 window,
2968 cx,
2969 );
2970 }
2971
2972 fn handle_project_entry_drop(
2973 &mut self,
2974 project_entry_id: &ProjectEntryId,
2975 target: Option<usize>,
2976 window: &mut Window,
2977 cx: &mut Context<Self>,
2978 ) {
2979 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2980 if let ControlFlow::Break(()) = custom_drop_handle(self, project_entry_id, window, cx) {
2981 return;
2982 }
2983 }
2984 let mut to_pane = cx.entity().clone();
2985 let split_direction = self.drag_split_direction;
2986 let project_entry_id = *project_entry_id;
2987 self.workspace
2988 .update(cx, |_, cx| {
2989 cx.defer_in(window, move |workspace, window, cx| {
2990 if let Some(project_path) = workspace
2991 .project()
2992 .read(cx)
2993 .path_for_entry(project_entry_id, cx)
2994 {
2995 let load_path_task = workspace.load_path(project_path.clone(), window, cx);
2996 cx.spawn_in(window, async move |workspace, cx| {
2997 if let Some((project_entry_id, build_item)) =
2998 load_path_task.await.notify_async_err(cx)
2999 {
3000 let (to_pane, new_item_handle) = workspace
3001 .update_in(cx, |workspace, window, cx| {
3002 if let Some(split_direction) = split_direction {
3003 to_pane = workspace.split_pane(
3004 to_pane,
3005 split_direction,
3006 window,
3007 cx,
3008 );
3009 }
3010 let new_item_handle = to_pane.update(cx, |pane, cx| {
3011 pane.open_item(
3012 project_entry_id,
3013 project_path,
3014 true,
3015 false,
3016 true,
3017 target,
3018 window,
3019 cx,
3020 build_item,
3021 )
3022 });
3023 (to_pane, new_item_handle)
3024 })
3025 .log_err()?;
3026 to_pane
3027 .update_in(cx, |this, window, cx| {
3028 let Some(index) = this.index_for_item(&*new_item_handle)
3029 else {
3030 return;
3031 };
3032
3033 if target.map_or(false, |target| this.is_tab_pinned(target))
3034 {
3035 this.pin_tab_at(index, window, cx);
3036 }
3037 })
3038 .ok()?
3039 }
3040 Some(())
3041 })
3042 .detach();
3043 };
3044 });
3045 })
3046 .log_err();
3047 }
3048
3049 fn handle_external_paths_drop(
3050 &mut self,
3051 paths: &ExternalPaths,
3052 window: &mut Window,
3053 cx: &mut Context<Self>,
3054 ) {
3055 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
3056 if let ControlFlow::Break(()) = custom_drop_handle(self, paths, window, cx) {
3057 return;
3058 }
3059 }
3060 let mut to_pane = cx.entity().clone();
3061 let mut split_direction = self.drag_split_direction;
3062 let paths = paths.paths().to_vec();
3063 let is_remote = self
3064 .workspace
3065 .update(cx, |workspace, cx| {
3066 if workspace.project().read(cx).is_via_collab() {
3067 workspace.show_error(
3068 &anyhow::anyhow!("Cannot drop files on a remote project"),
3069 cx,
3070 );
3071 true
3072 } else {
3073 false
3074 }
3075 })
3076 .unwrap_or(true);
3077 if is_remote {
3078 return;
3079 }
3080
3081 self.workspace
3082 .update(cx, |workspace, cx| {
3083 let fs = Arc::clone(workspace.project().read(cx).fs());
3084 cx.spawn_in(window, async move |workspace, cx| {
3085 let mut is_file_checks = FuturesUnordered::new();
3086 for path in &paths {
3087 is_file_checks.push(fs.is_file(path))
3088 }
3089 let mut has_files_to_open = false;
3090 while let Some(is_file) = is_file_checks.next().await {
3091 if is_file {
3092 has_files_to_open = true;
3093 break;
3094 }
3095 }
3096 drop(is_file_checks);
3097 if !has_files_to_open {
3098 split_direction = None;
3099 }
3100
3101 if let Ok(open_task) = workspace.update_in(cx, |workspace, window, cx| {
3102 if let Some(split_direction) = split_direction {
3103 to_pane = workspace.split_pane(to_pane, split_direction, window, cx);
3104 }
3105 workspace.open_paths(
3106 paths,
3107 OpenOptions {
3108 visible: Some(OpenVisible::OnlyDirectories),
3109 ..Default::default()
3110 },
3111 Some(to_pane.downgrade()),
3112 window,
3113 cx,
3114 )
3115 }) {
3116 let opened_items: Vec<_> = open_task.await;
3117 _ = workspace.update(cx, |workspace, cx| {
3118 for item in opened_items.into_iter().flatten() {
3119 if let Err(e) = item {
3120 workspace.show_error(&e, cx);
3121 }
3122 }
3123 });
3124 }
3125 })
3126 .detach();
3127 })
3128 .log_err();
3129 }
3130
3131 pub fn display_nav_history_buttons(&mut self, display: Option<bool>) {
3132 self.display_nav_history_buttons = display;
3133 }
3134
3135 fn pinned_item_ids(&self) -> HashSet<EntityId> {
3136 self.items
3137 .iter()
3138 .enumerate()
3139 .filter_map(|(index, item)| {
3140 if self.is_tab_pinned(index) {
3141 return Some(item.item_id());
3142 }
3143
3144 None
3145 })
3146 .collect()
3147 }
3148
3149 fn clean_item_ids(&self, cx: &mut Context<Pane>) -> HashSet<EntityId> {
3150 self.items()
3151 .filter_map(|item| {
3152 if !item.is_dirty(cx) {
3153 return Some(item.item_id());
3154 }
3155
3156 None
3157 })
3158 .collect()
3159 }
3160
3161 fn to_the_side_item_ids(&self, item_id: EntityId, side: Side) -> HashSet<EntityId> {
3162 match side {
3163 Side::Left => self
3164 .items()
3165 .take_while(|item| item.item_id() != item_id)
3166 .map(|item| item.item_id())
3167 .collect(),
3168 Side::Right => self
3169 .items()
3170 .rev()
3171 .take_while(|item| item.item_id() != item_id)
3172 .map(|item| item.item_id())
3173 .collect(),
3174 }
3175 }
3176
3177 pub fn drag_split_direction(&self) -> Option<SplitDirection> {
3178 self.drag_split_direction
3179 }
3180
3181 pub fn set_zoom_out_on_close(&mut self, zoom_out_on_close: bool) {
3182 self.zoom_out_on_close = zoom_out_on_close;
3183 }
3184}
3185
3186fn default_render_tab_bar_buttons(
3187 pane: &mut Pane,
3188 window: &mut Window,
3189 cx: &mut Context<Pane>,
3190) -> (Option<AnyElement>, Option<AnyElement>) {
3191 if !pane.has_focus(window, cx) && !pane.context_menu_focused(window, cx) {
3192 return (None, None);
3193 }
3194 // Ideally we would return a vec of elements here to pass directly to the [TabBar]'s
3195 // `end_slot`, but due to needing a view here that isn't possible.
3196 let right_children = h_flex()
3197 // Instead we need to replicate the spacing from the [TabBar]'s `end_slot` here.
3198 .gap(DynamicSpacing::Base04.rems(cx))
3199 .child(
3200 PopoverMenu::new("pane-tab-bar-popover-menu")
3201 .trigger_with_tooltip(
3202 IconButton::new("plus", IconName::Plus).icon_size(IconSize::Small),
3203 Tooltip::text("New..."),
3204 )
3205 .anchor(Corner::TopRight)
3206 .with_handle(pane.new_item_context_menu_handle.clone())
3207 .menu(move |window, cx| {
3208 Some(ContextMenu::build(window, cx, |menu, _, _| {
3209 menu.action("New File", NewFile.boxed_clone())
3210 .action("Open File", ToggleFileFinder::default().boxed_clone())
3211 .separator()
3212 .action(
3213 "Search Project",
3214 DeploySearch {
3215 replace_enabled: false,
3216 included_files: None,
3217 excluded_files: None,
3218 }
3219 .boxed_clone(),
3220 )
3221 .action("Search Symbols", ToggleProjectSymbols.boxed_clone())
3222 .separator()
3223 .action("New Terminal", NewTerminal.boxed_clone())
3224 }))
3225 }),
3226 )
3227 .child(
3228 PopoverMenu::new("pane-tab-bar-split")
3229 .trigger_with_tooltip(
3230 IconButton::new("split", IconName::Split).icon_size(IconSize::Small),
3231 Tooltip::text("Split Pane"),
3232 )
3233 .anchor(Corner::TopRight)
3234 .with_handle(pane.split_item_context_menu_handle.clone())
3235 .menu(move |window, cx| {
3236 ContextMenu::build(window, cx, |menu, _, _| {
3237 menu.action("Split Right", SplitRight.boxed_clone())
3238 .action("Split Left", SplitLeft.boxed_clone())
3239 .action("Split Up", SplitUp.boxed_clone())
3240 .action("Split Down", SplitDown.boxed_clone())
3241 })
3242 .into()
3243 }),
3244 )
3245 .child({
3246 let zoomed = pane.is_zoomed();
3247 IconButton::new("toggle_zoom", IconName::Maximize)
3248 .icon_size(IconSize::Small)
3249 .toggle_state(zoomed)
3250 .selected_icon(IconName::Minimize)
3251 .on_click(cx.listener(|pane, _, window, cx| {
3252 pane.toggle_zoom(&crate::ToggleZoom, window, cx);
3253 }))
3254 .tooltip(move |window, cx| {
3255 Tooltip::for_action(
3256 if zoomed { "Zoom Out" } else { "Zoom In" },
3257 &ToggleZoom,
3258 window,
3259 cx,
3260 )
3261 })
3262 })
3263 .into_any_element()
3264 .into();
3265 (None, right_children)
3266}
3267
3268impl Focusable for Pane {
3269 fn focus_handle(&self, _cx: &App) -> FocusHandle {
3270 self.focus_handle.clone()
3271 }
3272}
3273
3274impl Render for Pane {
3275 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3276 let mut key_context = KeyContext::new_with_defaults();
3277 key_context.add("Pane");
3278 if self.active_item().is_none() {
3279 key_context.add("EmptyPane");
3280 }
3281
3282 let should_display_tab_bar = self.should_display_tab_bar.clone();
3283 let display_tab_bar = should_display_tab_bar(window, cx);
3284 let Some(project) = self.project.upgrade() else {
3285 return div().track_focus(&self.focus_handle(cx));
3286 };
3287 let is_local = project.read(cx).is_local();
3288
3289 v_flex()
3290 .key_context(key_context)
3291 .track_focus(&self.focus_handle(cx))
3292 .size_full()
3293 .flex_none()
3294 .overflow_hidden()
3295 .on_action(cx.listener(|pane, _: &AlternateFile, window, cx| {
3296 pane.alternate_file(window, cx);
3297 }))
3298 .on_action(
3299 cx.listener(|pane, _: &SplitLeft, _, cx| pane.split(SplitDirection::Left, cx)),
3300 )
3301 .on_action(cx.listener(|pane, _: &SplitUp, _, cx| pane.split(SplitDirection::Up, cx)))
3302 .on_action(cx.listener(|pane, _: &SplitHorizontal, _, cx| {
3303 pane.split(SplitDirection::horizontal(cx), cx)
3304 }))
3305 .on_action(cx.listener(|pane, _: &SplitVertical, _, cx| {
3306 pane.split(SplitDirection::vertical(cx), cx)
3307 }))
3308 .on_action(
3309 cx.listener(|pane, _: &SplitRight, _, cx| pane.split(SplitDirection::Right, cx)),
3310 )
3311 .on_action(
3312 cx.listener(|pane, _: &SplitDown, _, cx| pane.split(SplitDirection::Down, cx)),
3313 )
3314 .on_action(
3315 cx.listener(|pane, _: &GoBack, window, cx| pane.navigate_backward(window, cx)),
3316 )
3317 .on_action(
3318 cx.listener(|pane, _: &GoForward, window, cx| pane.navigate_forward(window, cx)),
3319 )
3320 .on_action(cx.listener(|_, _: &JoinIntoNext, _, cx| {
3321 cx.emit(Event::JoinIntoNext);
3322 }))
3323 .on_action(cx.listener(|_, _: &JoinAll, _, cx| {
3324 cx.emit(Event::JoinAll);
3325 }))
3326 .on_action(cx.listener(Pane::toggle_zoom))
3327 .on_action(
3328 cx.listener(|pane: &mut Pane, action: &ActivateItem, window, cx| {
3329 pane.activate_item(
3330 action.0.min(pane.items.len().saturating_sub(1)),
3331 true,
3332 true,
3333 window,
3334 cx,
3335 );
3336 }),
3337 )
3338 .on_action(
3339 cx.listener(|pane: &mut Pane, _: &ActivateLastItem, window, cx| {
3340 pane.activate_item(pane.items.len().saturating_sub(1), true, true, window, cx);
3341 }),
3342 )
3343 .on_action(
3344 cx.listener(|pane: &mut Pane, _: &ActivatePreviousItem, window, cx| {
3345 pane.activate_prev_item(true, window, cx);
3346 }),
3347 )
3348 .on_action(
3349 cx.listener(|pane: &mut Pane, _: &ActivateNextItem, window, cx| {
3350 pane.activate_next_item(true, window, cx);
3351 }),
3352 )
3353 .on_action(
3354 cx.listener(|pane, _: &SwapItemLeft, window, cx| pane.swap_item_left(window, cx)),
3355 )
3356 .on_action(
3357 cx.listener(|pane, _: &SwapItemRight, window, cx| pane.swap_item_right(window, cx)),
3358 )
3359 .on_action(cx.listener(|pane, action, window, cx| {
3360 pane.toggle_pin_tab(action, window, cx);
3361 }))
3362 .when(PreviewTabsSettings::get_global(cx).enabled, |this| {
3363 this.on_action(cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, _, cx| {
3364 if let Some(active_item_id) = pane.active_item().map(|i| i.item_id()) {
3365 if pane.is_active_preview_item(active_item_id) {
3366 pane.set_preview_item_id(None, cx);
3367 } else {
3368 pane.set_preview_item_id(Some(active_item_id), cx);
3369 }
3370 }
3371 }))
3372 })
3373 .on_action(
3374 cx.listener(|pane: &mut Self, action: &CloseActiveItem, window, cx| {
3375 pane.close_active_item(action, window, cx)
3376 .detach_and_log_err(cx)
3377 }),
3378 )
3379 .on_action(
3380 cx.listener(|pane: &mut Self, action: &CloseInactiveItems, window, cx| {
3381 pane.close_inactive_items(action, window, cx)
3382 .detach_and_log_err(cx);
3383 }),
3384 )
3385 .on_action(
3386 cx.listener(|pane: &mut Self, action: &CloseCleanItems, window, cx| {
3387 pane.close_clean_items(action, window, cx)
3388 .detach_and_log_err(cx)
3389 }),
3390 )
3391 .on_action(cx.listener(
3392 |pane: &mut Self, action: &CloseItemsToTheLeft, window, cx| {
3393 pane.close_items_to_the_left_by_id(None, action, window, cx)
3394 .detach_and_log_err(cx)
3395 },
3396 ))
3397 .on_action(cx.listener(
3398 |pane: &mut Self, action: &CloseItemsToTheRight, window, cx| {
3399 pane.close_items_to_the_right_by_id(None, action, window, cx)
3400 .detach_and_log_err(cx)
3401 },
3402 ))
3403 .on_action(
3404 cx.listener(|pane: &mut Self, action: &CloseAllItems, window, cx| {
3405 pane.close_all_items(action, window, cx)
3406 .detach_and_log_err(cx)
3407 }),
3408 )
3409 .on_action(
3410 cx.listener(|pane: &mut Self, action: &RevealInProjectPanel, _, cx| {
3411 let entry_id = action
3412 .entry_id
3413 .map(ProjectEntryId::from_proto)
3414 .or_else(|| pane.active_item()?.project_entry_ids(cx).first().copied());
3415 if let Some(entry_id) = entry_id {
3416 pane.project
3417 .update(cx, |_, cx| {
3418 cx.emit(project::Event::RevealInProjectPanel(entry_id))
3419 })
3420 .ok();
3421 }
3422 }),
3423 )
3424 .on_action(cx.listener(|_, _: &menu::Cancel, window, cx| {
3425 if cx.stop_active_drag(window) {
3426 return;
3427 } else {
3428 cx.propagate();
3429 }
3430 }))
3431 .when(self.active_item().is_some() && display_tab_bar, |pane| {
3432 pane.child((self.render_tab_bar.clone())(self, window, cx))
3433 })
3434 .child({
3435 let has_worktrees = project.read(cx).visible_worktrees(cx).next().is_some();
3436 // main content
3437 div()
3438 .flex_1()
3439 .relative()
3440 .group("")
3441 .overflow_hidden()
3442 .on_drag_move::<DraggedTab>(cx.listener(Self::handle_drag_move))
3443 .on_drag_move::<DraggedSelection>(cx.listener(Self::handle_drag_move))
3444 .when(is_local, |div| {
3445 div.on_drag_move::<ExternalPaths>(cx.listener(Self::handle_drag_move))
3446 })
3447 .map(|div| {
3448 if let Some(item) = self.active_item() {
3449 div.id("pane_placeholder")
3450 .v_flex()
3451 .size_full()
3452 .overflow_hidden()
3453 .child(self.toolbar.clone())
3454 .child(item.to_any())
3455 } else {
3456 let placeholder = div
3457 .id("pane_placeholder")
3458 .h_flex()
3459 .size_full()
3460 .justify_center()
3461 .on_click(cx.listener(
3462 move |this, event: &ClickEvent, window, cx| {
3463 if event.up.click_count == 2 {
3464 window.dispatch_action(
3465 this.double_click_dispatch_action.boxed_clone(),
3466 cx,
3467 );
3468 }
3469 },
3470 ));
3471 if has_worktrees {
3472 placeholder
3473 } else {
3474 placeholder.child(
3475 Label::new("Open a file or project to get started.")
3476 .color(Color::Muted),
3477 )
3478 }
3479 }
3480 })
3481 .child(
3482 // drag target
3483 div()
3484 .invisible()
3485 .absolute()
3486 .bg(cx.theme().colors().drop_target_background)
3487 .group_drag_over::<DraggedTab>("", |style| style.visible())
3488 .group_drag_over::<DraggedSelection>("", |style| style.visible())
3489 .when(is_local, |div| {
3490 div.group_drag_over::<ExternalPaths>("", |style| style.visible())
3491 })
3492 .when_some(self.can_drop_predicate.clone(), |this, p| {
3493 this.can_drop(move |a, window, cx| p(a, window, cx))
3494 })
3495 .on_drop(cx.listener(move |this, dragged_tab, window, cx| {
3496 this.handle_tab_drop(
3497 dragged_tab,
3498 this.active_item_index(),
3499 window,
3500 cx,
3501 )
3502 }))
3503 .on_drop(cx.listener(
3504 move |this, selection: &DraggedSelection, window, cx| {
3505 this.handle_dragged_selection_drop(selection, None, window, cx)
3506 },
3507 ))
3508 .on_drop(cx.listener(move |this, paths, window, cx| {
3509 this.handle_external_paths_drop(paths, window, cx)
3510 }))
3511 .map(|div| {
3512 let size = DefiniteLength::Fraction(0.5);
3513 match self.drag_split_direction {
3514 None => div.top_0().right_0().bottom_0().left_0(),
3515 Some(SplitDirection::Up) => {
3516 div.top_0().left_0().right_0().h(size)
3517 }
3518 Some(SplitDirection::Down) => {
3519 div.left_0().bottom_0().right_0().h(size)
3520 }
3521 Some(SplitDirection::Left) => {
3522 div.top_0().left_0().bottom_0().w(size)
3523 }
3524 Some(SplitDirection::Right) => {
3525 div.top_0().bottom_0().right_0().w(size)
3526 }
3527 }
3528 }),
3529 )
3530 })
3531 .on_mouse_down(
3532 MouseButton::Navigate(NavigationDirection::Back),
3533 cx.listener(|pane, _, window, cx| {
3534 if let Some(workspace) = pane.workspace.upgrade() {
3535 let pane = cx.entity().downgrade();
3536 window.defer(cx, move |window, cx| {
3537 workspace.update(cx, |workspace, cx| {
3538 workspace.go_back(pane, window, cx).detach_and_log_err(cx)
3539 })
3540 })
3541 }
3542 }),
3543 )
3544 .on_mouse_down(
3545 MouseButton::Navigate(NavigationDirection::Forward),
3546 cx.listener(|pane, _, window, cx| {
3547 if let Some(workspace) = pane.workspace.upgrade() {
3548 let pane = cx.entity().downgrade();
3549 window.defer(cx, move |window, cx| {
3550 workspace.update(cx, |workspace, cx| {
3551 workspace
3552 .go_forward(pane, window, cx)
3553 .detach_and_log_err(cx)
3554 })
3555 })
3556 }
3557 }),
3558 )
3559 }
3560}
3561
3562impl ItemNavHistory {
3563 pub fn push<D: 'static + Send + Any>(&mut self, data: Option<D>, cx: &mut App) {
3564 if self
3565 .item
3566 .upgrade()
3567 .is_some_and(|item| item.include_in_nav_history())
3568 {
3569 self.history
3570 .push(data, self.item.clone(), self.is_preview, cx);
3571 }
3572 }
3573
3574 pub fn pop_backward(&mut self, cx: &mut App) -> Option<NavigationEntry> {
3575 self.history.pop(NavigationMode::GoingBack, cx)
3576 }
3577
3578 pub fn pop_forward(&mut self, cx: &mut App) -> Option<NavigationEntry> {
3579 self.history.pop(NavigationMode::GoingForward, cx)
3580 }
3581}
3582
3583impl NavHistory {
3584 pub fn for_each_entry(
3585 &self,
3586 cx: &App,
3587 mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
3588 ) {
3589 let borrowed_history = self.0.lock();
3590 borrowed_history
3591 .forward_stack
3592 .iter()
3593 .chain(borrowed_history.backward_stack.iter())
3594 .chain(borrowed_history.closed_stack.iter())
3595 .for_each(|entry| {
3596 if let Some(project_and_abs_path) =
3597 borrowed_history.paths_by_item.get(&entry.item.id())
3598 {
3599 f(entry, project_and_abs_path.clone());
3600 } else if let Some(item) = entry.item.upgrade() {
3601 if let Some(path) = item.project_path(cx) {
3602 f(entry, (path, None));
3603 }
3604 }
3605 })
3606 }
3607
3608 pub fn set_mode(&mut self, mode: NavigationMode) {
3609 self.0.lock().mode = mode;
3610 }
3611
3612 pub fn mode(&self) -> NavigationMode {
3613 self.0.lock().mode
3614 }
3615
3616 pub fn disable(&mut self) {
3617 self.0.lock().mode = NavigationMode::Disabled;
3618 }
3619
3620 pub fn enable(&mut self) {
3621 self.0.lock().mode = NavigationMode::Normal;
3622 }
3623
3624 pub fn pop(&mut self, mode: NavigationMode, cx: &mut App) -> Option<NavigationEntry> {
3625 let mut state = self.0.lock();
3626 let entry = match mode {
3627 NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
3628 return None;
3629 }
3630 NavigationMode::GoingBack => &mut state.backward_stack,
3631 NavigationMode::GoingForward => &mut state.forward_stack,
3632 NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
3633 }
3634 .pop_back();
3635 if entry.is_some() {
3636 state.did_update(cx);
3637 }
3638 entry
3639 }
3640
3641 pub fn push<D: 'static + Send + Any>(
3642 &mut self,
3643 data: Option<D>,
3644 item: Arc<dyn WeakItemHandle>,
3645 is_preview: bool,
3646 cx: &mut App,
3647 ) {
3648 let state = &mut *self.0.lock();
3649 match state.mode {
3650 NavigationMode::Disabled => {}
3651 NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
3652 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3653 state.backward_stack.pop_front();
3654 }
3655 state.backward_stack.push_back(NavigationEntry {
3656 item,
3657 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3658 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3659 is_preview,
3660 });
3661 state.forward_stack.clear();
3662 }
3663 NavigationMode::GoingBack => {
3664 if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3665 state.forward_stack.pop_front();
3666 }
3667 state.forward_stack.push_back(NavigationEntry {
3668 item,
3669 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3670 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3671 is_preview,
3672 });
3673 }
3674 NavigationMode::GoingForward => {
3675 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3676 state.backward_stack.pop_front();
3677 }
3678 state.backward_stack.push_back(NavigationEntry {
3679 item,
3680 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3681 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3682 is_preview,
3683 });
3684 }
3685 NavigationMode::ClosingItem => {
3686 if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3687 state.closed_stack.pop_front();
3688 }
3689 state.closed_stack.push_back(NavigationEntry {
3690 item,
3691 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3692 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3693 is_preview,
3694 });
3695 }
3696 }
3697 state.did_update(cx);
3698 }
3699
3700 pub fn remove_item(&mut self, item_id: EntityId) {
3701 let mut state = self.0.lock();
3702 state.paths_by_item.remove(&item_id);
3703 state
3704 .backward_stack
3705 .retain(|entry| entry.item.id() != item_id);
3706 state
3707 .forward_stack
3708 .retain(|entry| entry.item.id() != item_id);
3709 state
3710 .closed_stack
3711 .retain(|entry| entry.item.id() != item_id);
3712 }
3713
3714 pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option<PathBuf>)> {
3715 self.0.lock().paths_by_item.get(&item_id).cloned()
3716 }
3717}
3718
3719impl NavHistoryState {
3720 pub fn did_update(&self, cx: &mut App) {
3721 if let Some(pane) = self.pane.upgrade() {
3722 cx.defer(move |cx| {
3723 pane.update(cx, |pane, cx| pane.history_updated(cx));
3724 });
3725 }
3726 }
3727}
3728
3729fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
3730 let path = buffer_path
3731 .as_ref()
3732 .and_then(|p| {
3733 p.path
3734 .to_str()
3735 .and_then(|s| if s.is_empty() { None } else { Some(s) })
3736 })
3737 .unwrap_or("This buffer");
3738 let path = truncate_and_remove_front(path, 80);
3739 format!("{path} contains unsaved edits. Do you want to save it?")
3740}
3741
3742pub fn tab_details(items: &[Box<dyn ItemHandle>], _window: &Window, cx: &App) -> Vec<usize> {
3743 let mut tab_details = items.iter().map(|_| 0).collect::<Vec<_>>();
3744 let mut tab_descriptions = HashMap::default();
3745 let mut done = false;
3746 while !done {
3747 done = true;
3748
3749 // Store item indices by their tab description.
3750 for (ix, (item, detail)) in items.iter().zip(&tab_details).enumerate() {
3751 let description = item.tab_content_text(*detail, cx);
3752 if *detail == 0 || description != item.tab_content_text(detail - 1, cx) {
3753 tab_descriptions
3754 .entry(description)
3755 .or_insert(Vec::new())
3756 .push(ix);
3757 }
3758 }
3759
3760 // If two or more items have the same tab description, increase their level
3761 // of detail and try again.
3762 for (_, item_ixs) in tab_descriptions.drain() {
3763 if item_ixs.len() > 1 {
3764 done = false;
3765 for ix in item_ixs {
3766 tab_details[ix] += 1;
3767 }
3768 }
3769 }
3770 }
3771
3772 tab_details
3773}
3774
3775pub fn render_item_indicator(item: Box<dyn ItemHandle>, cx: &App) -> Option<Indicator> {
3776 maybe!({
3777 let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
3778 (true, _) => Color::Warning,
3779 (_, true) => Color::Accent,
3780 (false, false) => return None,
3781 };
3782
3783 Some(Indicator::dot().color(indicator_color))
3784 })
3785}
3786
3787impl Render for DraggedTab {
3788 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3789 let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
3790 let label = self.item.tab_content(
3791 TabContentParams {
3792 detail: Some(self.detail),
3793 selected: false,
3794 preview: false,
3795 deemphasized: false,
3796 },
3797 window,
3798 cx,
3799 );
3800 Tab::new("")
3801 .toggle_state(self.is_active)
3802 .child(label)
3803 .render(window, cx)
3804 .font(ui_font)
3805 }
3806}
3807
3808#[cfg(test)]
3809mod tests {
3810 use std::num::NonZero;
3811
3812 use super::*;
3813 use crate::item::test::{TestItem, TestProjectItem};
3814 use gpui::{TestAppContext, VisualTestContext};
3815 use project::FakeFs;
3816 use settings::SettingsStore;
3817 use theme::LoadThemes;
3818 use util::TryFutureExt;
3819
3820 #[gpui::test]
3821 async fn test_add_item_capped_to_max_tabs(cx: &mut TestAppContext) {
3822 init_test(cx);
3823 let fs = FakeFs::new(cx.executor());
3824
3825 let project = Project::test(fs, None, cx).await;
3826 let (workspace, cx) =
3827 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3828 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3829
3830 for i in 0..7 {
3831 add_labeled_item(&pane, format!("{}", i).as_str(), false, cx);
3832 }
3833 set_max_tabs(cx, Some(5));
3834 add_labeled_item(&pane, "7", false, cx);
3835 // Remove items to respect the max tab cap.
3836 assert_item_labels(&pane, ["3", "4", "5", "6", "7*"], cx);
3837 pane.update_in(cx, |pane, window, cx| {
3838 pane.activate_item(0, false, false, window, cx);
3839 });
3840 add_labeled_item(&pane, "X", false, cx);
3841 // Respect activation order.
3842 assert_item_labels(&pane, ["3", "X*", "5", "6", "7"], cx);
3843
3844 for i in 0..7 {
3845 add_labeled_item(&pane, format!("D{}", i).as_str(), true, cx);
3846 }
3847 // Keeps dirty items, even over max tab cap.
3848 assert_item_labels(
3849 &pane,
3850 ["D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6*^"],
3851 cx,
3852 );
3853
3854 set_max_tabs(cx, None);
3855 for i in 0..7 {
3856 add_labeled_item(&pane, format!("N{}", i).as_str(), false, cx);
3857 }
3858 // No cap when max tabs is None.
3859 assert_item_labels(
3860 &pane,
3861 [
3862 "D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6^", "N0", "N1", "N2", "N3", "N4",
3863 "N5", "N6*",
3864 ],
3865 cx,
3866 );
3867 }
3868
3869 #[gpui::test]
3870 async fn test_allow_pinning_dirty_item_at_max_tabs(cx: &mut TestAppContext) {
3871 init_test(cx);
3872 let fs = FakeFs::new(cx.executor());
3873
3874 let project = Project::test(fs, None, cx).await;
3875 let (workspace, cx) =
3876 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3877 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3878
3879 set_max_tabs(cx, Some(1));
3880 let item_a = add_labeled_item(&pane, "A", true, cx);
3881
3882 pane.update_in(cx, |pane, window, cx| {
3883 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
3884 pane.pin_tab_at(ix, window, cx);
3885 });
3886 assert_item_labels(&pane, ["A*^!"], cx);
3887 }
3888
3889 #[gpui::test]
3890 async fn test_allow_pinning_non_dirty_item_at_max_tabs(cx: &mut TestAppContext) {
3891 init_test(cx);
3892 let fs = FakeFs::new(cx.executor());
3893
3894 let project = Project::test(fs, None, cx).await;
3895 let (workspace, cx) =
3896 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3897 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3898
3899 set_max_tabs(cx, Some(1));
3900 let item_a = add_labeled_item(&pane, "A", false, cx);
3901
3902 pane.update_in(cx, |pane, window, cx| {
3903 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
3904 pane.pin_tab_at(ix, window, cx);
3905 });
3906 assert_item_labels(&pane, ["A*!"], cx);
3907 }
3908
3909 #[gpui::test]
3910 async fn test_pin_tabs_incrementally_at_max_capacity(cx: &mut TestAppContext) {
3911 init_test(cx);
3912 let fs = FakeFs::new(cx.executor());
3913
3914 let project = Project::test(fs, None, cx).await;
3915 let (workspace, cx) =
3916 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3917 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3918
3919 set_max_tabs(cx, Some(3));
3920
3921 let item_a = add_labeled_item(&pane, "A", false, cx);
3922 assert_item_labels(&pane, ["A*"], cx);
3923
3924 pane.update_in(cx, |pane, window, cx| {
3925 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
3926 pane.pin_tab_at(ix, window, cx);
3927 });
3928 assert_item_labels(&pane, ["A*!"], cx);
3929
3930 let item_b = add_labeled_item(&pane, "B", false, cx);
3931 assert_item_labels(&pane, ["A!", "B*"], cx);
3932
3933 pane.update_in(cx, |pane, window, cx| {
3934 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
3935 pane.pin_tab_at(ix, window, cx);
3936 });
3937 assert_item_labels(&pane, ["A!", "B*!"], cx);
3938
3939 let item_c = add_labeled_item(&pane, "C", false, cx);
3940 assert_item_labels(&pane, ["A!", "B!", "C*"], cx);
3941
3942 pane.update_in(cx, |pane, window, cx| {
3943 let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
3944 pane.pin_tab_at(ix, window, cx);
3945 });
3946 assert_item_labels(&pane, ["A!", "B!", "C*!"], cx);
3947 }
3948
3949 #[gpui::test]
3950 async fn test_pin_tabs_left_to_right_after_opening_at_max_capacity(cx: &mut TestAppContext) {
3951 init_test(cx);
3952 let fs = FakeFs::new(cx.executor());
3953
3954 let project = Project::test(fs, None, cx).await;
3955 let (workspace, cx) =
3956 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3957 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3958
3959 set_max_tabs(cx, Some(3));
3960
3961 let item_a = add_labeled_item(&pane, "A", false, cx);
3962 assert_item_labels(&pane, ["A*"], cx);
3963
3964 let item_b = add_labeled_item(&pane, "B", false, cx);
3965 assert_item_labels(&pane, ["A", "B*"], cx);
3966
3967 let item_c = add_labeled_item(&pane, "C", false, cx);
3968 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3969
3970 pane.update_in(cx, |pane, window, cx| {
3971 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
3972 pane.pin_tab_at(ix, window, cx);
3973 });
3974 assert_item_labels(&pane, ["A!", "B", "C*"], cx);
3975
3976 pane.update_in(cx, |pane, window, cx| {
3977 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
3978 pane.pin_tab_at(ix, window, cx);
3979 });
3980 assert_item_labels(&pane, ["A!", "B!", "C*"], cx);
3981
3982 pane.update_in(cx, |pane, window, cx| {
3983 let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
3984 pane.pin_tab_at(ix, window, cx);
3985 });
3986 assert_item_labels(&pane, ["A!", "B!", "C*!"], cx);
3987 }
3988
3989 #[gpui::test]
3990 async fn test_pin_tabs_right_to_left_after_opening_at_max_capacity(cx: &mut TestAppContext) {
3991 init_test(cx);
3992 let fs = FakeFs::new(cx.executor());
3993
3994 let project = Project::test(fs, None, cx).await;
3995 let (workspace, cx) =
3996 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3997 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3998
3999 set_max_tabs(cx, Some(3));
4000
4001 let item_a = add_labeled_item(&pane, "A", false, cx);
4002 assert_item_labels(&pane, ["A*"], cx);
4003
4004 let item_b = add_labeled_item(&pane, "B", false, cx);
4005 assert_item_labels(&pane, ["A", "B*"], cx);
4006
4007 let item_c = add_labeled_item(&pane, "C", false, cx);
4008 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4009
4010 pane.update_in(cx, |pane, window, cx| {
4011 let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4012 pane.pin_tab_at(ix, window, cx);
4013 });
4014 assert_item_labels(&pane, ["C*!", "A", "B"], cx);
4015
4016 pane.update_in(cx, |pane, window, cx| {
4017 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4018 pane.pin_tab_at(ix, window, cx);
4019 });
4020 assert_item_labels(&pane, ["C*!", "B!", "A"], cx);
4021
4022 pane.update_in(cx, |pane, window, cx| {
4023 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4024 pane.pin_tab_at(ix, window, cx);
4025 });
4026 assert_item_labels(&pane, ["C*!", "B!", "A!"], cx);
4027 }
4028
4029 #[gpui::test]
4030 async fn test_pinned_tabs_never_closed_at_max_tabs(cx: &mut TestAppContext) {
4031 init_test(cx);
4032 let fs = FakeFs::new(cx.executor());
4033
4034 let project = Project::test(fs, None, cx).await;
4035 let (workspace, cx) =
4036 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4037 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4038
4039 let item_a = add_labeled_item(&pane, "A", false, cx);
4040 pane.update_in(cx, |pane, window, cx| {
4041 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4042 pane.pin_tab_at(ix, window, cx);
4043 });
4044
4045 let item_b = add_labeled_item(&pane, "B", false, cx);
4046 pane.update_in(cx, |pane, window, cx| {
4047 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4048 pane.pin_tab_at(ix, window, cx);
4049 });
4050
4051 add_labeled_item(&pane, "C", false, cx);
4052 add_labeled_item(&pane, "D", false, cx);
4053 add_labeled_item(&pane, "E", false, cx);
4054 assert_item_labels(&pane, ["A!", "B!", "C", "D", "E*"], cx);
4055
4056 set_max_tabs(cx, Some(3));
4057 add_labeled_item(&pane, "F", false, cx);
4058 assert_item_labels(&pane, ["A!", "B!", "F*"], cx);
4059
4060 add_labeled_item(&pane, "G", false, cx);
4061 assert_item_labels(&pane, ["A!", "B!", "G*"], cx);
4062
4063 add_labeled_item(&pane, "H", false, cx);
4064 assert_item_labels(&pane, ["A!", "B!", "H*"], cx);
4065 }
4066
4067 #[gpui::test]
4068 async fn test_always_allows_one_unpinned_item_over_max_tabs_regardless_of_pinned_count(
4069 cx: &mut TestAppContext,
4070 ) {
4071 init_test(cx);
4072 let fs = FakeFs::new(cx.executor());
4073
4074 let project = Project::test(fs, None, cx).await;
4075 let (workspace, cx) =
4076 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4077 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4078
4079 set_max_tabs(cx, Some(3));
4080
4081 let item_a = add_labeled_item(&pane, "A", false, cx);
4082 pane.update_in(cx, |pane, window, cx| {
4083 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4084 pane.pin_tab_at(ix, window, cx);
4085 });
4086
4087 let item_b = add_labeled_item(&pane, "B", false, cx);
4088 pane.update_in(cx, |pane, window, cx| {
4089 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4090 pane.pin_tab_at(ix, window, cx);
4091 });
4092
4093 let item_c = add_labeled_item(&pane, "C", false, cx);
4094 pane.update_in(cx, |pane, window, cx| {
4095 let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4096 pane.pin_tab_at(ix, window, cx);
4097 });
4098
4099 assert_item_labels(&pane, ["A!", "B!", "C*!"], cx);
4100
4101 let item_d = add_labeled_item(&pane, "D", false, cx);
4102 assert_item_labels(&pane, ["A!", "B!", "C!", "D*"], cx);
4103
4104 pane.update_in(cx, |pane, window, cx| {
4105 let ix = pane.index_for_item_id(item_d.item_id()).unwrap();
4106 pane.pin_tab_at(ix, window, cx);
4107 });
4108 assert_item_labels(&pane, ["A!", "B!", "C!", "D*!"], cx);
4109
4110 add_labeled_item(&pane, "E", false, cx);
4111 assert_item_labels(&pane, ["A!", "B!", "C!", "D!", "E*"], cx);
4112
4113 add_labeled_item(&pane, "F", false, cx);
4114 assert_item_labels(&pane, ["A!", "B!", "C!", "D!", "F*"], cx);
4115 }
4116
4117 #[gpui::test]
4118 async fn test_can_open_one_item_when_all_tabs_are_dirty_at_max(cx: &mut TestAppContext) {
4119 init_test(cx);
4120 let fs = FakeFs::new(cx.executor());
4121
4122 let project = Project::test(fs, None, cx).await;
4123 let (workspace, cx) =
4124 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4125 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4126
4127 set_max_tabs(cx, Some(3));
4128
4129 add_labeled_item(&pane, "A", true, cx);
4130 assert_item_labels(&pane, ["A*^"], cx);
4131
4132 add_labeled_item(&pane, "B", true, cx);
4133 assert_item_labels(&pane, ["A^", "B*^"], cx);
4134
4135 add_labeled_item(&pane, "C", true, cx);
4136 assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
4137
4138 add_labeled_item(&pane, "D", false, cx);
4139 assert_item_labels(&pane, ["A^", "B^", "C^", "D*"], cx);
4140
4141 add_labeled_item(&pane, "E", false, cx);
4142 assert_item_labels(&pane, ["A^", "B^", "C^", "E*"], cx);
4143
4144 add_labeled_item(&pane, "F", false, cx);
4145 assert_item_labels(&pane, ["A^", "B^", "C^", "F*"], cx);
4146
4147 add_labeled_item(&pane, "G", true, cx);
4148 assert_item_labels(&pane, ["A^", "B^", "C^", "G*^"], cx);
4149 }
4150
4151 #[gpui::test]
4152 async fn test_toggle_pin_tab(cx: &mut TestAppContext) {
4153 init_test(cx);
4154 let fs = FakeFs::new(cx.executor());
4155
4156 let project = Project::test(fs, None, cx).await;
4157 let (workspace, cx) =
4158 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4159 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4160
4161 set_labeled_items(&pane, ["A", "B*", "C"], cx);
4162 assert_item_labels(&pane, ["A", "B*", "C"], cx);
4163
4164 pane.update_in(cx, |pane, window, cx| {
4165 pane.toggle_pin_tab(&TogglePinTab, window, cx);
4166 });
4167 assert_item_labels(&pane, ["B*!", "A", "C"], cx);
4168
4169 pane.update_in(cx, |pane, window, cx| {
4170 pane.toggle_pin_tab(&TogglePinTab, window, cx);
4171 });
4172 assert_item_labels(&pane, ["B*", "A", "C"], cx);
4173 }
4174
4175 #[gpui::test]
4176 async fn test_pinning_active_tab_without_position_change_maintains_focus(
4177 cx: &mut TestAppContext,
4178 ) {
4179 init_test(cx);
4180 let fs = FakeFs::new(cx.executor());
4181
4182 let project = Project::test(fs, None, cx).await;
4183 let (workspace, cx) =
4184 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4185 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4186
4187 // Add A
4188 let item_a = add_labeled_item(&pane, "A", false, cx);
4189 assert_item_labels(&pane, ["A*"], cx);
4190
4191 // Add B
4192 add_labeled_item(&pane, "B", false, cx);
4193 assert_item_labels(&pane, ["A", "B*"], cx);
4194
4195 // Activate A again
4196 pane.update_in(cx, |pane, window, cx| {
4197 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4198 pane.activate_item(ix, true, true, window, cx);
4199 });
4200 assert_item_labels(&pane, ["A*", "B"], cx);
4201
4202 // Pin A - remains active
4203 pane.update_in(cx, |pane, window, cx| {
4204 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4205 pane.pin_tab_at(ix, window, cx);
4206 });
4207 assert_item_labels(&pane, ["A*!", "B"], cx);
4208
4209 // Unpin A - remain active
4210 pane.update_in(cx, |pane, window, cx| {
4211 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4212 pane.unpin_tab_at(ix, window, cx);
4213 });
4214 assert_item_labels(&pane, ["A*", "B"], cx);
4215 }
4216
4217 #[gpui::test]
4218 async fn test_pinning_active_tab_with_position_change_maintains_focus(cx: &mut TestAppContext) {
4219 init_test(cx);
4220 let fs = FakeFs::new(cx.executor());
4221
4222 let project = Project::test(fs, None, cx).await;
4223 let (workspace, cx) =
4224 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4225 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4226
4227 // Add A, B, C
4228 add_labeled_item(&pane, "A", false, cx);
4229 add_labeled_item(&pane, "B", false, cx);
4230 let item_c = add_labeled_item(&pane, "C", false, cx);
4231 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4232
4233 // Pin C - moves to pinned area, remains active
4234 pane.update_in(cx, |pane, window, cx| {
4235 let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4236 pane.pin_tab_at(ix, window, cx);
4237 });
4238 assert_item_labels(&pane, ["C*!", "A", "B"], cx);
4239
4240 // Unpin C - moves after pinned area, remains active
4241 pane.update_in(cx, |pane, window, cx| {
4242 let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4243 pane.unpin_tab_at(ix, window, cx);
4244 });
4245 assert_item_labels(&pane, ["C*", "A", "B"], cx);
4246 }
4247
4248 #[gpui::test]
4249 async fn test_pinning_inactive_tab_without_position_change_preserves_existing_focus(
4250 cx: &mut TestAppContext,
4251 ) {
4252 init_test(cx);
4253 let fs = FakeFs::new(cx.executor());
4254
4255 let project = Project::test(fs, None, cx).await;
4256 let (workspace, cx) =
4257 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4258 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4259
4260 // Add A, B
4261 let item_a = add_labeled_item(&pane, "A", false, cx);
4262 add_labeled_item(&pane, "B", false, cx);
4263 assert_item_labels(&pane, ["A", "B*"], cx);
4264
4265 // Pin A - already in pinned area, B remains active
4266 pane.update_in(cx, |pane, window, cx| {
4267 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4268 pane.pin_tab_at(ix, window, cx);
4269 });
4270 assert_item_labels(&pane, ["A!", "B*"], cx);
4271
4272 // Unpin A - stays in place, B remains active
4273 pane.update_in(cx, |pane, window, cx| {
4274 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4275 pane.unpin_tab_at(ix, window, cx);
4276 });
4277 assert_item_labels(&pane, ["A", "B*"], cx);
4278 }
4279
4280 #[gpui::test]
4281 async fn test_pinning_inactive_tab_with_position_change_preserves_existing_focus(
4282 cx: &mut TestAppContext,
4283 ) {
4284 init_test(cx);
4285 let fs = FakeFs::new(cx.executor());
4286
4287 let project = Project::test(fs, None, cx).await;
4288 let (workspace, cx) =
4289 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4290 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4291
4292 // Add A, B, C
4293 add_labeled_item(&pane, "A", false, cx);
4294 let item_b = add_labeled_item(&pane, "B", false, cx);
4295 let item_c = add_labeled_item(&pane, "C", false, cx);
4296 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4297
4298 // Activate B
4299 pane.update_in(cx, |pane, window, cx| {
4300 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4301 pane.activate_item(ix, true, true, window, cx);
4302 });
4303 assert_item_labels(&pane, ["A", "B*", "C"], cx);
4304
4305 // Pin C - moves to pinned area, B remains active
4306 pane.update_in(cx, |pane, window, cx| {
4307 let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4308 pane.pin_tab_at(ix, window, cx);
4309 });
4310 assert_item_labels(&pane, ["C!", "A", "B*"], cx);
4311
4312 // Unpin C - moves after pinned area, B remains active
4313 pane.update_in(cx, |pane, window, cx| {
4314 let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4315 pane.unpin_tab_at(ix, window, cx);
4316 });
4317 assert_item_labels(&pane, ["C", "A", "B*"], cx);
4318 }
4319
4320 #[gpui::test]
4321 async fn test_drag_unpinned_tab_to_split_creates_pane_with_unpinned_tab(
4322 cx: &mut TestAppContext,
4323 ) {
4324 init_test(cx);
4325 let fs = FakeFs::new(cx.executor());
4326
4327 let project = Project::test(fs, None, cx).await;
4328 let (workspace, cx) =
4329 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4330 let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4331
4332 // Add A, B. Pin B. Activate A
4333 let item_a = add_labeled_item(&pane_a, "A", false, cx);
4334 let item_b = add_labeled_item(&pane_a, "B", false, cx);
4335
4336 pane_a.update_in(cx, |pane, window, cx| {
4337 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4338 pane.pin_tab_at(ix, window, cx);
4339
4340 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4341 pane.activate_item(ix, true, true, window, cx);
4342 });
4343
4344 // Drag A to create new split
4345 pane_a.update_in(cx, |pane, window, cx| {
4346 pane.drag_split_direction = Some(SplitDirection::Right);
4347
4348 let dragged_tab = DraggedTab {
4349 pane: pane_a.clone(),
4350 item: item_a.boxed_clone(),
4351 ix: 0,
4352 detail: 0,
4353 is_active: true,
4354 };
4355 pane.handle_tab_drop(&dragged_tab, 0, window, cx);
4356 });
4357
4358 // A should be moved to new pane. B should remain pinned, A should not be pinned
4359 let (pane_a, pane_b) = workspace.read_with(cx, |workspace, _| {
4360 let panes = workspace.panes();
4361 (panes[0].clone(), panes[1].clone())
4362 });
4363 assert_item_labels(&pane_a, ["B*!"], cx);
4364 assert_item_labels(&pane_b, ["A*"], cx);
4365 }
4366
4367 #[gpui::test]
4368 async fn test_drag_pinned_tab_to_split_creates_pane_with_pinned_tab(cx: &mut TestAppContext) {
4369 init_test(cx);
4370 let fs = FakeFs::new(cx.executor());
4371
4372 let project = Project::test(fs, None, cx).await;
4373 let (workspace, cx) =
4374 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4375 let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4376
4377 // Add A, B. Pin both. Activate A
4378 let item_a = add_labeled_item(&pane_a, "A", false, cx);
4379 let item_b = add_labeled_item(&pane_a, "B", false, cx);
4380
4381 pane_a.update_in(cx, |pane, window, cx| {
4382 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4383 pane.pin_tab_at(ix, window, cx);
4384
4385 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4386 pane.pin_tab_at(ix, window, cx);
4387
4388 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4389 pane.activate_item(ix, true, true, window, cx);
4390 });
4391 assert_item_labels(&pane_a, ["A*!", "B!"], cx);
4392
4393 // Drag A to create new split
4394 pane_a.update_in(cx, |pane, window, cx| {
4395 pane.drag_split_direction = Some(SplitDirection::Right);
4396
4397 let dragged_tab = DraggedTab {
4398 pane: pane_a.clone(),
4399 item: item_a.boxed_clone(),
4400 ix: 0,
4401 detail: 0,
4402 is_active: true,
4403 };
4404 pane.handle_tab_drop(&dragged_tab, 0, window, cx);
4405 });
4406
4407 // A should be moved to new pane. Both A and B should still be pinned
4408 let (pane_a, pane_b) = workspace.read_with(cx, |workspace, _| {
4409 let panes = workspace.panes();
4410 (panes[0].clone(), panes[1].clone())
4411 });
4412 assert_item_labels(&pane_a, ["B*!"], cx);
4413 assert_item_labels(&pane_b, ["A*!"], cx);
4414 }
4415
4416 #[gpui::test]
4417 async fn test_drag_pinned_tab_into_existing_panes_pinned_region(cx: &mut TestAppContext) {
4418 init_test(cx);
4419 let fs = FakeFs::new(cx.executor());
4420
4421 let project = Project::test(fs, None, cx).await;
4422 let (workspace, cx) =
4423 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4424 let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4425
4426 // Add A to pane A and pin
4427 let item_a = add_labeled_item(&pane_a, "A", false, cx);
4428 pane_a.update_in(cx, |pane, window, cx| {
4429 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4430 pane.pin_tab_at(ix, window, cx);
4431 });
4432 assert_item_labels(&pane_a, ["A*!"], cx);
4433
4434 // Add B to pane B and pin
4435 let pane_b = workspace.update_in(cx, |workspace, window, cx| {
4436 workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx)
4437 });
4438 let item_b = add_labeled_item(&pane_b, "B", false, cx);
4439 pane_b.update_in(cx, |pane, window, cx| {
4440 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4441 pane.pin_tab_at(ix, window, cx);
4442 });
4443 assert_item_labels(&pane_b, ["B*!"], cx);
4444
4445 // Move A from pane A to pane B's pinned region
4446 pane_b.update_in(cx, |pane, window, cx| {
4447 let dragged_tab = DraggedTab {
4448 pane: pane_a.clone(),
4449 item: item_a.boxed_clone(),
4450 ix: 0,
4451 detail: 0,
4452 is_active: true,
4453 };
4454 pane.handle_tab_drop(&dragged_tab, 0, window, cx);
4455 });
4456
4457 // A should stay pinned
4458 assert_item_labels(&pane_a, [], cx);
4459 assert_item_labels(&pane_b, ["A*!", "B!"], cx);
4460 }
4461
4462 #[gpui::test]
4463 async fn test_drag_pinned_tab_into_existing_panes_unpinned_region(cx: &mut TestAppContext) {
4464 init_test(cx);
4465 let fs = FakeFs::new(cx.executor());
4466
4467 let project = Project::test(fs, None, cx).await;
4468 let (workspace, cx) =
4469 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4470 let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4471
4472 // Add A to pane A and pin
4473 let item_a = add_labeled_item(&pane_a, "A", false, cx);
4474 pane_a.update_in(cx, |pane, window, cx| {
4475 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4476 pane.pin_tab_at(ix, window, cx);
4477 });
4478 assert_item_labels(&pane_a, ["A*!"], cx);
4479
4480 // Create pane B with pinned item B
4481 let pane_b = workspace.update_in(cx, |workspace, window, cx| {
4482 workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx)
4483 });
4484 let item_b = add_labeled_item(&pane_b, "B", false, cx);
4485 assert_item_labels(&pane_b, ["B*"], cx);
4486
4487 pane_b.update_in(cx, |pane, window, cx| {
4488 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4489 pane.pin_tab_at(ix, window, cx);
4490 });
4491 assert_item_labels(&pane_b, ["B*!"], cx);
4492
4493 // Move A from pane A to pane B's unpinned region
4494 pane_b.update_in(cx, |pane, window, cx| {
4495 let dragged_tab = DraggedTab {
4496 pane: pane_a.clone(),
4497 item: item_a.boxed_clone(),
4498 ix: 0,
4499 detail: 0,
4500 is_active: true,
4501 };
4502 pane.handle_tab_drop(&dragged_tab, 1, window, cx);
4503 });
4504
4505 // A should become pinned
4506 assert_item_labels(&pane_a, [], cx);
4507 assert_item_labels(&pane_b, ["B!", "A*"], cx);
4508 }
4509
4510 #[gpui::test]
4511 async fn test_drag_pinned_tab_into_existing_panes_first_position_with_no_pinned_tabs(
4512 cx: &mut TestAppContext,
4513 ) {
4514 init_test(cx);
4515 let fs = FakeFs::new(cx.executor());
4516
4517 let project = Project::test(fs, None, cx).await;
4518 let (workspace, cx) =
4519 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4520 let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4521
4522 // Add A to pane A and pin
4523 let item_a = add_labeled_item(&pane_a, "A", false, cx);
4524 pane_a.update_in(cx, |pane, window, cx| {
4525 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4526 pane.pin_tab_at(ix, window, cx);
4527 });
4528 assert_item_labels(&pane_a, ["A*!"], cx);
4529
4530 // Add B to pane B
4531 let pane_b = workspace.update_in(cx, |workspace, window, cx| {
4532 workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx)
4533 });
4534 add_labeled_item(&pane_b, "B", false, cx);
4535 assert_item_labels(&pane_b, ["B*"], cx);
4536
4537 // Move A from pane A to position 0 in pane B, indicating it should stay pinned
4538 pane_b.update_in(cx, |pane, window, cx| {
4539 let dragged_tab = DraggedTab {
4540 pane: pane_a.clone(),
4541 item: item_a.boxed_clone(),
4542 ix: 0,
4543 detail: 0,
4544 is_active: true,
4545 };
4546 pane.handle_tab_drop(&dragged_tab, 0, window, cx);
4547 });
4548
4549 // A should stay pinned
4550 assert_item_labels(&pane_a, [], cx);
4551 assert_item_labels(&pane_b, ["A*!", "B"], cx);
4552 }
4553
4554 #[gpui::test]
4555 async fn test_drag_pinned_tab_into_existing_pane_at_max_capacity_closes_unpinned_tabs(
4556 cx: &mut TestAppContext,
4557 ) {
4558 init_test(cx);
4559 let fs = FakeFs::new(cx.executor());
4560
4561 let project = Project::test(fs, None, cx).await;
4562 let (workspace, cx) =
4563 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4564 let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4565 set_max_tabs(cx, Some(2));
4566
4567 // Add A, B to pane A. Pin both
4568 let item_a = add_labeled_item(&pane_a, "A", false, cx);
4569 let item_b = add_labeled_item(&pane_a, "B", false, cx);
4570 pane_a.update_in(cx, |pane, window, cx| {
4571 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4572 pane.pin_tab_at(ix, window, cx);
4573
4574 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4575 pane.pin_tab_at(ix, window, cx);
4576 });
4577 assert_item_labels(&pane_a, ["A!", "B*!"], cx);
4578
4579 // Add C, D to pane B. Pin both
4580 let pane_b = workspace.update_in(cx, |workspace, window, cx| {
4581 workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx)
4582 });
4583 let item_c = add_labeled_item(&pane_b, "C", false, cx);
4584 let item_d = add_labeled_item(&pane_b, "D", false, cx);
4585 pane_b.update_in(cx, |pane, window, cx| {
4586 let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4587 pane.pin_tab_at(ix, window, cx);
4588
4589 let ix = pane.index_for_item_id(item_d.item_id()).unwrap();
4590 pane.pin_tab_at(ix, window, cx);
4591 });
4592 assert_item_labels(&pane_b, ["C!", "D*!"], cx);
4593
4594 // Add a third unpinned item to pane B (exceeds max tabs), but is allowed,
4595 // as we allow 1 tab over max if the others are pinned or dirty
4596 add_labeled_item(&pane_b, "E", false, cx);
4597 assert_item_labels(&pane_b, ["C!", "D!", "E*"], cx);
4598
4599 // Drag pinned A from pane A to position 0 in pane B
4600 pane_b.update_in(cx, |pane, window, cx| {
4601 let dragged_tab = DraggedTab {
4602 pane: pane_a.clone(),
4603 item: item_a.boxed_clone(),
4604 ix: 0,
4605 detail: 0,
4606 is_active: true,
4607 };
4608 pane.handle_tab_drop(&dragged_tab, 0, window, cx);
4609 });
4610
4611 // E (unpinned) should be closed, leaving 3 pinned items
4612 assert_item_labels(&pane_a, ["B*!"], cx);
4613 assert_item_labels(&pane_b, ["A*!", "C!", "D!"], cx);
4614 }
4615
4616 #[gpui::test]
4617 async fn test_drag_last_pinned_tab_to_same_position_stays_pinned(cx: &mut TestAppContext) {
4618 init_test(cx);
4619 let fs = FakeFs::new(cx.executor());
4620
4621 let project = Project::test(fs, None, cx).await;
4622 let (workspace, cx) =
4623 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4624 let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4625
4626 // Add A to pane A and pin it
4627 let item_a = add_labeled_item(&pane_a, "A", false, cx);
4628 pane_a.update_in(cx, |pane, window, cx| {
4629 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4630 pane.pin_tab_at(ix, window, cx);
4631 });
4632 assert_item_labels(&pane_a, ["A*!"], cx);
4633
4634 // Drag pinned A to position 1 (directly to the right) in the same pane
4635 pane_a.update_in(cx, |pane, window, cx| {
4636 let dragged_tab = DraggedTab {
4637 pane: pane_a.clone(),
4638 item: item_a.boxed_clone(),
4639 ix: 0,
4640 detail: 0,
4641 is_active: true,
4642 };
4643 pane.handle_tab_drop(&dragged_tab, 1, window, cx);
4644 });
4645
4646 // A should still be pinned and active
4647 assert_item_labels(&pane_a, ["A*!"], cx);
4648 }
4649
4650 #[gpui::test]
4651 async fn test_drag_pinned_tab_beyond_last_pinned_tab_in_same_pane_stays_pinned(
4652 cx: &mut TestAppContext,
4653 ) {
4654 init_test(cx);
4655 let fs = FakeFs::new(cx.executor());
4656
4657 let project = Project::test(fs, None, cx).await;
4658 let (workspace, cx) =
4659 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4660 let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4661
4662 // Add A, B to pane A and pin both
4663 let item_a = add_labeled_item(&pane_a, "A", false, cx);
4664 let item_b = add_labeled_item(&pane_a, "B", false, cx);
4665 pane_a.update_in(cx, |pane, window, cx| {
4666 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4667 pane.pin_tab_at(ix, window, cx);
4668
4669 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4670 pane.pin_tab_at(ix, window, cx);
4671 });
4672 assert_item_labels(&pane_a, ["A!", "B*!"], cx);
4673
4674 // Drag pinned A right of B in the same pane
4675 pane_a.update_in(cx, |pane, window, cx| {
4676 let dragged_tab = DraggedTab {
4677 pane: pane_a.clone(),
4678 item: item_a.boxed_clone(),
4679 ix: 0,
4680 detail: 0,
4681 is_active: true,
4682 };
4683 pane.handle_tab_drop(&dragged_tab, 2, window, cx);
4684 });
4685
4686 // A stays pinned
4687 assert_item_labels(&pane_a, ["B!", "A*!"], cx);
4688 }
4689
4690 #[gpui::test]
4691 async fn test_drag_pinned_tab_beyond_unpinned_tab_in_same_pane_becomes_unpinned(
4692 cx: &mut TestAppContext,
4693 ) {
4694 init_test(cx);
4695 let fs = FakeFs::new(cx.executor());
4696
4697 let project = Project::test(fs, None, cx).await;
4698 let (workspace, cx) =
4699 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4700 let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4701
4702 // Add A, B to pane A and pin A
4703 let item_a = add_labeled_item(&pane_a, "A", false, cx);
4704 add_labeled_item(&pane_a, "B", false, cx);
4705 pane_a.update_in(cx, |pane, window, cx| {
4706 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4707 pane.pin_tab_at(ix, window, cx);
4708 });
4709 assert_item_labels(&pane_a, ["A!", "B*"], cx);
4710
4711 // Drag pinned A right of B in the same pane
4712 pane_a.update_in(cx, |pane, window, cx| {
4713 let dragged_tab = DraggedTab {
4714 pane: pane_a.clone(),
4715 item: item_a.boxed_clone(),
4716 ix: 0,
4717 detail: 0,
4718 is_active: true,
4719 };
4720 pane.handle_tab_drop(&dragged_tab, 2, window, cx);
4721 });
4722
4723 // A becomes unpinned
4724 assert_item_labels(&pane_a, ["B", "A*"], cx);
4725 }
4726
4727 #[gpui::test]
4728 async fn test_drag_unpinned_tab_in_front_of_pinned_tab_in_same_pane_becomes_pinned(
4729 cx: &mut TestAppContext,
4730 ) {
4731 init_test(cx);
4732 let fs = FakeFs::new(cx.executor());
4733
4734 let project = Project::test(fs, None, cx).await;
4735 let (workspace, cx) =
4736 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4737 let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4738
4739 // Add A, B to pane A and pin A
4740 let item_a = add_labeled_item(&pane_a, "A", false, cx);
4741 let item_b = add_labeled_item(&pane_a, "B", false, cx);
4742 pane_a.update_in(cx, |pane, window, cx| {
4743 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4744 pane.pin_tab_at(ix, window, cx);
4745 });
4746 assert_item_labels(&pane_a, ["A!", "B*"], cx);
4747
4748 // Drag pinned B left of A in the same pane
4749 pane_a.update_in(cx, |pane, window, cx| {
4750 let dragged_tab = DraggedTab {
4751 pane: pane_a.clone(),
4752 item: item_b.boxed_clone(),
4753 ix: 1,
4754 detail: 0,
4755 is_active: true,
4756 };
4757 pane.handle_tab_drop(&dragged_tab, 0, window, cx);
4758 });
4759
4760 // A becomes unpinned
4761 assert_item_labels(&pane_a, ["B*!", "A!"], cx);
4762 }
4763
4764 #[gpui::test]
4765 async fn test_drag_unpinned_tab_to_the_pinned_region_stays_pinned(cx: &mut TestAppContext) {
4766 init_test(cx);
4767 let fs = FakeFs::new(cx.executor());
4768
4769 let project = Project::test(fs, None, cx).await;
4770 let (workspace, cx) =
4771 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4772 let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4773
4774 // Add A, B, C to pane A and pin A
4775 let item_a = add_labeled_item(&pane_a, "A", false, cx);
4776 add_labeled_item(&pane_a, "B", false, cx);
4777 let item_c = add_labeled_item(&pane_a, "C", false, cx);
4778 pane_a.update_in(cx, |pane, window, cx| {
4779 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4780 pane.pin_tab_at(ix, window, cx);
4781 });
4782 assert_item_labels(&pane_a, ["A!", "B", "C*"], cx);
4783
4784 // Drag pinned C left of B in the same pane
4785 pane_a.update_in(cx, |pane, window, cx| {
4786 let dragged_tab = DraggedTab {
4787 pane: pane_a.clone(),
4788 item: item_c.boxed_clone(),
4789 ix: 2,
4790 detail: 0,
4791 is_active: true,
4792 };
4793 pane.handle_tab_drop(&dragged_tab, 1, window, cx);
4794 });
4795
4796 // A stays pinned, B and C remain unpinned
4797 assert_item_labels(&pane_a, ["A!", "C*", "B"], cx);
4798 }
4799
4800 #[gpui::test]
4801 async fn test_drag_unpinned_tab_into_existing_panes_pinned_region(cx: &mut TestAppContext) {
4802 init_test(cx);
4803 let fs = FakeFs::new(cx.executor());
4804
4805 let project = Project::test(fs, None, cx).await;
4806 let (workspace, cx) =
4807 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4808 let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4809
4810 // Add unpinned item A to pane A
4811 let item_a = add_labeled_item(&pane_a, "A", false, cx);
4812 assert_item_labels(&pane_a, ["A*"], cx);
4813
4814 // Create pane B with pinned item B
4815 let pane_b = workspace.update_in(cx, |workspace, window, cx| {
4816 workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx)
4817 });
4818 let item_b = add_labeled_item(&pane_b, "B", false, cx);
4819 pane_b.update_in(cx, |pane, window, cx| {
4820 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4821 pane.pin_tab_at(ix, window, cx);
4822 });
4823 assert_item_labels(&pane_b, ["B*!"], cx);
4824
4825 // Move A from pane A to pane B's pinned region
4826 pane_b.update_in(cx, |pane, window, cx| {
4827 let dragged_tab = DraggedTab {
4828 pane: pane_a.clone(),
4829 item: item_a.boxed_clone(),
4830 ix: 0,
4831 detail: 0,
4832 is_active: true,
4833 };
4834 pane.handle_tab_drop(&dragged_tab, 0, window, cx);
4835 });
4836
4837 // A should become pinned since it was dropped in the pinned region
4838 assert_item_labels(&pane_a, [], cx);
4839 assert_item_labels(&pane_b, ["A*!", "B!"], cx);
4840 }
4841
4842 #[gpui::test]
4843 async fn test_drag_unpinned_tab_into_existing_panes_unpinned_region(cx: &mut TestAppContext) {
4844 init_test(cx);
4845 let fs = FakeFs::new(cx.executor());
4846
4847 let project = Project::test(fs, None, cx).await;
4848 let (workspace, cx) =
4849 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4850 let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4851
4852 // Add unpinned item A to pane A
4853 let item_a = add_labeled_item(&pane_a, "A", false, cx);
4854 assert_item_labels(&pane_a, ["A*"], cx);
4855
4856 // Create pane B with one pinned item B
4857 let pane_b = workspace.update_in(cx, |workspace, window, cx| {
4858 workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx)
4859 });
4860 let item_b = add_labeled_item(&pane_b, "B", false, cx);
4861 pane_b.update_in(cx, |pane, window, cx| {
4862 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4863 pane.pin_tab_at(ix, window, cx);
4864 });
4865 assert_item_labels(&pane_b, ["B*!"], cx);
4866
4867 // Move A from pane A to pane B's unpinned region
4868 pane_b.update_in(cx, |pane, window, cx| {
4869 let dragged_tab = DraggedTab {
4870 pane: pane_a.clone(),
4871 item: item_a.boxed_clone(),
4872 ix: 0,
4873 detail: 0,
4874 is_active: true,
4875 };
4876 pane.handle_tab_drop(&dragged_tab, 1, window, cx);
4877 });
4878
4879 // A should remain unpinned since it was dropped outside the pinned region
4880 assert_item_labels(&pane_a, [], cx);
4881 assert_item_labels(&pane_b, ["B!", "A*"], cx);
4882 }
4883
4884 #[gpui::test]
4885 async fn test_drag_pinned_tab_throughout_entire_range_of_pinned_tabs_both_directions(
4886 cx: &mut TestAppContext,
4887 ) {
4888 init_test(cx);
4889 let fs = FakeFs::new(cx.executor());
4890
4891 let project = Project::test(fs, None, cx).await;
4892 let (workspace, cx) =
4893 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4894 let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4895
4896 // Add A, B, C and pin all
4897 let item_a = add_labeled_item(&pane_a, "A", false, cx);
4898 let item_b = add_labeled_item(&pane_a, "B", false, cx);
4899 let item_c = add_labeled_item(&pane_a, "C", false, cx);
4900 assert_item_labels(&pane_a, ["A", "B", "C*"], cx);
4901
4902 pane_a.update_in(cx, |pane, window, cx| {
4903 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4904 pane.pin_tab_at(ix, window, cx);
4905
4906 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4907 pane.pin_tab_at(ix, window, cx);
4908
4909 let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4910 pane.pin_tab_at(ix, window, cx);
4911 });
4912 assert_item_labels(&pane_a, ["A!", "B!", "C*!"], cx);
4913
4914 // Move A to right of B
4915 pane_a.update_in(cx, |pane, window, cx| {
4916 let dragged_tab = DraggedTab {
4917 pane: pane_a.clone(),
4918 item: item_a.boxed_clone(),
4919 ix: 0,
4920 detail: 0,
4921 is_active: true,
4922 };
4923 pane.handle_tab_drop(&dragged_tab, 1, window, cx);
4924 });
4925
4926 // A should be after B and all are pinned
4927 assert_item_labels(&pane_a, ["B!", "A*!", "C!"], cx);
4928
4929 // Move A to right of C
4930 pane_a.update_in(cx, |pane, window, cx| {
4931 let dragged_tab = DraggedTab {
4932 pane: pane_a.clone(),
4933 item: item_a.boxed_clone(),
4934 ix: 1,
4935 detail: 0,
4936 is_active: true,
4937 };
4938 pane.handle_tab_drop(&dragged_tab, 2, window, cx);
4939 });
4940
4941 // A should be after C and all are pinned
4942 assert_item_labels(&pane_a, ["B!", "C!", "A*!"], cx);
4943
4944 // Move A to left of C
4945 pane_a.update_in(cx, |pane, window, cx| {
4946 let dragged_tab = DraggedTab {
4947 pane: pane_a.clone(),
4948 item: item_a.boxed_clone(),
4949 ix: 2,
4950 detail: 0,
4951 is_active: true,
4952 };
4953 pane.handle_tab_drop(&dragged_tab, 1, window, cx);
4954 });
4955
4956 // A should be before C and all are pinned
4957 assert_item_labels(&pane_a, ["B!", "A*!", "C!"], cx);
4958
4959 // Move A to left of B
4960 pane_a.update_in(cx, |pane, window, cx| {
4961 let dragged_tab = DraggedTab {
4962 pane: pane_a.clone(),
4963 item: item_a.boxed_clone(),
4964 ix: 1,
4965 detail: 0,
4966 is_active: true,
4967 };
4968 pane.handle_tab_drop(&dragged_tab, 0, window, cx);
4969 });
4970
4971 // A should be before B and all are pinned
4972 assert_item_labels(&pane_a, ["A*!", "B!", "C!"], cx);
4973 }
4974
4975 #[gpui::test]
4976 async fn test_drag_first_tab_to_last_position(cx: &mut TestAppContext) {
4977 init_test(cx);
4978 let fs = FakeFs::new(cx.executor());
4979
4980 let project = Project::test(fs, None, cx).await;
4981 let (workspace, cx) =
4982 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4983 let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4984
4985 // Add A, B, C
4986 let item_a = add_labeled_item(&pane_a, "A", false, cx);
4987 add_labeled_item(&pane_a, "B", false, cx);
4988 add_labeled_item(&pane_a, "C", false, cx);
4989 assert_item_labels(&pane_a, ["A", "B", "C*"], cx);
4990
4991 // Move A to the end
4992 pane_a.update_in(cx, |pane, window, cx| {
4993 let dragged_tab = DraggedTab {
4994 pane: pane_a.clone(),
4995 item: item_a.boxed_clone(),
4996 ix: 0,
4997 detail: 0,
4998 is_active: true,
4999 };
5000 pane.handle_tab_drop(&dragged_tab, 2, window, cx);
5001 });
5002
5003 // A should be at the end
5004 assert_item_labels(&pane_a, ["B", "C", "A*"], cx);
5005 }
5006
5007 #[gpui::test]
5008 async fn test_drag_last_tab_to_first_position(cx: &mut TestAppContext) {
5009 init_test(cx);
5010 let fs = FakeFs::new(cx.executor());
5011
5012 let project = Project::test(fs, None, cx).await;
5013 let (workspace, cx) =
5014 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5015 let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5016
5017 // Add A, B, C
5018 add_labeled_item(&pane_a, "A", false, cx);
5019 add_labeled_item(&pane_a, "B", false, cx);
5020 let item_c = add_labeled_item(&pane_a, "C", false, cx);
5021 assert_item_labels(&pane_a, ["A", "B", "C*"], cx);
5022
5023 // Move C to the beginning
5024 pane_a.update_in(cx, |pane, window, cx| {
5025 let dragged_tab = DraggedTab {
5026 pane: pane_a.clone(),
5027 item: item_c.boxed_clone(),
5028 ix: 2,
5029 detail: 0,
5030 is_active: true,
5031 };
5032 pane.handle_tab_drop(&dragged_tab, 0, window, cx);
5033 });
5034
5035 // C should be at the beginning
5036 assert_item_labels(&pane_a, ["C*", "A", "B"], cx);
5037 }
5038
5039 #[gpui::test]
5040 async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
5041 init_test(cx);
5042 let fs = FakeFs::new(cx.executor());
5043
5044 let project = Project::test(fs, None, cx).await;
5045 let (workspace, cx) =
5046 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5047 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5048
5049 // 1. Add with a destination index
5050 // a. Add before the active item
5051 set_labeled_items(&pane, ["A", "B*", "C"], cx);
5052 pane.update_in(cx, |pane, window, cx| {
5053 pane.add_item(
5054 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
5055 false,
5056 false,
5057 Some(0),
5058 window,
5059 cx,
5060 );
5061 });
5062 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
5063
5064 // b. Add after the active item
5065 set_labeled_items(&pane, ["A", "B*", "C"], cx);
5066 pane.update_in(cx, |pane, window, cx| {
5067 pane.add_item(
5068 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
5069 false,
5070 false,
5071 Some(2),
5072 window,
5073 cx,
5074 );
5075 });
5076 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
5077
5078 // c. Add at the end of the item list (including off the length)
5079 set_labeled_items(&pane, ["A", "B*", "C"], cx);
5080 pane.update_in(cx, |pane, window, cx| {
5081 pane.add_item(
5082 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
5083 false,
5084 false,
5085 Some(5),
5086 window,
5087 cx,
5088 );
5089 });
5090 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
5091
5092 // 2. Add without a destination index
5093 // a. Add with active item at the start of the item list
5094 set_labeled_items(&pane, ["A*", "B", "C"], cx);
5095 pane.update_in(cx, |pane, window, cx| {
5096 pane.add_item(
5097 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
5098 false,
5099 false,
5100 None,
5101 window,
5102 cx,
5103 );
5104 });
5105 set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
5106
5107 // b. Add with active item at the end of the item list
5108 set_labeled_items(&pane, ["A", "B", "C*"], cx);
5109 pane.update_in(cx, |pane, window, cx| {
5110 pane.add_item(
5111 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
5112 false,
5113 false,
5114 None,
5115 window,
5116 cx,
5117 );
5118 });
5119 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
5120 }
5121
5122 #[gpui::test]
5123 async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
5124 init_test(cx);
5125 let fs = FakeFs::new(cx.executor());
5126
5127 let project = Project::test(fs, None, cx).await;
5128 let (workspace, cx) =
5129 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5130 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5131
5132 // 1. Add with a destination index
5133 // 1a. Add before the active item
5134 let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
5135 pane.update_in(cx, |pane, window, cx| {
5136 pane.add_item(d, false, false, Some(0), window, cx);
5137 });
5138 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
5139
5140 // 1b. Add after the active item
5141 let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
5142 pane.update_in(cx, |pane, window, cx| {
5143 pane.add_item(d, false, false, Some(2), window, cx);
5144 });
5145 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
5146
5147 // 1c. Add at the end of the item list (including off the length)
5148 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
5149 pane.update_in(cx, |pane, window, cx| {
5150 pane.add_item(a, false, false, Some(5), window, cx);
5151 });
5152 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
5153
5154 // 1d. Add same item to active index
5155 let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
5156 pane.update_in(cx, |pane, window, cx| {
5157 pane.add_item(b, false, false, Some(1), window, cx);
5158 });
5159 assert_item_labels(&pane, ["A", "B*", "C"], cx);
5160
5161 // 1e. Add item to index after same item in last position
5162 let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
5163 pane.update_in(cx, |pane, window, cx| {
5164 pane.add_item(c, false, false, Some(2), window, cx);
5165 });
5166 assert_item_labels(&pane, ["A", "B", "C*"], cx);
5167
5168 // 2. Add without a destination index
5169 // 2a. Add with active item at the start of the item list
5170 let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
5171 pane.update_in(cx, |pane, window, cx| {
5172 pane.add_item(d, false, false, None, window, cx);
5173 });
5174 assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
5175
5176 // 2b. Add with active item at the end of the item list
5177 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
5178 pane.update_in(cx, |pane, window, cx| {
5179 pane.add_item(a, false, false, None, window, cx);
5180 });
5181 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
5182
5183 // 2c. Add active item to active item at end of list
5184 let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
5185 pane.update_in(cx, |pane, window, cx| {
5186 pane.add_item(c, false, false, None, window, cx);
5187 });
5188 assert_item_labels(&pane, ["A", "B", "C*"], cx);
5189
5190 // 2d. Add active item to active item at start of list
5191 let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
5192 pane.update_in(cx, |pane, window, cx| {
5193 pane.add_item(a, false, false, None, window, cx);
5194 });
5195 assert_item_labels(&pane, ["A*", "B", "C"], cx);
5196 }
5197
5198 #[gpui::test]
5199 async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
5200 init_test(cx);
5201 let fs = FakeFs::new(cx.executor());
5202
5203 let project = Project::test(fs, None, cx).await;
5204 let (workspace, cx) =
5205 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5206 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5207
5208 // singleton view
5209 pane.update_in(cx, |pane, window, cx| {
5210 pane.add_item(
5211 Box::new(cx.new(|cx| {
5212 TestItem::new(cx)
5213 .with_singleton(true)
5214 .with_label("buffer 1")
5215 .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
5216 })),
5217 false,
5218 false,
5219 None,
5220 window,
5221 cx,
5222 );
5223 });
5224 assert_item_labels(&pane, ["buffer 1*"], cx);
5225
5226 // new singleton view with the same project entry
5227 pane.update_in(cx, |pane, window, cx| {
5228 pane.add_item(
5229 Box::new(cx.new(|cx| {
5230 TestItem::new(cx)
5231 .with_singleton(true)
5232 .with_label("buffer 1")
5233 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
5234 })),
5235 false,
5236 false,
5237 None,
5238 window,
5239 cx,
5240 );
5241 });
5242 assert_item_labels(&pane, ["buffer 1*"], cx);
5243
5244 // new singleton view with different project entry
5245 pane.update_in(cx, |pane, window, cx| {
5246 pane.add_item(
5247 Box::new(cx.new(|cx| {
5248 TestItem::new(cx)
5249 .with_singleton(true)
5250 .with_label("buffer 2")
5251 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
5252 })),
5253 false,
5254 false,
5255 None,
5256 window,
5257 cx,
5258 );
5259 });
5260 assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
5261
5262 // new multibuffer view with the same project entry
5263 pane.update_in(cx, |pane, window, cx| {
5264 pane.add_item(
5265 Box::new(cx.new(|cx| {
5266 TestItem::new(cx)
5267 .with_singleton(false)
5268 .with_label("multibuffer 1")
5269 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
5270 })),
5271 false,
5272 false,
5273 None,
5274 window,
5275 cx,
5276 );
5277 });
5278 assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
5279
5280 // another multibuffer view with the same project entry
5281 pane.update_in(cx, |pane, window, cx| {
5282 pane.add_item(
5283 Box::new(cx.new(|cx| {
5284 TestItem::new(cx)
5285 .with_singleton(false)
5286 .with_label("multibuffer 1b")
5287 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
5288 })),
5289 false,
5290 false,
5291 None,
5292 window,
5293 cx,
5294 );
5295 });
5296 assert_item_labels(
5297 &pane,
5298 ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
5299 cx,
5300 );
5301 }
5302
5303 #[gpui::test]
5304 async fn test_remove_item_ordering_history(cx: &mut TestAppContext) {
5305 init_test(cx);
5306 let fs = FakeFs::new(cx.executor());
5307
5308 let project = Project::test(fs, None, cx).await;
5309 let (workspace, cx) =
5310 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5311 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5312
5313 add_labeled_item(&pane, "A", false, cx);
5314 add_labeled_item(&pane, "B", false, cx);
5315 add_labeled_item(&pane, "C", false, cx);
5316 add_labeled_item(&pane, "D", false, cx);
5317 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
5318
5319 pane.update_in(cx, |pane, window, cx| {
5320 pane.activate_item(1, false, false, window, cx)
5321 });
5322 add_labeled_item(&pane, "1", false, cx);
5323 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
5324
5325 pane.update_in(cx, |pane, window, cx| {
5326 pane.close_active_item(
5327 &CloseActiveItem {
5328 save_intent: None,
5329 close_pinned: false,
5330 },
5331 window,
5332 cx,
5333 )
5334 })
5335 .await
5336 .unwrap();
5337 assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
5338
5339 pane.update_in(cx, |pane, window, cx| {
5340 pane.activate_item(3, false, false, window, cx)
5341 });
5342 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
5343
5344 pane.update_in(cx, |pane, window, cx| {
5345 pane.close_active_item(
5346 &CloseActiveItem {
5347 save_intent: None,
5348 close_pinned: false,
5349 },
5350 window,
5351 cx,
5352 )
5353 })
5354 .await
5355 .unwrap();
5356 assert_item_labels(&pane, ["A", "B*", "C"], cx);
5357
5358 pane.update_in(cx, |pane, window, cx| {
5359 pane.close_active_item(
5360 &CloseActiveItem {
5361 save_intent: None,
5362 close_pinned: false,
5363 },
5364 window,
5365 cx,
5366 )
5367 })
5368 .await
5369 .unwrap();
5370 assert_item_labels(&pane, ["A", "C*"], cx);
5371
5372 pane.update_in(cx, |pane, window, cx| {
5373 pane.close_active_item(
5374 &CloseActiveItem {
5375 save_intent: None,
5376 close_pinned: false,
5377 },
5378 window,
5379 cx,
5380 )
5381 })
5382 .await
5383 .unwrap();
5384 assert_item_labels(&pane, ["A*"], cx);
5385 }
5386
5387 #[gpui::test]
5388 async fn test_remove_item_ordering_neighbour(cx: &mut TestAppContext) {
5389 init_test(cx);
5390 cx.update_global::<SettingsStore, ()>(|s, cx| {
5391 s.update_user_settings::<ItemSettings>(cx, |s| {
5392 s.activate_on_close = Some(ActivateOnClose::Neighbour);
5393 });
5394 });
5395 let fs = FakeFs::new(cx.executor());
5396
5397 let project = Project::test(fs, None, cx).await;
5398 let (workspace, cx) =
5399 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5400 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5401
5402 add_labeled_item(&pane, "A", false, cx);
5403 add_labeled_item(&pane, "B", false, cx);
5404 add_labeled_item(&pane, "C", false, cx);
5405 add_labeled_item(&pane, "D", false, cx);
5406 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
5407
5408 pane.update_in(cx, |pane, window, cx| {
5409 pane.activate_item(1, false, false, window, cx)
5410 });
5411 add_labeled_item(&pane, "1", false, cx);
5412 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
5413
5414 pane.update_in(cx, |pane, window, cx| {
5415 pane.close_active_item(
5416 &CloseActiveItem {
5417 save_intent: None,
5418 close_pinned: false,
5419 },
5420 window,
5421 cx,
5422 )
5423 })
5424 .await
5425 .unwrap();
5426 assert_item_labels(&pane, ["A", "B", "C*", "D"], cx);
5427
5428 pane.update_in(cx, |pane, window, cx| {
5429 pane.activate_item(3, false, false, window, cx)
5430 });
5431 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
5432
5433 pane.update_in(cx, |pane, window, cx| {
5434 pane.close_active_item(
5435 &CloseActiveItem {
5436 save_intent: None,
5437 close_pinned: false,
5438 },
5439 window,
5440 cx,
5441 )
5442 })
5443 .await
5444 .unwrap();
5445 assert_item_labels(&pane, ["A", "B", "C*"], cx);
5446
5447 pane.update_in(cx, |pane, window, cx| {
5448 pane.close_active_item(
5449 &CloseActiveItem {
5450 save_intent: None,
5451 close_pinned: false,
5452 },
5453 window,
5454 cx,
5455 )
5456 })
5457 .await
5458 .unwrap();
5459 assert_item_labels(&pane, ["A", "B*"], cx);
5460
5461 pane.update_in(cx, |pane, window, cx| {
5462 pane.close_active_item(
5463 &CloseActiveItem {
5464 save_intent: None,
5465 close_pinned: false,
5466 },
5467 window,
5468 cx,
5469 )
5470 })
5471 .await
5472 .unwrap();
5473 assert_item_labels(&pane, ["A*"], cx);
5474 }
5475
5476 #[gpui::test]
5477 async fn test_remove_item_ordering_left_neighbour(cx: &mut TestAppContext) {
5478 init_test(cx);
5479 cx.update_global::<SettingsStore, ()>(|s, cx| {
5480 s.update_user_settings::<ItemSettings>(cx, |s| {
5481 s.activate_on_close = Some(ActivateOnClose::LeftNeighbour);
5482 });
5483 });
5484 let fs = FakeFs::new(cx.executor());
5485
5486 let project = Project::test(fs, None, cx).await;
5487 let (workspace, cx) =
5488 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5489 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5490
5491 add_labeled_item(&pane, "A", false, cx);
5492 add_labeled_item(&pane, "B", false, cx);
5493 add_labeled_item(&pane, "C", false, cx);
5494 add_labeled_item(&pane, "D", false, cx);
5495 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
5496
5497 pane.update_in(cx, |pane, window, cx| {
5498 pane.activate_item(1, false, false, window, cx)
5499 });
5500 add_labeled_item(&pane, "1", false, cx);
5501 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
5502
5503 pane.update_in(cx, |pane, window, cx| {
5504 pane.close_active_item(
5505 &CloseActiveItem {
5506 save_intent: None,
5507 close_pinned: false,
5508 },
5509 window,
5510 cx,
5511 )
5512 })
5513 .await
5514 .unwrap();
5515 assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
5516
5517 pane.update_in(cx, |pane, window, cx| {
5518 pane.activate_item(3, false, false, window, cx)
5519 });
5520 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
5521
5522 pane.update_in(cx, |pane, window, cx| {
5523 pane.close_active_item(
5524 &CloseActiveItem {
5525 save_intent: None,
5526 close_pinned: false,
5527 },
5528 window,
5529 cx,
5530 )
5531 })
5532 .await
5533 .unwrap();
5534 assert_item_labels(&pane, ["A", "B", "C*"], cx);
5535
5536 pane.update_in(cx, |pane, window, cx| {
5537 pane.activate_item(0, false, false, window, cx)
5538 });
5539 assert_item_labels(&pane, ["A*", "B", "C"], cx);
5540
5541 pane.update_in(cx, |pane, window, cx| {
5542 pane.close_active_item(
5543 &CloseActiveItem {
5544 save_intent: None,
5545 close_pinned: false,
5546 },
5547 window,
5548 cx,
5549 )
5550 })
5551 .await
5552 .unwrap();
5553 assert_item_labels(&pane, ["B*", "C"], cx);
5554
5555 pane.update_in(cx, |pane, window, cx| {
5556 pane.close_active_item(
5557 &CloseActiveItem {
5558 save_intent: None,
5559 close_pinned: false,
5560 },
5561 window,
5562 cx,
5563 )
5564 })
5565 .await
5566 .unwrap();
5567 assert_item_labels(&pane, ["C*"], cx);
5568 }
5569
5570 #[gpui::test]
5571 async fn test_close_inactive_items(cx: &mut TestAppContext) {
5572 init_test(cx);
5573 let fs = FakeFs::new(cx.executor());
5574
5575 let project = Project::test(fs, None, cx).await;
5576 let (workspace, cx) =
5577 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5578 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5579
5580 let item_a = add_labeled_item(&pane, "A", false, cx);
5581 pane.update_in(cx, |pane, window, cx| {
5582 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
5583 pane.pin_tab_at(ix, window, cx);
5584 });
5585 assert_item_labels(&pane, ["A*!"], cx);
5586
5587 let item_b = add_labeled_item(&pane, "B", false, cx);
5588 pane.update_in(cx, |pane, window, cx| {
5589 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
5590 pane.pin_tab_at(ix, window, cx);
5591 });
5592 assert_item_labels(&pane, ["A!", "B*!"], cx);
5593
5594 add_labeled_item(&pane, "C", false, cx);
5595 assert_item_labels(&pane, ["A!", "B!", "C*"], cx);
5596
5597 add_labeled_item(&pane, "D", false, cx);
5598 add_labeled_item(&pane, "E", false, cx);
5599 assert_item_labels(&pane, ["A!", "B!", "C", "D", "E*"], cx);
5600
5601 pane.update_in(cx, |pane, window, cx| {
5602 pane.close_inactive_items(
5603 &CloseInactiveItems {
5604 save_intent: None,
5605 close_pinned: false,
5606 },
5607 window,
5608 cx,
5609 )
5610 })
5611 .await
5612 .unwrap();
5613 assert_item_labels(&pane, ["A!", "B!", "E*"], cx);
5614 }
5615
5616 #[gpui::test]
5617 async fn test_close_clean_items(cx: &mut TestAppContext) {
5618 init_test(cx);
5619 let fs = FakeFs::new(cx.executor());
5620
5621 let project = Project::test(fs, None, cx).await;
5622 let (workspace, cx) =
5623 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5624 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5625
5626 add_labeled_item(&pane, "A", true, cx);
5627 add_labeled_item(&pane, "B", false, cx);
5628 add_labeled_item(&pane, "C", true, cx);
5629 add_labeled_item(&pane, "D", false, cx);
5630 add_labeled_item(&pane, "E", false, cx);
5631 assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
5632
5633 pane.update_in(cx, |pane, window, cx| {
5634 pane.close_clean_items(
5635 &CloseCleanItems {
5636 close_pinned: false,
5637 },
5638 window,
5639 cx,
5640 )
5641 })
5642 .await
5643 .unwrap();
5644 assert_item_labels(&pane, ["A^", "C*^"], cx);
5645 }
5646
5647 #[gpui::test]
5648 async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
5649 init_test(cx);
5650 let fs = FakeFs::new(cx.executor());
5651
5652 let project = Project::test(fs, None, cx).await;
5653 let (workspace, cx) =
5654 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5655 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5656
5657 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
5658
5659 pane.update_in(cx, |pane, window, cx| {
5660 pane.close_items_to_the_left_by_id(
5661 None,
5662 &CloseItemsToTheLeft {
5663 close_pinned: false,
5664 },
5665 window,
5666 cx,
5667 )
5668 })
5669 .await
5670 .unwrap();
5671 assert_item_labels(&pane, ["C*", "D", "E"], cx);
5672 }
5673
5674 #[gpui::test]
5675 async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
5676 init_test(cx);
5677 let fs = FakeFs::new(cx.executor());
5678
5679 let project = Project::test(fs, None, cx).await;
5680 let (workspace, cx) =
5681 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5682 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5683
5684 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
5685
5686 pane.update_in(cx, |pane, window, cx| {
5687 pane.close_items_to_the_right_by_id(
5688 None,
5689 &CloseItemsToTheRight {
5690 close_pinned: false,
5691 },
5692 window,
5693 cx,
5694 )
5695 })
5696 .await
5697 .unwrap();
5698 assert_item_labels(&pane, ["A", "B", "C*"], cx);
5699 }
5700
5701 #[gpui::test]
5702 async fn test_close_all_items(cx: &mut TestAppContext) {
5703 init_test(cx);
5704 let fs = FakeFs::new(cx.executor());
5705
5706 let project = Project::test(fs, None, cx).await;
5707 let (workspace, cx) =
5708 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5709 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5710
5711 let item_a = add_labeled_item(&pane, "A", false, cx);
5712 add_labeled_item(&pane, "B", false, cx);
5713 add_labeled_item(&pane, "C", false, cx);
5714 assert_item_labels(&pane, ["A", "B", "C*"], cx);
5715
5716 pane.update_in(cx, |pane, window, cx| {
5717 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
5718 pane.pin_tab_at(ix, window, cx);
5719 pane.close_all_items(
5720 &CloseAllItems {
5721 save_intent: None,
5722 close_pinned: false,
5723 },
5724 window,
5725 cx,
5726 )
5727 })
5728 .await
5729 .unwrap();
5730 assert_item_labels(&pane, ["A*!"], cx);
5731
5732 pane.update_in(cx, |pane, window, cx| {
5733 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
5734 pane.unpin_tab_at(ix, window, cx);
5735 pane.close_all_items(
5736 &CloseAllItems {
5737 save_intent: None,
5738 close_pinned: false,
5739 },
5740 window,
5741 cx,
5742 )
5743 })
5744 .await
5745 .unwrap();
5746
5747 assert_item_labels(&pane, [], cx);
5748
5749 add_labeled_item(&pane, "A", true, cx).update(cx, |item, cx| {
5750 item.project_items
5751 .push(TestProjectItem::new_dirty(1, "A.txt", cx))
5752 });
5753 add_labeled_item(&pane, "B", true, cx).update(cx, |item, cx| {
5754 item.project_items
5755 .push(TestProjectItem::new_dirty(2, "B.txt", cx))
5756 });
5757 add_labeled_item(&pane, "C", true, cx).update(cx, |item, cx| {
5758 item.project_items
5759 .push(TestProjectItem::new_dirty(3, "C.txt", cx))
5760 });
5761 assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
5762
5763 let save = pane.update_in(cx, |pane, window, cx| {
5764 pane.close_all_items(
5765 &CloseAllItems {
5766 save_intent: None,
5767 close_pinned: false,
5768 },
5769 window,
5770 cx,
5771 )
5772 });
5773
5774 cx.executor().run_until_parked();
5775 cx.simulate_prompt_answer("Save all");
5776 save.await.unwrap();
5777 assert_item_labels(&pane, [], cx);
5778
5779 add_labeled_item(&pane, "A", true, cx);
5780 add_labeled_item(&pane, "B", true, cx);
5781 add_labeled_item(&pane, "C", true, cx);
5782 assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
5783 let save = pane.update_in(cx, |pane, window, cx| {
5784 pane.close_all_items(
5785 &CloseAllItems {
5786 save_intent: None,
5787 close_pinned: false,
5788 },
5789 window,
5790 cx,
5791 )
5792 });
5793
5794 cx.executor().run_until_parked();
5795 cx.simulate_prompt_answer("Discard all");
5796 save.await.unwrap();
5797 assert_item_labels(&pane, [], cx);
5798 }
5799
5800 #[gpui::test]
5801 async fn test_close_with_save_intent(cx: &mut TestAppContext) {
5802 init_test(cx);
5803 let fs = FakeFs::new(cx.executor());
5804
5805 let project = Project::test(fs, None, cx).await;
5806 let (workspace, cx) =
5807 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
5808 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5809
5810 let a = cx.update(|_, cx| TestProjectItem::new_dirty(1, "A.txt", cx));
5811 let b = cx.update(|_, cx| TestProjectItem::new_dirty(1, "B.txt", cx));
5812 let c = cx.update(|_, cx| TestProjectItem::new_dirty(1, "C.txt", cx));
5813
5814 add_labeled_item(&pane, "AB", true, cx).update(cx, |item, _| {
5815 item.project_items.push(a.clone());
5816 item.project_items.push(b.clone());
5817 });
5818 add_labeled_item(&pane, "C", true, cx)
5819 .update(cx, |item, _| item.project_items.push(c.clone()));
5820 assert_item_labels(&pane, ["AB^", "C*^"], cx);
5821
5822 pane.update_in(cx, |pane, window, cx| {
5823 pane.close_all_items(
5824 &CloseAllItems {
5825 save_intent: Some(SaveIntent::Save),
5826 close_pinned: false,
5827 },
5828 window,
5829 cx,
5830 )
5831 })
5832 .await
5833 .unwrap();
5834
5835 assert_item_labels(&pane, [], cx);
5836 cx.update(|_, cx| {
5837 assert!(!a.read(cx).is_dirty);
5838 assert!(!b.read(cx).is_dirty);
5839 assert!(!c.read(cx).is_dirty);
5840 });
5841 }
5842
5843 #[gpui::test]
5844 async fn test_close_all_items_including_pinned(cx: &mut TestAppContext) {
5845 init_test(cx);
5846 let fs = FakeFs::new(cx.executor());
5847
5848 let project = Project::test(fs, None, cx).await;
5849 let (workspace, cx) =
5850 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
5851 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5852
5853 let item_a = add_labeled_item(&pane, "A", false, cx);
5854 add_labeled_item(&pane, "B", false, cx);
5855 add_labeled_item(&pane, "C", false, cx);
5856 assert_item_labels(&pane, ["A", "B", "C*"], cx);
5857
5858 pane.update_in(cx, |pane, window, cx| {
5859 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
5860 pane.pin_tab_at(ix, window, cx);
5861 pane.close_all_items(
5862 &CloseAllItems {
5863 save_intent: None,
5864 close_pinned: true,
5865 },
5866 window,
5867 cx,
5868 )
5869 })
5870 .await
5871 .unwrap();
5872 assert_item_labels(&pane, [], cx);
5873 }
5874
5875 #[gpui::test]
5876 async fn test_close_pinned_tab_with_non_pinned_in_same_pane(cx: &mut TestAppContext) {
5877 init_test(cx);
5878 let fs = FakeFs::new(cx.executor());
5879 let project = Project::test(fs, None, cx).await;
5880 let (workspace, cx) =
5881 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
5882
5883 // Non-pinned tabs in same pane
5884 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5885 add_labeled_item(&pane, "A", false, cx);
5886 add_labeled_item(&pane, "B", false, cx);
5887 add_labeled_item(&pane, "C", false, cx);
5888 pane.update_in(cx, |pane, window, cx| {
5889 pane.pin_tab_at(0, window, cx);
5890 });
5891 set_labeled_items(&pane, ["A*", "B", "C"], cx);
5892 pane.update_in(cx, |pane, window, cx| {
5893 pane.close_active_item(
5894 &CloseActiveItem {
5895 save_intent: None,
5896 close_pinned: false,
5897 },
5898 window,
5899 cx,
5900 )
5901 .unwrap();
5902 });
5903 // Non-pinned tab should be active
5904 assert_item_labels(&pane, ["A!", "B*", "C"], cx);
5905 }
5906
5907 #[gpui::test]
5908 async fn test_close_pinned_tab_with_non_pinned_in_different_pane(cx: &mut TestAppContext) {
5909 init_test(cx);
5910 let fs = FakeFs::new(cx.executor());
5911 let project = Project::test(fs, None, cx).await;
5912 let (workspace, cx) =
5913 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
5914
5915 // No non-pinned tabs in same pane, non-pinned tabs in another pane
5916 let pane1 = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5917 let pane2 = workspace.update_in(cx, |workspace, window, cx| {
5918 workspace.split_pane(pane1.clone(), SplitDirection::Right, window, cx)
5919 });
5920 add_labeled_item(&pane1, "A", false, cx);
5921 pane1.update_in(cx, |pane, window, cx| {
5922 pane.pin_tab_at(0, window, cx);
5923 });
5924 set_labeled_items(&pane1, ["A*"], cx);
5925 add_labeled_item(&pane2, "B", false, cx);
5926 set_labeled_items(&pane2, ["B"], cx);
5927 pane1.update_in(cx, |pane, window, cx| {
5928 pane.close_active_item(
5929 &CloseActiveItem {
5930 save_intent: None,
5931 close_pinned: false,
5932 },
5933 window,
5934 cx,
5935 )
5936 .unwrap();
5937 });
5938 // Non-pinned tab of other pane should be active
5939 assert_item_labels(&pane2, ["B*"], cx);
5940 }
5941
5942 #[gpui::test]
5943 async fn ensure_item_closing_actions_do_not_panic_when_no_items_exist(cx: &mut TestAppContext) {
5944 init_test(cx);
5945 let fs = FakeFs::new(cx.executor());
5946 let project = Project::test(fs, None, cx).await;
5947 let (workspace, cx) =
5948 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
5949
5950 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5951 assert_item_labels(&pane, [], cx);
5952
5953 pane.update_in(cx, |pane, window, cx| {
5954 pane.close_active_item(
5955 &CloseActiveItem {
5956 save_intent: None,
5957 close_pinned: false,
5958 },
5959 window,
5960 cx,
5961 )
5962 })
5963 .await
5964 .unwrap();
5965
5966 pane.update_in(cx, |pane, window, cx| {
5967 pane.close_inactive_items(
5968 &CloseInactiveItems {
5969 save_intent: None,
5970 close_pinned: false,
5971 },
5972 window,
5973 cx,
5974 )
5975 })
5976 .await
5977 .unwrap();
5978
5979 pane.update_in(cx, |pane, window, cx| {
5980 pane.close_all_items(
5981 &CloseAllItems {
5982 save_intent: None,
5983 close_pinned: false,
5984 },
5985 window,
5986 cx,
5987 )
5988 })
5989 .await
5990 .unwrap();
5991
5992 pane.update_in(cx, |pane, window, cx| {
5993 pane.close_clean_items(
5994 &CloseCleanItems {
5995 close_pinned: false,
5996 },
5997 window,
5998 cx,
5999 )
6000 })
6001 .await
6002 .unwrap();
6003
6004 pane.update_in(cx, |pane, window, cx| {
6005 pane.close_items_to_the_right_by_id(
6006 None,
6007 &CloseItemsToTheRight {
6008 close_pinned: false,
6009 },
6010 window,
6011 cx,
6012 )
6013 })
6014 .await
6015 .unwrap();
6016
6017 pane.update_in(cx, |pane, window, cx| {
6018 pane.close_items_to_the_left_by_id(
6019 None,
6020 &CloseItemsToTheLeft {
6021 close_pinned: false,
6022 },
6023 window,
6024 cx,
6025 )
6026 })
6027 .await
6028 .unwrap();
6029 }
6030
6031 fn init_test(cx: &mut TestAppContext) {
6032 cx.update(|cx| {
6033 let settings_store = SettingsStore::test(cx);
6034 cx.set_global(settings_store);
6035 theme::init(LoadThemes::JustBase, cx);
6036 crate::init_settings(cx);
6037 Project::init_settings(cx);
6038 });
6039 }
6040
6041 fn set_max_tabs(cx: &mut TestAppContext, value: Option<usize>) {
6042 cx.update_global(|store: &mut SettingsStore, cx| {
6043 store.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6044 settings.max_tabs = value.map(|v| NonZero::new(v).unwrap())
6045 });
6046 });
6047 }
6048
6049 fn add_labeled_item(
6050 pane: &Entity<Pane>,
6051 label: &str,
6052 is_dirty: bool,
6053 cx: &mut VisualTestContext,
6054 ) -> Box<Entity<TestItem>> {
6055 pane.update_in(cx, |pane, window, cx| {
6056 let labeled_item =
6057 Box::new(cx.new(|cx| TestItem::new(cx).with_label(label).with_dirty(is_dirty)));
6058 pane.add_item(labeled_item.clone(), false, false, None, window, cx);
6059 labeled_item
6060 })
6061 }
6062
6063 fn set_labeled_items<const COUNT: usize>(
6064 pane: &Entity<Pane>,
6065 labels: [&str; COUNT],
6066 cx: &mut VisualTestContext,
6067 ) -> [Box<Entity<TestItem>>; COUNT] {
6068 pane.update_in(cx, |pane, window, cx| {
6069 pane.items.clear();
6070 let mut active_item_index = 0;
6071
6072 let mut index = 0;
6073 let items = labels.map(|mut label| {
6074 if label.ends_with('*') {
6075 label = label.trim_end_matches('*');
6076 active_item_index = index;
6077 }
6078
6079 let labeled_item = Box::new(cx.new(|cx| TestItem::new(cx).with_label(label)));
6080 pane.add_item(labeled_item.clone(), false, false, None, window, cx);
6081 index += 1;
6082 labeled_item
6083 });
6084
6085 pane.activate_item(active_item_index, false, false, window, cx);
6086
6087 items
6088 })
6089 }
6090
6091 // Assert the item label, with the active item label suffixed with a '*'
6092 #[track_caller]
6093 fn assert_item_labels<const COUNT: usize>(
6094 pane: &Entity<Pane>,
6095 expected_states: [&str; COUNT],
6096 cx: &mut VisualTestContext,
6097 ) {
6098 let actual_states = pane.update(cx, |pane, cx| {
6099 pane.items
6100 .iter()
6101 .enumerate()
6102 .map(|(ix, item)| {
6103 let mut state = item
6104 .to_any()
6105 .downcast::<TestItem>()
6106 .unwrap()
6107 .read(cx)
6108 .label
6109 .clone();
6110 if ix == pane.active_item_index {
6111 state.push('*');
6112 }
6113 if item.is_dirty(cx) {
6114 state.push('^');
6115 }
6116 if pane.is_tab_pinned(ix) {
6117 state.push('!');
6118 }
6119 state
6120 })
6121 .collect::<Vec<_>>()
6122 });
6123 assert_eq!(
6124 actual_states, expected_states,
6125 "pane items do not match expectation"
6126 );
6127 }
6128}