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