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