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