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