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