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