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