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