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