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