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