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