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 safe_pinned_count = if self.pinned_tab_count > tab_count {
2794 log::warn!(
2795 "Pinned tab count ({}) exceeds actual tab count ({}). \
2796 This should not happen. If possible, add reproduction steps, \
2797 in a comment, to https://github.com/zed-industries/zed/issues/33342",
2798 self.pinned_tab_count,
2799 tab_count
2800 );
2801 tab_count
2802 } else {
2803 self.pinned_tab_count
2804 };
2805 let unpinned_tabs = tab_items.split_off(safe_pinned_count);
2806 let pinned_tabs = tab_items;
2807 TabBar::new("tab_bar")
2808 .when(
2809 self.display_nav_history_buttons.unwrap_or_default(),
2810 |tab_bar| {
2811 tab_bar
2812 .start_child(navigate_backward)
2813 .start_child(navigate_forward)
2814 },
2815 )
2816 .map(|tab_bar| {
2817 if self.show_tab_bar_buttons {
2818 let render_tab_buttons = self.render_tab_bar_buttons.clone();
2819 let (left_children, right_children) = render_tab_buttons(self, window, cx);
2820 tab_bar
2821 .start_children(left_children)
2822 .end_children(right_children)
2823 } else {
2824 tab_bar
2825 }
2826 })
2827 .children(pinned_tabs.len().ne(&0).then(|| {
2828 let content_width = self.tab_bar_scroll_handle.content_size().width;
2829 let viewport_width = self.tab_bar_scroll_handle.viewport().size.width;
2830 // We need to check both because offset returns delta values even when the scroll handle is not scrollable
2831 let is_scrollable = content_width > viewport_width;
2832 let is_scrolled = self.tab_bar_scroll_handle.offset().x < px(0.);
2833 let has_active_unpinned_tab = self.active_item_index >= self.pinned_tab_count;
2834 h_flex()
2835 .children(pinned_tabs)
2836 .when(is_scrollable && is_scrolled, |this| {
2837 this.when(has_active_unpinned_tab, |this| this.border_r_2())
2838 .when(!has_active_unpinned_tab, |this| this.border_r_1())
2839 .border_color(cx.theme().colors().border)
2840 })
2841 }))
2842 .child(
2843 h_flex()
2844 .id("unpinned tabs")
2845 .overflow_x_scroll()
2846 .w_full()
2847 .track_scroll(&self.tab_bar_scroll_handle)
2848 .children(unpinned_tabs)
2849 .child(
2850 div()
2851 .id("tab_bar_drop_target")
2852 .min_w_6()
2853 // HACK: This empty child is currently necessary to force the drop target to appear
2854 // despite us setting a min width above.
2855 .child("")
2856 .h_full()
2857 .flex_grow()
2858 .drag_over::<DraggedTab>(|bar, _, _, cx| {
2859 bar.bg(cx.theme().colors().drop_target_background)
2860 })
2861 .drag_over::<DraggedSelection>(|bar, _, _, cx| {
2862 bar.bg(cx.theme().colors().drop_target_background)
2863 })
2864 .on_drop(cx.listener(
2865 move |this, dragged_tab: &DraggedTab, window, cx| {
2866 this.drag_split_direction = None;
2867 this.handle_tab_drop(dragged_tab, this.items.len(), window, cx)
2868 },
2869 ))
2870 .on_drop(cx.listener(
2871 move |this, selection: &DraggedSelection, window, cx| {
2872 this.drag_split_direction = None;
2873 this.handle_project_entry_drop(
2874 &selection.active_selection.entry_id,
2875 Some(tab_count),
2876 window,
2877 cx,
2878 )
2879 },
2880 ))
2881 .on_drop(cx.listener(move |this, paths, window, cx| {
2882 this.drag_split_direction = None;
2883 this.handle_external_paths_drop(paths, window, cx)
2884 }))
2885 .on_click(cx.listener(move |this, event: &ClickEvent, window, cx| {
2886 if event.up.click_count == 2 {
2887 window.dispatch_action(
2888 this.double_click_dispatch_action.boxed_clone(),
2889 cx,
2890 );
2891 }
2892 })),
2893 ),
2894 )
2895 .into_any_element()
2896 }
2897
2898 pub fn render_menu_overlay(menu: &Entity<ContextMenu>) -> Div {
2899 div().absolute().bottom_0().right_0().size_0().child(
2900 deferred(anchored().anchor(Corner::TopRight).child(menu.clone())).with_priority(1),
2901 )
2902 }
2903
2904 pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut Context<Self>) {
2905 self.zoomed = zoomed;
2906 cx.notify();
2907 }
2908
2909 pub fn is_zoomed(&self) -> bool {
2910 self.zoomed
2911 }
2912
2913 fn handle_drag_move<T: 'static>(
2914 &mut self,
2915 event: &DragMoveEvent<T>,
2916 window: &mut Window,
2917 cx: &mut Context<Self>,
2918 ) {
2919 let can_split_predicate = self.can_split_predicate.take();
2920 let can_split = match &can_split_predicate {
2921 Some(can_split_predicate) => {
2922 can_split_predicate(self, event.dragged_item(), window, cx)
2923 }
2924 None => false,
2925 };
2926 self.can_split_predicate = can_split_predicate;
2927 if !can_split {
2928 return;
2929 }
2930
2931 let rect = event.bounds.size;
2932
2933 let size = event.bounds.size.width.min(event.bounds.size.height)
2934 * WorkspaceSettings::get_global(cx).drop_target_size;
2935
2936 let relative_cursor = Point::new(
2937 event.event.position.x - event.bounds.left(),
2938 event.event.position.y - event.bounds.top(),
2939 );
2940
2941 let direction = if relative_cursor.x < size
2942 || relative_cursor.x > rect.width - size
2943 || relative_cursor.y < size
2944 || relative_cursor.y > rect.height - size
2945 {
2946 [
2947 SplitDirection::Up,
2948 SplitDirection::Right,
2949 SplitDirection::Down,
2950 SplitDirection::Left,
2951 ]
2952 .iter()
2953 .min_by_key(|side| match side {
2954 SplitDirection::Up => relative_cursor.y,
2955 SplitDirection::Right => rect.width - relative_cursor.x,
2956 SplitDirection::Down => rect.height - relative_cursor.y,
2957 SplitDirection::Left => relative_cursor.x,
2958 })
2959 .cloned()
2960 } else {
2961 None
2962 };
2963
2964 if direction != self.drag_split_direction {
2965 self.drag_split_direction = direction;
2966 }
2967 }
2968
2969 pub fn handle_tab_drop(
2970 &mut self,
2971 dragged_tab: &DraggedTab,
2972 ix: usize,
2973 window: &mut Window,
2974 cx: &mut Context<Self>,
2975 ) {
2976 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2977 if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_tab, window, cx) {
2978 return;
2979 }
2980 }
2981 let mut to_pane = cx.entity().clone();
2982 let split_direction = self.drag_split_direction;
2983 let item_id = dragged_tab.item.item_id();
2984 if let Some(preview_item_id) = self.preview_item_id {
2985 if item_id == preview_item_id {
2986 self.set_preview_item_id(None, cx);
2987 }
2988 }
2989
2990 let is_clone = cfg!(target_os = "macos") && window.modifiers().alt
2991 || cfg!(not(target_os = "macos")) && window.modifiers().control;
2992
2993 let from_pane = dragged_tab.pane.clone();
2994 let from_ix = dragged_tab.ix;
2995 self.workspace
2996 .update(cx, |_, cx| {
2997 cx.defer_in(window, move |workspace, window, cx| {
2998 if let Some(split_direction) = split_direction {
2999 to_pane = workspace.split_pane(to_pane, split_direction, window, cx);
3000 }
3001 let database_id = workspace.database_id();
3002 let was_pinned_in_from_pane = from_pane.read_with(cx, |pane, _| {
3003 pane.index_for_item_id(item_id)
3004 .is_some_and(|ix| pane.is_tab_pinned(ix))
3005 });
3006 let to_pane_old_length = to_pane.read(cx).items.len();
3007 if is_clone {
3008 let Some(item) = from_pane
3009 .read(cx)
3010 .items()
3011 .find(|item| item.item_id() == item_id)
3012 .map(|item| item.clone())
3013 else {
3014 return;
3015 };
3016 if let Some(item) = item.clone_on_split(database_id, window, cx) {
3017 to_pane.update(cx, |pane, cx| {
3018 pane.add_item(item, true, true, None, window, cx);
3019 })
3020 }
3021 } else {
3022 move_item(&from_pane, &to_pane, item_id, ix, true, window, cx);
3023 }
3024 to_pane.update(cx, |this, _| {
3025 if to_pane == from_pane {
3026 let moved_right = ix > from_ix;
3027 let ix = if moved_right { ix - 1 } else { ix };
3028 let is_pinned_in_to_pane = this.is_tab_pinned(ix);
3029
3030 if !was_pinned_in_from_pane && is_pinned_in_to_pane {
3031 this.pinned_tab_count += 1;
3032 } else if was_pinned_in_from_pane && !is_pinned_in_to_pane {
3033 this.pinned_tab_count -= 1;
3034 }
3035 } else if this.items.len() >= to_pane_old_length {
3036 let is_pinned_in_to_pane = this.is_tab_pinned(ix);
3037 let item_created_pane = to_pane_old_length == 0;
3038 let is_first_position = ix == 0;
3039 let was_dropped_at_beginning = item_created_pane || is_first_position;
3040 let should_remain_pinned = is_pinned_in_to_pane
3041 || (was_pinned_in_from_pane && was_dropped_at_beginning);
3042
3043 if should_remain_pinned {
3044 this.pinned_tab_count += 1;
3045 }
3046 }
3047 });
3048 });
3049 })
3050 .log_err();
3051 }
3052
3053 fn handle_dragged_selection_drop(
3054 &mut self,
3055 dragged_selection: &DraggedSelection,
3056 dragged_onto: Option<usize>,
3057 window: &mut Window,
3058 cx: &mut Context<Self>,
3059 ) {
3060 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
3061 if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_selection, window, cx)
3062 {
3063 return;
3064 }
3065 }
3066 self.handle_project_entry_drop(
3067 &dragged_selection.active_selection.entry_id,
3068 dragged_onto,
3069 window,
3070 cx,
3071 );
3072 }
3073
3074 fn handle_project_entry_drop(
3075 &mut self,
3076 project_entry_id: &ProjectEntryId,
3077 target: Option<usize>,
3078 window: &mut Window,
3079 cx: &mut Context<Self>,
3080 ) {
3081 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
3082 if let ControlFlow::Break(()) = custom_drop_handle(self, project_entry_id, window, cx) {
3083 return;
3084 }
3085 }
3086 let mut to_pane = cx.entity().clone();
3087 let split_direction = self.drag_split_direction;
3088 let project_entry_id = *project_entry_id;
3089 self.workspace
3090 .update(cx, |_, cx| {
3091 cx.defer_in(window, move |workspace, window, cx| {
3092 if let Some(project_path) = workspace
3093 .project()
3094 .read(cx)
3095 .path_for_entry(project_entry_id, cx)
3096 {
3097 let load_path_task = workspace.load_path(project_path.clone(), window, cx);
3098 cx.spawn_in(window, async move |workspace, cx| {
3099 if let Some((project_entry_id, build_item)) =
3100 load_path_task.await.notify_async_err(cx)
3101 {
3102 let (to_pane, new_item_handle) = workspace
3103 .update_in(cx, |workspace, window, cx| {
3104 if let Some(split_direction) = split_direction {
3105 to_pane = workspace.split_pane(
3106 to_pane,
3107 split_direction,
3108 window,
3109 cx,
3110 );
3111 }
3112 let new_item_handle = to_pane.update(cx, |pane, cx| {
3113 pane.open_item(
3114 project_entry_id,
3115 project_path,
3116 true,
3117 false,
3118 true,
3119 target,
3120 window,
3121 cx,
3122 build_item,
3123 )
3124 });
3125 (to_pane, new_item_handle)
3126 })
3127 .log_err()?;
3128 to_pane
3129 .update_in(cx, |this, window, cx| {
3130 let Some(index) = this.index_for_item(&*new_item_handle)
3131 else {
3132 return;
3133 };
3134
3135 if target.map_or(false, |target| this.is_tab_pinned(target))
3136 {
3137 this.pin_tab_at(index, window, cx);
3138 }
3139 })
3140 .ok()?
3141 }
3142 Some(())
3143 })
3144 .detach();
3145 };
3146 });
3147 })
3148 .log_err();
3149 }
3150
3151 fn handle_external_paths_drop(
3152 &mut self,
3153 paths: &ExternalPaths,
3154 window: &mut Window,
3155 cx: &mut Context<Self>,
3156 ) {
3157 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
3158 if let ControlFlow::Break(()) = custom_drop_handle(self, paths, window, cx) {
3159 return;
3160 }
3161 }
3162 let mut to_pane = cx.entity().clone();
3163 let mut split_direction = self.drag_split_direction;
3164 let paths = paths.paths().to_vec();
3165 let is_remote = self
3166 .workspace
3167 .update(cx, |workspace, cx| {
3168 if workspace.project().read(cx).is_via_collab() {
3169 workspace.show_error(
3170 &anyhow::anyhow!("Cannot drop files on a remote project"),
3171 cx,
3172 );
3173 true
3174 } else {
3175 false
3176 }
3177 })
3178 .unwrap_or(true);
3179 if is_remote {
3180 return;
3181 }
3182
3183 self.workspace
3184 .update(cx, |workspace, cx| {
3185 let fs = Arc::clone(workspace.project().read(cx).fs());
3186 cx.spawn_in(window, async move |workspace, cx| {
3187 let mut is_file_checks = FuturesUnordered::new();
3188 for path in &paths {
3189 is_file_checks.push(fs.is_file(path))
3190 }
3191 let mut has_files_to_open = false;
3192 while let Some(is_file) = is_file_checks.next().await {
3193 if is_file {
3194 has_files_to_open = true;
3195 break;
3196 }
3197 }
3198 drop(is_file_checks);
3199 if !has_files_to_open {
3200 split_direction = None;
3201 }
3202
3203 if let Ok(open_task) = workspace.update_in(cx, |workspace, window, cx| {
3204 if let Some(split_direction) = split_direction {
3205 to_pane = workspace.split_pane(to_pane, split_direction, window, cx);
3206 }
3207 workspace.open_paths(
3208 paths,
3209 OpenOptions {
3210 visible: Some(OpenVisible::OnlyDirectories),
3211 ..Default::default()
3212 },
3213 Some(to_pane.downgrade()),
3214 window,
3215 cx,
3216 )
3217 }) {
3218 let opened_items: Vec<_> = open_task.await;
3219 _ = workspace.update(cx, |workspace, cx| {
3220 for item in opened_items.into_iter().flatten() {
3221 if let Err(e) = item {
3222 workspace.show_error(&e, cx);
3223 }
3224 }
3225 });
3226 }
3227 })
3228 .detach();
3229 })
3230 .log_err();
3231 }
3232
3233 pub fn display_nav_history_buttons(&mut self, display: Option<bool>) {
3234 self.display_nav_history_buttons = display;
3235 }
3236
3237 fn pinned_item_ids(&self) -> Vec<EntityId> {
3238 self.items
3239 .iter()
3240 .enumerate()
3241 .filter_map(|(index, item)| {
3242 if self.is_tab_pinned(index) {
3243 return Some(item.item_id());
3244 }
3245
3246 None
3247 })
3248 .collect()
3249 }
3250
3251 fn clean_item_ids(&self, cx: &mut Context<Pane>) -> Vec<EntityId> {
3252 self.items()
3253 .filter_map(|item| {
3254 if !item.is_dirty(cx) {
3255 return Some(item.item_id());
3256 }
3257
3258 None
3259 })
3260 .collect()
3261 }
3262
3263 fn to_the_side_item_ids(&self, item_id: EntityId, side: Side) -> Vec<EntityId> {
3264 match side {
3265 Side::Left => self
3266 .items()
3267 .take_while(|item| item.item_id() != item_id)
3268 .map(|item| item.item_id())
3269 .collect(),
3270 Side::Right => self
3271 .items()
3272 .rev()
3273 .take_while(|item| item.item_id() != item_id)
3274 .map(|item| item.item_id())
3275 .collect(),
3276 }
3277 }
3278
3279 pub fn drag_split_direction(&self) -> Option<SplitDirection> {
3280 self.drag_split_direction
3281 }
3282
3283 pub fn set_zoom_out_on_close(&mut self, zoom_out_on_close: bool) {
3284 self.zoom_out_on_close = zoom_out_on_close;
3285 }
3286}
3287
3288fn default_render_tab_bar_buttons(
3289 pane: &mut Pane,
3290 window: &mut Window,
3291 cx: &mut Context<Pane>,
3292) -> (Option<AnyElement>, Option<AnyElement>) {
3293 if !pane.has_focus(window, cx) && !pane.context_menu_focused(window, cx) {
3294 return (None, None);
3295 }
3296 // Ideally we would return a vec of elements here to pass directly to the [TabBar]'s
3297 // `end_slot`, but due to needing a view here that isn't possible.
3298 let right_children = h_flex()
3299 // Instead we need to replicate the spacing from the [TabBar]'s `end_slot` here.
3300 .gap(DynamicSpacing::Base04.rems(cx))
3301 .child(
3302 PopoverMenu::new("pane-tab-bar-popover-menu")
3303 .trigger_with_tooltip(
3304 IconButton::new("plus", IconName::Plus).icon_size(IconSize::Small),
3305 Tooltip::text("New..."),
3306 )
3307 .anchor(Corner::TopRight)
3308 .with_handle(pane.new_item_context_menu_handle.clone())
3309 .menu(move |window, cx| {
3310 Some(ContextMenu::build(window, cx, |menu, _, _| {
3311 menu.action("New File", NewFile.boxed_clone())
3312 .action("Open File", ToggleFileFinder::default().boxed_clone())
3313 .separator()
3314 .action(
3315 "Search Project",
3316 DeploySearch {
3317 replace_enabled: false,
3318 included_files: None,
3319 excluded_files: None,
3320 }
3321 .boxed_clone(),
3322 )
3323 .action("Search Symbols", ToggleProjectSymbols.boxed_clone())
3324 .separator()
3325 .action("New Terminal", NewTerminal.boxed_clone())
3326 }))
3327 }),
3328 )
3329 .child(
3330 PopoverMenu::new("pane-tab-bar-split")
3331 .trigger_with_tooltip(
3332 IconButton::new("split", IconName::Split).icon_size(IconSize::Small),
3333 Tooltip::text("Split Pane"),
3334 )
3335 .anchor(Corner::TopRight)
3336 .with_handle(pane.split_item_context_menu_handle.clone())
3337 .menu(move |window, cx| {
3338 ContextMenu::build(window, cx, |menu, _, _| {
3339 menu.action("Split Right", SplitRight.boxed_clone())
3340 .action("Split Left", SplitLeft.boxed_clone())
3341 .action("Split Up", SplitUp.boxed_clone())
3342 .action("Split Down", SplitDown.boxed_clone())
3343 })
3344 .into()
3345 }),
3346 )
3347 .child({
3348 let zoomed = pane.is_zoomed();
3349 IconButton::new("toggle_zoom", IconName::Maximize)
3350 .icon_size(IconSize::Small)
3351 .toggle_state(zoomed)
3352 .selected_icon(IconName::Minimize)
3353 .on_click(cx.listener(|pane, _, window, cx| {
3354 pane.toggle_zoom(&crate::ToggleZoom, window, cx);
3355 }))
3356 .tooltip(move |window, cx| {
3357 Tooltip::for_action(
3358 if zoomed { "Zoom Out" } else { "Zoom In" },
3359 &ToggleZoom,
3360 window,
3361 cx,
3362 )
3363 })
3364 })
3365 .into_any_element()
3366 .into();
3367 (None, right_children)
3368}
3369
3370impl Focusable for Pane {
3371 fn focus_handle(&self, _cx: &App) -> FocusHandle {
3372 self.focus_handle.clone()
3373 }
3374}
3375
3376impl Render for Pane {
3377 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3378 let mut key_context = KeyContext::new_with_defaults();
3379 key_context.add("Pane");
3380 if self.active_item().is_none() {
3381 key_context.add("EmptyPane");
3382 }
3383
3384 let should_display_tab_bar = self.should_display_tab_bar.clone();
3385 let display_tab_bar = should_display_tab_bar(window, cx);
3386 let Some(project) = self.project.upgrade() else {
3387 return div().track_focus(&self.focus_handle(cx));
3388 };
3389 let is_local = project.read(cx).is_local();
3390
3391 v_flex()
3392 .key_context(key_context)
3393 .track_focus(&self.focus_handle(cx))
3394 .size_full()
3395 .flex_none()
3396 .overflow_hidden()
3397 .on_action(cx.listener(|pane, _: &AlternateFile, window, cx| {
3398 pane.alternate_file(window, cx);
3399 }))
3400 .on_action(
3401 cx.listener(|pane, _: &SplitLeft, _, cx| pane.split(SplitDirection::Left, cx)),
3402 )
3403 .on_action(cx.listener(|pane, _: &SplitUp, _, cx| pane.split(SplitDirection::Up, cx)))
3404 .on_action(cx.listener(|pane, _: &SplitHorizontal, _, cx| {
3405 pane.split(SplitDirection::horizontal(cx), cx)
3406 }))
3407 .on_action(cx.listener(|pane, _: &SplitVertical, _, cx| {
3408 pane.split(SplitDirection::vertical(cx), cx)
3409 }))
3410 .on_action(
3411 cx.listener(|pane, _: &SplitRight, _, cx| pane.split(SplitDirection::Right, cx)),
3412 )
3413 .on_action(
3414 cx.listener(|pane, _: &SplitDown, _, cx| pane.split(SplitDirection::Down, cx)),
3415 )
3416 .on_action(
3417 cx.listener(|pane, _: &GoBack, window, cx| pane.navigate_backward(window, cx)),
3418 )
3419 .on_action(
3420 cx.listener(|pane, _: &GoForward, window, cx| pane.navigate_forward(window, cx)),
3421 )
3422 .on_action(cx.listener(|_, _: &JoinIntoNext, _, cx| {
3423 cx.emit(Event::JoinIntoNext);
3424 }))
3425 .on_action(cx.listener(|_, _: &JoinAll, _, cx| {
3426 cx.emit(Event::JoinAll);
3427 }))
3428 .on_action(cx.listener(Pane::toggle_zoom))
3429 .on_action(
3430 cx.listener(|pane: &mut Pane, action: &ActivateItem, window, cx| {
3431 pane.activate_item(
3432 action.0.min(pane.items.len().saturating_sub(1)),
3433 true,
3434 true,
3435 window,
3436 cx,
3437 );
3438 }),
3439 )
3440 .on_action(
3441 cx.listener(|pane: &mut Pane, _: &ActivateLastItem, window, cx| {
3442 pane.activate_item(pane.items.len().saturating_sub(1), true, true, window, cx);
3443 }),
3444 )
3445 .on_action(
3446 cx.listener(|pane: &mut Pane, _: &ActivatePreviousItem, window, cx| {
3447 pane.activate_prev_item(true, window, cx);
3448 }),
3449 )
3450 .on_action(
3451 cx.listener(|pane: &mut Pane, _: &ActivateNextItem, window, cx| {
3452 pane.activate_next_item(true, window, cx);
3453 }),
3454 )
3455 .on_action(
3456 cx.listener(|pane, _: &SwapItemLeft, window, cx| pane.swap_item_left(window, cx)),
3457 )
3458 .on_action(
3459 cx.listener(|pane, _: &SwapItemRight, window, cx| pane.swap_item_right(window, cx)),
3460 )
3461 .on_action(cx.listener(|pane, action, window, cx| {
3462 pane.toggle_pin_tab(action, window, cx);
3463 }))
3464 .on_action(cx.listener(|pane, action, window, cx| {
3465 pane.unpin_all_tabs(action, window, cx);
3466 }))
3467 .when(PreviewTabsSettings::get_global(cx).enabled, |this| {
3468 this.on_action(cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, _, cx| {
3469 if let Some(active_item_id) = pane.active_item().map(|i| i.item_id()) {
3470 if pane.is_active_preview_item(active_item_id) {
3471 pane.set_preview_item_id(None, cx);
3472 } else {
3473 pane.set_preview_item_id(Some(active_item_id), cx);
3474 }
3475 }
3476 }))
3477 })
3478 .on_action(
3479 cx.listener(|pane: &mut Self, action: &CloseActiveItem, window, cx| {
3480 pane.close_active_item(action, window, cx)
3481 .detach_and_log_err(cx)
3482 }),
3483 )
3484 .on_action(
3485 cx.listener(|pane: &mut Self, action: &CloseInactiveItems, window, cx| {
3486 pane.close_inactive_items(action, window, cx)
3487 .detach_and_log_err(cx);
3488 }),
3489 )
3490 .on_action(
3491 cx.listener(|pane: &mut Self, action: &CloseCleanItems, window, cx| {
3492 pane.close_clean_items(action, window, cx)
3493 .detach_and_log_err(cx)
3494 }),
3495 )
3496 .on_action(cx.listener(
3497 |pane: &mut Self, action: &CloseItemsToTheLeft, window, cx| {
3498 pane.close_items_to_the_left_by_id(None, action, window, cx)
3499 .detach_and_log_err(cx)
3500 },
3501 ))
3502 .on_action(cx.listener(
3503 |pane: &mut Self, action: &CloseItemsToTheRight, window, cx| {
3504 pane.close_items_to_the_right_by_id(None, action, window, cx)
3505 .detach_and_log_err(cx)
3506 },
3507 ))
3508 .on_action(
3509 cx.listener(|pane: &mut Self, action: &CloseAllItems, window, cx| {
3510 pane.close_all_items(action, window, cx)
3511 .detach_and_log_err(cx)
3512 }),
3513 )
3514 .on_action(
3515 cx.listener(|pane: &mut Self, action: &RevealInProjectPanel, _, cx| {
3516 let entry_id = action
3517 .entry_id
3518 .map(ProjectEntryId::from_proto)
3519 .or_else(|| pane.active_item()?.project_entry_ids(cx).first().copied());
3520 if let Some(entry_id) = entry_id {
3521 pane.project
3522 .update(cx, |_, cx| {
3523 cx.emit(project::Event::RevealInProjectPanel(entry_id))
3524 })
3525 .ok();
3526 }
3527 }),
3528 )
3529 .on_action(cx.listener(|_, _: &menu::Cancel, window, cx| {
3530 if cx.stop_active_drag(window) {
3531 return;
3532 } else {
3533 cx.propagate();
3534 }
3535 }))
3536 .when(self.active_item().is_some() && display_tab_bar, |pane| {
3537 pane.child((self.render_tab_bar.clone())(self, window, cx))
3538 })
3539 .child({
3540 let has_worktrees = project.read(cx).visible_worktrees(cx).next().is_some();
3541 // main content
3542 div()
3543 .flex_1()
3544 .relative()
3545 .group("")
3546 .overflow_hidden()
3547 .on_drag_move::<DraggedTab>(cx.listener(Self::handle_drag_move))
3548 .on_drag_move::<DraggedSelection>(cx.listener(Self::handle_drag_move))
3549 .when(is_local, |div| {
3550 div.on_drag_move::<ExternalPaths>(cx.listener(Self::handle_drag_move))
3551 })
3552 .map(|div| {
3553 if let Some(item) = self.active_item() {
3554 div.id("pane_placeholder")
3555 .v_flex()
3556 .size_full()
3557 .overflow_hidden()
3558 .child(self.toolbar.clone())
3559 .child(item.to_any())
3560 } else {
3561 let placeholder = div
3562 .id("pane_placeholder")
3563 .h_flex()
3564 .size_full()
3565 .justify_center()
3566 .on_click(cx.listener(
3567 move |this, event: &ClickEvent, window, cx| {
3568 if event.up.click_count == 2 {
3569 window.dispatch_action(
3570 this.double_click_dispatch_action.boxed_clone(),
3571 cx,
3572 );
3573 }
3574 },
3575 ));
3576 if has_worktrees {
3577 placeholder
3578 } else {
3579 placeholder.child(
3580 Label::new("Open a file or project to get started.")
3581 .color(Color::Muted),
3582 )
3583 }
3584 }
3585 })
3586 .child(
3587 // drag target
3588 div()
3589 .invisible()
3590 .absolute()
3591 .bg(cx.theme().colors().drop_target_background)
3592 .group_drag_over::<DraggedTab>("", |style| style.visible())
3593 .group_drag_over::<DraggedSelection>("", |style| style.visible())
3594 .when(is_local, |div| {
3595 div.group_drag_over::<ExternalPaths>("", |style| style.visible())
3596 })
3597 .when_some(self.can_drop_predicate.clone(), |this, p| {
3598 this.can_drop(move |a, window, cx| p(a, window, cx))
3599 })
3600 .on_drop(cx.listener(move |this, dragged_tab, window, cx| {
3601 this.handle_tab_drop(
3602 dragged_tab,
3603 this.active_item_index(),
3604 window,
3605 cx,
3606 )
3607 }))
3608 .on_drop(cx.listener(
3609 move |this, selection: &DraggedSelection, window, cx| {
3610 this.handle_dragged_selection_drop(selection, None, window, cx)
3611 },
3612 ))
3613 .on_drop(cx.listener(move |this, paths, window, cx| {
3614 this.handle_external_paths_drop(paths, window, cx)
3615 }))
3616 .map(|div| {
3617 let size = DefiniteLength::Fraction(0.5);
3618 match self.drag_split_direction {
3619 None => div.top_0().right_0().bottom_0().left_0(),
3620 Some(SplitDirection::Up) => {
3621 div.top_0().left_0().right_0().h(size)
3622 }
3623 Some(SplitDirection::Down) => {
3624 div.left_0().bottom_0().right_0().h(size)
3625 }
3626 Some(SplitDirection::Left) => {
3627 div.top_0().left_0().bottom_0().w(size)
3628 }
3629 Some(SplitDirection::Right) => {
3630 div.top_0().bottom_0().right_0().w(size)
3631 }
3632 }
3633 }),
3634 )
3635 })
3636 .on_mouse_down(
3637 MouseButton::Navigate(NavigationDirection::Back),
3638 cx.listener(|pane, _, window, cx| {
3639 if let Some(workspace) = pane.workspace.upgrade() {
3640 let pane = cx.entity().downgrade();
3641 window.defer(cx, move |window, cx| {
3642 workspace.update(cx, |workspace, cx| {
3643 workspace.go_back(pane, window, cx).detach_and_log_err(cx)
3644 })
3645 })
3646 }
3647 }),
3648 )
3649 .on_mouse_down(
3650 MouseButton::Navigate(NavigationDirection::Forward),
3651 cx.listener(|pane, _, window, cx| {
3652 if let Some(workspace) = pane.workspace.upgrade() {
3653 let pane = cx.entity().downgrade();
3654 window.defer(cx, move |window, cx| {
3655 workspace.update(cx, |workspace, cx| {
3656 workspace
3657 .go_forward(pane, window, cx)
3658 .detach_and_log_err(cx)
3659 })
3660 })
3661 }
3662 }),
3663 )
3664 }
3665}
3666
3667impl ItemNavHistory {
3668 pub fn push<D: 'static + Send + Any>(&mut self, data: Option<D>, cx: &mut App) {
3669 if self
3670 .item
3671 .upgrade()
3672 .is_some_and(|item| item.include_in_nav_history())
3673 {
3674 self.history
3675 .push(data, self.item.clone(), self.is_preview, cx);
3676 }
3677 }
3678
3679 pub fn pop_backward(&mut self, cx: &mut App) -> Option<NavigationEntry> {
3680 self.history.pop(NavigationMode::GoingBack, cx)
3681 }
3682
3683 pub fn pop_forward(&mut self, cx: &mut App) -> Option<NavigationEntry> {
3684 self.history.pop(NavigationMode::GoingForward, cx)
3685 }
3686}
3687
3688impl NavHistory {
3689 pub fn for_each_entry(
3690 &self,
3691 cx: &App,
3692 mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
3693 ) {
3694 let borrowed_history = self.0.lock();
3695 borrowed_history
3696 .forward_stack
3697 .iter()
3698 .chain(borrowed_history.backward_stack.iter())
3699 .chain(borrowed_history.closed_stack.iter())
3700 .for_each(|entry| {
3701 if let Some(project_and_abs_path) =
3702 borrowed_history.paths_by_item.get(&entry.item.id())
3703 {
3704 f(entry, project_and_abs_path.clone());
3705 } else if let Some(item) = entry.item.upgrade() {
3706 if let Some(path) = item.project_path(cx) {
3707 f(entry, (path, None));
3708 }
3709 }
3710 })
3711 }
3712
3713 pub fn set_mode(&mut self, mode: NavigationMode) {
3714 self.0.lock().mode = mode;
3715 }
3716
3717 pub fn mode(&self) -> NavigationMode {
3718 self.0.lock().mode
3719 }
3720
3721 pub fn disable(&mut self) {
3722 self.0.lock().mode = NavigationMode::Disabled;
3723 }
3724
3725 pub fn enable(&mut self) {
3726 self.0.lock().mode = NavigationMode::Normal;
3727 }
3728
3729 pub fn pop(&mut self, mode: NavigationMode, cx: &mut App) -> Option<NavigationEntry> {
3730 let mut state = self.0.lock();
3731 let entry = match mode {
3732 NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
3733 return None;
3734 }
3735 NavigationMode::GoingBack => &mut state.backward_stack,
3736 NavigationMode::GoingForward => &mut state.forward_stack,
3737 NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
3738 }
3739 .pop_back();
3740 if entry.is_some() {
3741 state.did_update(cx);
3742 }
3743 entry
3744 }
3745
3746 pub fn push<D: 'static + Send + Any>(
3747 &mut self,
3748 data: Option<D>,
3749 item: Arc<dyn WeakItemHandle>,
3750 is_preview: bool,
3751 cx: &mut App,
3752 ) {
3753 let state = &mut *self.0.lock();
3754 match state.mode {
3755 NavigationMode::Disabled => {}
3756 NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
3757 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3758 state.backward_stack.pop_front();
3759 }
3760 state.backward_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 state.forward_stack.clear();
3767 }
3768 NavigationMode::GoingBack => {
3769 if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3770 state.forward_stack.pop_front();
3771 }
3772 state.forward_stack.push_back(NavigationEntry {
3773 item,
3774 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3775 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3776 is_preview,
3777 });
3778 }
3779 NavigationMode::GoingForward => {
3780 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3781 state.backward_stack.pop_front();
3782 }
3783 state.backward_stack.push_back(NavigationEntry {
3784 item,
3785 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3786 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3787 is_preview,
3788 });
3789 }
3790 NavigationMode::ClosingItem => {
3791 if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3792 state.closed_stack.pop_front();
3793 }
3794 state.closed_stack.push_back(NavigationEntry {
3795 item,
3796 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3797 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3798 is_preview,
3799 });
3800 }
3801 }
3802 state.did_update(cx);
3803 }
3804
3805 pub fn remove_item(&mut self, item_id: EntityId) {
3806 let mut state = self.0.lock();
3807 state.paths_by_item.remove(&item_id);
3808 state
3809 .backward_stack
3810 .retain(|entry| entry.item.id() != item_id);
3811 state
3812 .forward_stack
3813 .retain(|entry| entry.item.id() != item_id);
3814 state
3815 .closed_stack
3816 .retain(|entry| entry.item.id() != item_id);
3817 }
3818
3819 pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option<PathBuf>)> {
3820 self.0.lock().paths_by_item.get(&item_id).cloned()
3821 }
3822}
3823
3824impl NavHistoryState {
3825 pub fn did_update(&self, cx: &mut App) {
3826 if let Some(pane) = self.pane.upgrade() {
3827 cx.defer(move |cx| {
3828 pane.update(cx, |pane, cx| pane.history_updated(cx));
3829 });
3830 }
3831 }
3832}
3833
3834fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
3835 let path = buffer_path
3836 .as_ref()
3837 .and_then(|p| {
3838 p.path
3839 .to_str()
3840 .and_then(|s| if s.is_empty() { None } else { Some(s) })
3841 })
3842 .unwrap_or("This buffer");
3843 let path = truncate_and_remove_front(path, 80);
3844 format!("{path} contains unsaved edits. Do you want to save it?")
3845}
3846
3847pub fn tab_details(items: &[Box<dyn ItemHandle>], _window: &Window, cx: &App) -> Vec<usize> {
3848 let mut tab_details = items.iter().map(|_| 0).collect::<Vec<_>>();
3849 let mut tab_descriptions = HashMap::default();
3850 let mut done = false;
3851 while !done {
3852 done = true;
3853
3854 // Store item indices by their tab description.
3855 for (ix, (item, detail)) in items.iter().zip(&tab_details).enumerate() {
3856 let description = item.tab_content_text(*detail, cx);
3857 if *detail == 0 || description != item.tab_content_text(detail - 1, cx) {
3858 tab_descriptions
3859 .entry(description)
3860 .or_insert(Vec::new())
3861 .push(ix);
3862 }
3863 }
3864
3865 // If two or more items have the same tab description, increase their level
3866 // of detail and try again.
3867 for (_, item_ixs) in tab_descriptions.drain() {
3868 if item_ixs.len() > 1 {
3869 done = false;
3870 for ix in item_ixs {
3871 tab_details[ix] += 1;
3872 }
3873 }
3874 }
3875 }
3876
3877 tab_details
3878}
3879
3880pub fn render_item_indicator(item: Box<dyn ItemHandle>, cx: &App) -> Option<Indicator> {
3881 maybe!({
3882 let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
3883 (true, _) => Color::Warning,
3884 (_, true) => Color::Accent,
3885 (false, false) => return None,
3886 };
3887
3888 Some(Indicator::dot().color(indicator_color))
3889 })
3890}
3891
3892impl Render for DraggedTab {
3893 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3894 let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
3895 let label = self.item.tab_content(
3896 TabContentParams {
3897 detail: Some(self.detail),
3898 selected: false,
3899 preview: false,
3900 deemphasized: false,
3901 },
3902 window,
3903 cx,
3904 );
3905 Tab::new("")
3906 .toggle_state(self.is_active)
3907 .child(label)
3908 .render(window, cx)
3909 .font(ui_font)
3910 }
3911}
3912
3913#[cfg(test)]
3914mod tests {
3915 use std::num::NonZero;
3916
3917 use super::*;
3918 use crate::item::test::{TestItem, TestProjectItem};
3919 use gpui::{TestAppContext, VisualTestContext};
3920 use project::FakeFs;
3921 use settings::SettingsStore;
3922 use theme::LoadThemes;
3923 use util::TryFutureExt;
3924
3925 #[gpui::test]
3926 async fn test_add_item_capped_to_max_tabs(cx: &mut TestAppContext) {
3927 init_test(cx);
3928 let fs = FakeFs::new(cx.executor());
3929
3930 let project = Project::test(fs, None, cx).await;
3931 let (workspace, cx) =
3932 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3933 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3934
3935 for i in 0..7 {
3936 add_labeled_item(&pane, format!("{}", i).as_str(), false, cx);
3937 }
3938
3939 set_max_tabs(cx, Some(5));
3940 add_labeled_item(&pane, "7", false, cx);
3941 // Remove items to respect the max tab cap.
3942 assert_item_labels(&pane, ["3", "4", "5", "6", "7*"], cx);
3943 pane.update_in(cx, |pane, window, cx| {
3944 pane.activate_item(0, false, false, window, cx);
3945 });
3946 add_labeled_item(&pane, "X", false, cx);
3947 // Respect activation order.
3948 assert_item_labels(&pane, ["3", "X*", "5", "6", "7"], cx);
3949
3950 for i in 0..7 {
3951 add_labeled_item(&pane, format!("D{}", i).as_str(), true, cx);
3952 }
3953 // Keeps dirty items, even over max tab cap.
3954 assert_item_labels(
3955 &pane,
3956 ["D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6*^"],
3957 cx,
3958 );
3959
3960 set_max_tabs(cx, None);
3961 for i in 0..7 {
3962 add_labeled_item(&pane, format!("N{}", i).as_str(), false, cx);
3963 }
3964 // No cap when max tabs is None.
3965 assert_item_labels(
3966 &pane,
3967 [
3968 "D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6^", "N0", "N1", "N2", "N3", "N4",
3969 "N5", "N6*",
3970 ],
3971 cx,
3972 );
3973 }
3974
3975 #[gpui::test]
3976 async fn test_reduce_max_tabs_closes_existing_items(cx: &mut TestAppContext) {
3977 init_test(cx);
3978 let fs = FakeFs::new(cx.executor());
3979
3980 let project = Project::test(fs, None, cx).await;
3981 let (workspace, cx) =
3982 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3983 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3984
3985 add_labeled_item(&pane, "A", false, cx);
3986 add_labeled_item(&pane, "B", false, cx);
3987 let item_c = add_labeled_item(&pane, "C", false, cx);
3988 let item_d = add_labeled_item(&pane, "D", false, cx);
3989 add_labeled_item(&pane, "E", false, cx);
3990 add_labeled_item(&pane, "Settings", false, cx);
3991 assert_item_labels(&pane, ["A", "B", "C", "D", "E", "Settings*"], cx);
3992
3993 set_max_tabs(cx, Some(5));
3994 assert_item_labels(&pane, ["B", "C", "D", "E", "Settings*"], cx);
3995
3996 set_max_tabs(cx, Some(4));
3997 assert_item_labels(&pane, ["C", "D", "E", "Settings*"], cx);
3998
3999 pane.update_in(cx, |pane, window, cx| {
4000 let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4001 pane.pin_tab_at(ix, window, cx);
4002
4003 let ix = pane.index_for_item_id(item_d.item_id()).unwrap();
4004 pane.pin_tab_at(ix, window, cx);
4005 });
4006 assert_item_labels(&pane, ["C!", "D!", "E", "Settings*"], cx);
4007
4008 set_max_tabs(cx, Some(2));
4009 assert_item_labels(&pane, ["C!", "D!", "Settings*"], cx);
4010 }
4011
4012 #[gpui::test]
4013 async fn test_allow_pinning_dirty_item_at_max_tabs(cx: &mut TestAppContext) {
4014 init_test(cx);
4015 let fs = FakeFs::new(cx.executor());
4016
4017 let project = Project::test(fs, None, cx).await;
4018 let (workspace, cx) =
4019 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4020 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4021
4022 set_max_tabs(cx, Some(1));
4023 let item_a = add_labeled_item(&pane, "A", true, cx);
4024
4025 pane.update_in(cx, |pane, window, cx| {
4026 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4027 pane.pin_tab_at(ix, window, cx);
4028 });
4029 assert_item_labels(&pane, ["A*^!"], cx);
4030 }
4031
4032 #[gpui::test]
4033 async fn test_allow_pinning_non_dirty_item_at_max_tabs(cx: &mut TestAppContext) {
4034 init_test(cx);
4035 let fs = FakeFs::new(cx.executor());
4036
4037 let project = Project::test(fs, None, cx).await;
4038 let (workspace, cx) =
4039 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4040 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4041
4042 set_max_tabs(cx, Some(1));
4043 let item_a = add_labeled_item(&pane, "A", false, cx);
4044
4045 pane.update_in(cx, |pane, window, cx| {
4046 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4047 pane.pin_tab_at(ix, window, cx);
4048 });
4049 assert_item_labels(&pane, ["A*!"], cx);
4050 }
4051
4052 #[gpui::test]
4053 async fn test_pin_tabs_incrementally_at_max_capacity(cx: &mut TestAppContext) {
4054 init_test(cx);
4055 let fs = FakeFs::new(cx.executor());
4056
4057 let project = Project::test(fs, None, cx).await;
4058 let (workspace, cx) =
4059 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4060 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4061
4062 set_max_tabs(cx, Some(3));
4063
4064 let item_a = add_labeled_item(&pane, "A", false, cx);
4065 assert_item_labels(&pane, ["A*"], cx);
4066
4067 pane.update_in(cx, |pane, window, cx| {
4068 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4069 pane.pin_tab_at(ix, window, cx);
4070 });
4071 assert_item_labels(&pane, ["A*!"], cx);
4072
4073 let item_b = add_labeled_item(&pane, "B", false, cx);
4074 assert_item_labels(&pane, ["A!", "B*"], cx);
4075
4076 pane.update_in(cx, |pane, window, cx| {
4077 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4078 pane.pin_tab_at(ix, window, cx);
4079 });
4080 assert_item_labels(&pane, ["A!", "B*!"], cx);
4081
4082 let item_c = add_labeled_item(&pane, "C", false, cx);
4083 assert_item_labels(&pane, ["A!", "B!", "C*"], cx);
4084
4085 pane.update_in(cx, |pane, window, cx| {
4086 let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4087 pane.pin_tab_at(ix, window, cx);
4088 });
4089 assert_item_labels(&pane, ["A!", "B!", "C*!"], cx);
4090 }
4091
4092 #[gpui::test]
4093 async fn test_pin_tabs_left_to_right_after_opening_at_max_capacity(cx: &mut TestAppContext) {
4094 init_test(cx);
4095 let fs = FakeFs::new(cx.executor());
4096
4097 let project = Project::test(fs, None, cx).await;
4098 let (workspace, cx) =
4099 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4100 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4101
4102 set_max_tabs(cx, Some(3));
4103
4104 let item_a = add_labeled_item(&pane, "A", false, cx);
4105 assert_item_labels(&pane, ["A*"], cx);
4106
4107 let item_b = add_labeled_item(&pane, "B", false, cx);
4108 assert_item_labels(&pane, ["A", "B*"], cx);
4109
4110 let item_c = add_labeled_item(&pane, "C", false, cx);
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_a.item_id()).unwrap();
4115 pane.pin_tab_at(ix, window, cx);
4116 });
4117 assert_item_labels(&pane, ["A!", "B", "C*"], cx);
4118
4119 pane.update_in(cx, |pane, window, cx| {
4120 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4121 pane.pin_tab_at(ix, window, cx);
4122 });
4123 assert_item_labels(&pane, ["A!", "B!", "C*"], cx);
4124
4125 pane.update_in(cx, |pane, window, cx| {
4126 let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4127 pane.pin_tab_at(ix, window, cx);
4128 });
4129 assert_item_labels(&pane, ["A!", "B!", "C*!"], cx);
4130 }
4131
4132 #[gpui::test]
4133 async fn test_pin_tabs_right_to_left_after_opening_at_max_capacity(cx: &mut TestAppContext) {
4134 init_test(cx);
4135 let fs = FakeFs::new(cx.executor());
4136
4137 let project = Project::test(fs, None, cx).await;
4138 let (workspace, cx) =
4139 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4140 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4141
4142 set_max_tabs(cx, Some(3));
4143
4144 let item_a = add_labeled_item(&pane, "A", false, cx);
4145 assert_item_labels(&pane, ["A*"], cx);
4146
4147 let item_b = add_labeled_item(&pane, "B", false, cx);
4148 assert_item_labels(&pane, ["A", "B*"], cx);
4149
4150 let item_c = add_labeled_item(&pane, "C", false, cx);
4151 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4152
4153 pane.update_in(cx, |pane, window, cx| {
4154 let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4155 pane.pin_tab_at(ix, window, cx);
4156 });
4157 assert_item_labels(&pane, ["C*!", "A", "B"], cx);
4158
4159 pane.update_in(cx, |pane, window, cx| {
4160 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4161 pane.pin_tab_at(ix, window, cx);
4162 });
4163 assert_item_labels(&pane, ["C*!", "B!", "A"], cx);
4164
4165 pane.update_in(cx, |pane, window, cx| {
4166 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4167 pane.pin_tab_at(ix, window, cx);
4168 });
4169 assert_item_labels(&pane, ["C*!", "B!", "A!"], cx);
4170 }
4171
4172 #[gpui::test]
4173 async fn test_pinned_tabs_never_closed_at_max_tabs(cx: &mut TestAppContext) {
4174 init_test(cx);
4175 let fs = FakeFs::new(cx.executor());
4176
4177 let project = Project::test(fs, None, cx).await;
4178 let (workspace, cx) =
4179 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4180 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4181
4182 let item_a = add_labeled_item(&pane, "A", false, cx);
4183 pane.update_in(cx, |pane, window, cx| {
4184 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4185 pane.pin_tab_at(ix, window, cx);
4186 });
4187
4188 let item_b = add_labeled_item(&pane, "B", false, cx);
4189 pane.update_in(cx, |pane, window, cx| {
4190 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4191 pane.pin_tab_at(ix, window, cx);
4192 });
4193
4194 add_labeled_item(&pane, "C", false, cx);
4195 add_labeled_item(&pane, "D", false, cx);
4196 add_labeled_item(&pane, "E", false, cx);
4197 assert_item_labels(&pane, ["A!", "B!", "C", "D", "E*"], cx);
4198
4199 set_max_tabs(cx, Some(3));
4200 add_labeled_item(&pane, "F", false, cx);
4201 assert_item_labels(&pane, ["A!", "B!", "F*"], cx);
4202
4203 add_labeled_item(&pane, "G", false, cx);
4204 assert_item_labels(&pane, ["A!", "B!", "G*"], cx);
4205
4206 add_labeled_item(&pane, "H", false, cx);
4207 assert_item_labels(&pane, ["A!", "B!", "H*"], cx);
4208 }
4209
4210 #[gpui::test]
4211 async fn test_always_allows_one_unpinned_item_over_max_tabs_regardless_of_pinned_count(
4212 cx: &mut TestAppContext,
4213 ) {
4214 init_test(cx);
4215 let fs = FakeFs::new(cx.executor());
4216
4217 let project = Project::test(fs, None, cx).await;
4218 let (workspace, cx) =
4219 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4220 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4221
4222 set_max_tabs(cx, Some(3));
4223
4224 let item_a = add_labeled_item(&pane, "A", false, cx);
4225 pane.update_in(cx, |pane, window, cx| {
4226 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4227 pane.pin_tab_at(ix, window, cx);
4228 });
4229
4230 let item_b = add_labeled_item(&pane, "B", false, cx);
4231 pane.update_in(cx, |pane, window, cx| {
4232 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4233 pane.pin_tab_at(ix, window, cx);
4234 });
4235
4236 let item_c = add_labeled_item(&pane, "C", false, cx);
4237 pane.update_in(cx, |pane, window, cx| {
4238 let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4239 pane.pin_tab_at(ix, window, cx);
4240 });
4241
4242 assert_item_labels(&pane, ["A!", "B!", "C*!"], cx);
4243
4244 let item_d = add_labeled_item(&pane, "D", false, cx);
4245 assert_item_labels(&pane, ["A!", "B!", "C!", "D*"], cx);
4246
4247 pane.update_in(cx, |pane, window, cx| {
4248 let ix = pane.index_for_item_id(item_d.item_id()).unwrap();
4249 pane.pin_tab_at(ix, window, cx);
4250 });
4251 assert_item_labels(&pane, ["A!", "B!", "C!", "D*!"], cx);
4252
4253 add_labeled_item(&pane, "E", false, cx);
4254 assert_item_labels(&pane, ["A!", "B!", "C!", "D!", "E*"], cx);
4255
4256 add_labeled_item(&pane, "F", false, cx);
4257 assert_item_labels(&pane, ["A!", "B!", "C!", "D!", "F*"], cx);
4258 }
4259
4260 #[gpui::test]
4261 async fn test_can_open_one_item_when_all_tabs_are_dirty_at_max(cx: &mut TestAppContext) {
4262 init_test(cx);
4263 let fs = FakeFs::new(cx.executor());
4264
4265 let project = Project::test(fs, None, cx).await;
4266 let (workspace, cx) =
4267 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4268 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4269
4270 set_max_tabs(cx, Some(3));
4271
4272 add_labeled_item(&pane, "A", true, cx);
4273 assert_item_labels(&pane, ["A*^"], cx);
4274
4275 add_labeled_item(&pane, "B", true, cx);
4276 assert_item_labels(&pane, ["A^", "B*^"], cx);
4277
4278 add_labeled_item(&pane, "C", true, cx);
4279 assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
4280
4281 add_labeled_item(&pane, "D", false, cx);
4282 assert_item_labels(&pane, ["A^", "B^", "C^", "D*"], cx);
4283
4284 add_labeled_item(&pane, "E", false, cx);
4285 assert_item_labels(&pane, ["A^", "B^", "C^", "E*"], cx);
4286
4287 add_labeled_item(&pane, "F", false, cx);
4288 assert_item_labels(&pane, ["A^", "B^", "C^", "F*"], cx);
4289
4290 add_labeled_item(&pane, "G", true, cx);
4291 assert_item_labels(&pane, ["A^", "B^", "C^", "G*^"], cx);
4292 }
4293
4294 #[gpui::test]
4295 async fn test_toggle_pin_tab(cx: &mut TestAppContext) {
4296 init_test(cx);
4297 let fs = FakeFs::new(cx.executor());
4298
4299 let project = Project::test(fs, None, cx).await;
4300 let (workspace, cx) =
4301 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4302 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4303
4304 set_labeled_items(&pane, ["A", "B*", "C"], cx);
4305 assert_item_labels(&pane, ["A", "B*", "C"], cx);
4306
4307 pane.update_in(cx, |pane, window, cx| {
4308 pane.toggle_pin_tab(&TogglePinTab, window, cx);
4309 });
4310 assert_item_labels(&pane, ["B*!", "A", "C"], cx);
4311
4312 pane.update_in(cx, |pane, window, cx| {
4313 pane.toggle_pin_tab(&TogglePinTab, window, cx);
4314 });
4315 assert_item_labels(&pane, ["B*", "A", "C"], cx);
4316 }
4317
4318 #[gpui::test]
4319 async fn test_unpin_all_tabs(cx: &mut TestAppContext) {
4320 init_test(cx);
4321 let fs = FakeFs::new(cx.executor());
4322
4323 let project = Project::test(fs, None, cx).await;
4324 let (workspace, cx) =
4325 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4326 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4327
4328 // Unpin all, in an empty pane
4329 pane.update_in(cx, |pane, window, cx| {
4330 pane.unpin_all_tabs(&UnpinAllTabs, window, cx);
4331 });
4332
4333 assert_item_labels(&pane, [], cx);
4334
4335 let item_a = add_labeled_item(&pane, "A", false, cx);
4336 let item_b = add_labeled_item(&pane, "B", false, cx);
4337 let item_c = add_labeled_item(&pane, "C", false, cx);
4338 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4339
4340 // Unpin all, when no tabs are pinned
4341 pane.update_in(cx, |pane, window, cx| {
4342 pane.unpin_all_tabs(&UnpinAllTabs, window, cx);
4343 });
4344
4345 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4346
4347 // Pin inactive tabs only
4348 pane.update_in(cx, |pane, window, cx| {
4349 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4350 pane.pin_tab_at(ix, window, cx);
4351
4352 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4353 pane.pin_tab_at(ix, window, cx);
4354 });
4355 assert_item_labels(&pane, ["A!", "B!", "C*"], cx);
4356
4357 pane.update_in(cx, |pane, window, cx| {
4358 pane.unpin_all_tabs(&UnpinAllTabs, window, cx);
4359 });
4360
4361 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4362
4363 // Pin all tabs
4364 pane.update_in(cx, |pane, window, cx| {
4365 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4366 pane.pin_tab_at(ix, window, cx);
4367
4368 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4369 pane.pin_tab_at(ix, window, cx);
4370
4371 let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4372 pane.pin_tab_at(ix, window, cx);
4373 });
4374 assert_item_labels(&pane, ["A!", "B!", "C*!"], cx);
4375
4376 // Activate middle tab
4377 pane.update_in(cx, |pane, window, cx| {
4378 pane.activate_item(1, false, false, window, cx);
4379 });
4380 assert_item_labels(&pane, ["A!", "B*!", "C!"], cx);
4381
4382 pane.update_in(cx, |pane, window, cx| {
4383 pane.unpin_all_tabs(&UnpinAllTabs, window, cx);
4384 });
4385
4386 // Order has not changed
4387 assert_item_labels(&pane, ["A", "B*", "C"], cx);
4388 }
4389
4390 #[gpui::test]
4391 async fn test_pinning_active_tab_without_position_change_maintains_focus(
4392 cx: &mut TestAppContext,
4393 ) {
4394 init_test(cx);
4395 let fs = FakeFs::new(cx.executor());
4396
4397 let project = Project::test(fs, None, cx).await;
4398 let (workspace, cx) =
4399 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4400 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4401
4402 // Add A
4403 let item_a = add_labeled_item(&pane, "A", false, cx);
4404 assert_item_labels(&pane, ["A*"], cx);
4405
4406 // Add B
4407 add_labeled_item(&pane, "B", false, cx);
4408 assert_item_labels(&pane, ["A", "B*"], cx);
4409
4410 // Activate A again
4411 pane.update_in(cx, |pane, window, cx| {
4412 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4413 pane.activate_item(ix, true, true, window, cx);
4414 });
4415 assert_item_labels(&pane, ["A*", "B"], cx);
4416
4417 // Pin A - remains active
4418 pane.update_in(cx, |pane, window, cx| {
4419 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4420 pane.pin_tab_at(ix, window, cx);
4421 });
4422 assert_item_labels(&pane, ["A*!", "B"], cx);
4423
4424 // Unpin A - remain active
4425 pane.update_in(cx, |pane, window, cx| {
4426 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4427 pane.unpin_tab_at(ix, window, cx);
4428 });
4429 assert_item_labels(&pane, ["A*", "B"], cx);
4430 }
4431
4432 #[gpui::test]
4433 async fn test_pinning_active_tab_with_position_change_maintains_focus(cx: &mut TestAppContext) {
4434 init_test(cx);
4435 let fs = FakeFs::new(cx.executor());
4436
4437 let project = Project::test(fs, None, cx).await;
4438 let (workspace, cx) =
4439 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4440 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4441
4442 // Add A, B, C
4443 add_labeled_item(&pane, "A", false, cx);
4444 add_labeled_item(&pane, "B", false, cx);
4445 let item_c = add_labeled_item(&pane, "C", false, cx);
4446 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4447
4448 // Pin C - moves to pinned area, remains active
4449 pane.update_in(cx, |pane, window, cx| {
4450 let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4451 pane.pin_tab_at(ix, window, cx);
4452 });
4453 assert_item_labels(&pane, ["C*!", "A", "B"], cx);
4454
4455 // Unpin C - moves after pinned area, remains active
4456 pane.update_in(cx, |pane, window, cx| {
4457 let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4458 pane.unpin_tab_at(ix, window, cx);
4459 });
4460 assert_item_labels(&pane, ["C*", "A", "B"], cx);
4461 }
4462
4463 #[gpui::test]
4464 async fn test_pinning_inactive_tab_without_position_change_preserves_existing_focus(
4465 cx: &mut TestAppContext,
4466 ) {
4467 init_test(cx);
4468 let fs = FakeFs::new(cx.executor());
4469
4470 let project = Project::test(fs, None, cx).await;
4471 let (workspace, cx) =
4472 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4473 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4474
4475 // Add A, B
4476 let item_a = add_labeled_item(&pane, "A", false, cx);
4477 add_labeled_item(&pane, "B", false, cx);
4478 assert_item_labels(&pane, ["A", "B*"], cx);
4479
4480 // Pin A - already in pinned area, B remains active
4481 pane.update_in(cx, |pane, window, cx| {
4482 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4483 pane.pin_tab_at(ix, window, cx);
4484 });
4485 assert_item_labels(&pane, ["A!", "B*"], cx);
4486
4487 // Unpin A - stays in place, B remains active
4488 pane.update_in(cx, |pane, window, cx| {
4489 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4490 pane.unpin_tab_at(ix, window, cx);
4491 });
4492 assert_item_labels(&pane, ["A", "B*"], cx);
4493 }
4494
4495 #[gpui::test]
4496 async fn test_pinning_inactive_tab_with_position_change_preserves_existing_focus(
4497 cx: &mut TestAppContext,
4498 ) {
4499 init_test(cx);
4500 let fs = FakeFs::new(cx.executor());
4501
4502 let project = Project::test(fs, None, cx).await;
4503 let (workspace, cx) =
4504 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4505 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4506
4507 // Add A, B, C
4508 add_labeled_item(&pane, "A", false, cx);
4509 let item_b = add_labeled_item(&pane, "B", false, cx);
4510 let item_c = add_labeled_item(&pane, "C", false, cx);
4511 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4512
4513 // Activate B
4514 pane.update_in(cx, |pane, window, cx| {
4515 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4516 pane.activate_item(ix, true, true, window, cx);
4517 });
4518 assert_item_labels(&pane, ["A", "B*", "C"], cx);
4519
4520 // Pin C - moves to pinned area, B remains active
4521 pane.update_in(cx, |pane, window, cx| {
4522 let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4523 pane.pin_tab_at(ix, window, cx);
4524 });
4525 assert_item_labels(&pane, ["C!", "A", "B*"], cx);
4526
4527 // Unpin C - moves after pinned area, B remains active
4528 pane.update_in(cx, |pane, window, cx| {
4529 let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4530 pane.unpin_tab_at(ix, window, cx);
4531 });
4532 assert_item_labels(&pane, ["C", "A", "B*"], cx);
4533 }
4534
4535 #[gpui::test]
4536 async fn test_drag_unpinned_tab_to_split_creates_pane_with_unpinned_tab(
4537 cx: &mut TestAppContext,
4538 ) {
4539 init_test(cx);
4540 let fs = FakeFs::new(cx.executor());
4541
4542 let project = Project::test(fs, None, cx).await;
4543 let (workspace, cx) =
4544 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4545 let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4546
4547 // Add A, B. Pin B. Activate A
4548 let item_a = add_labeled_item(&pane_a, "A", false, cx);
4549 let item_b = add_labeled_item(&pane_a, "B", false, cx);
4550
4551 pane_a.update_in(cx, |pane, window, cx| {
4552 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4553 pane.pin_tab_at(ix, window, cx);
4554
4555 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4556 pane.activate_item(ix, true, true, window, cx);
4557 });
4558
4559 // Drag A to create new split
4560 pane_a.update_in(cx, |pane, window, cx| {
4561 pane.drag_split_direction = Some(SplitDirection::Right);
4562
4563 let dragged_tab = DraggedTab {
4564 pane: pane_a.clone(),
4565 item: item_a.boxed_clone(),
4566 ix: 0,
4567 detail: 0,
4568 is_active: true,
4569 };
4570 pane.handle_tab_drop(&dragged_tab, 0, window, cx);
4571 });
4572
4573 // A should be moved to new pane. B should remain pinned, A should not be pinned
4574 let (pane_a, pane_b) = workspace.read_with(cx, |workspace, _| {
4575 let panes = workspace.panes();
4576 (panes[0].clone(), panes[1].clone())
4577 });
4578 assert_item_labels(&pane_a, ["B*!"], cx);
4579 assert_item_labels(&pane_b, ["A*"], cx);
4580 }
4581
4582 #[gpui::test]
4583 async fn test_drag_pinned_tab_to_split_creates_pane_with_pinned_tab(cx: &mut TestAppContext) {
4584 init_test(cx);
4585 let fs = FakeFs::new(cx.executor());
4586
4587 let project = Project::test(fs, None, cx).await;
4588 let (workspace, cx) =
4589 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4590 let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4591
4592 // Add A, B. Pin both. Activate A
4593 let item_a = add_labeled_item(&pane_a, "A", false, cx);
4594 let item_b = add_labeled_item(&pane_a, "B", false, cx);
4595
4596 pane_a.update_in(cx, |pane, window, cx| {
4597 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4598 pane.pin_tab_at(ix, window, cx);
4599
4600 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4601 pane.pin_tab_at(ix, window, cx);
4602
4603 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4604 pane.activate_item(ix, true, true, window, cx);
4605 });
4606 assert_item_labels(&pane_a, ["A*!", "B!"], cx);
4607
4608 // Drag A to create new split
4609 pane_a.update_in(cx, |pane, window, cx| {
4610 pane.drag_split_direction = Some(SplitDirection::Right);
4611
4612 let dragged_tab = DraggedTab {
4613 pane: pane_a.clone(),
4614 item: item_a.boxed_clone(),
4615 ix: 0,
4616 detail: 0,
4617 is_active: true,
4618 };
4619 pane.handle_tab_drop(&dragged_tab, 0, window, cx);
4620 });
4621
4622 // A should be moved to new pane. Both A and B should still be pinned
4623 let (pane_a, pane_b) = workspace.read_with(cx, |workspace, _| {
4624 let panes = workspace.panes();
4625 (panes[0].clone(), panes[1].clone())
4626 });
4627 assert_item_labels(&pane_a, ["B*!"], cx);
4628 assert_item_labels(&pane_b, ["A*!"], cx);
4629 }
4630
4631 #[gpui::test]
4632 async fn test_drag_pinned_tab_into_existing_panes_pinned_region(cx: &mut TestAppContext) {
4633 init_test(cx);
4634 let fs = FakeFs::new(cx.executor());
4635
4636 let project = Project::test(fs, None, cx).await;
4637 let (workspace, cx) =
4638 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4639 let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4640
4641 // Add A to pane A and pin
4642 let item_a = add_labeled_item(&pane_a, "A", false, cx);
4643 pane_a.update_in(cx, |pane, window, cx| {
4644 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4645 pane.pin_tab_at(ix, window, cx);
4646 });
4647 assert_item_labels(&pane_a, ["A*!"], cx);
4648
4649 // Add B to pane B and pin
4650 let pane_b = workspace.update_in(cx, |workspace, window, cx| {
4651 workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx)
4652 });
4653 let item_b = add_labeled_item(&pane_b, "B", false, cx);
4654 pane_b.update_in(cx, |pane, window, cx| {
4655 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4656 pane.pin_tab_at(ix, window, cx);
4657 });
4658 assert_item_labels(&pane_b, ["B*!"], cx);
4659
4660 // Move A from pane A to pane B's pinned region
4661 pane_b.update_in(cx, |pane, window, cx| {
4662 let dragged_tab = DraggedTab {
4663 pane: pane_a.clone(),
4664 item: item_a.boxed_clone(),
4665 ix: 0,
4666 detail: 0,
4667 is_active: true,
4668 };
4669 pane.handle_tab_drop(&dragged_tab, 0, window, cx);
4670 });
4671
4672 // A should stay pinned
4673 assert_item_labels(&pane_a, [], cx);
4674 assert_item_labels(&pane_b, ["A*!", "B!"], cx);
4675 }
4676
4677 #[gpui::test]
4678 async fn test_drag_pinned_tab_into_existing_panes_unpinned_region(cx: &mut TestAppContext) {
4679 init_test(cx);
4680 let fs = FakeFs::new(cx.executor());
4681
4682 let project = Project::test(fs, None, cx).await;
4683 let (workspace, cx) =
4684 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4685 let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4686
4687 // Add A to pane A and pin
4688 let item_a = add_labeled_item(&pane_a, "A", false, cx);
4689 pane_a.update_in(cx, |pane, window, cx| {
4690 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4691 pane.pin_tab_at(ix, window, cx);
4692 });
4693 assert_item_labels(&pane_a, ["A*!"], cx);
4694
4695 // Create pane B with pinned item B
4696 let pane_b = workspace.update_in(cx, |workspace, window, cx| {
4697 workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx)
4698 });
4699 let item_b = add_labeled_item(&pane_b, "B", false, cx);
4700 assert_item_labels(&pane_b, ["B*"], cx);
4701
4702 pane_b.update_in(cx, |pane, window, cx| {
4703 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4704 pane.pin_tab_at(ix, window, cx);
4705 });
4706 assert_item_labels(&pane_b, ["B*!"], cx);
4707
4708 // Move A from pane A to pane B's unpinned region
4709 pane_b.update_in(cx, |pane, window, cx| {
4710 let dragged_tab = DraggedTab {
4711 pane: pane_a.clone(),
4712 item: item_a.boxed_clone(),
4713 ix: 0,
4714 detail: 0,
4715 is_active: true,
4716 };
4717 pane.handle_tab_drop(&dragged_tab, 1, window, cx);
4718 });
4719
4720 // A should become pinned
4721 assert_item_labels(&pane_a, [], cx);
4722 assert_item_labels(&pane_b, ["B!", "A*"], cx);
4723 }
4724
4725 #[gpui::test]
4726 async fn test_drag_pinned_tab_into_existing_panes_first_position_with_no_pinned_tabs(
4727 cx: &mut TestAppContext,
4728 ) {
4729 init_test(cx);
4730 let fs = FakeFs::new(cx.executor());
4731
4732 let project = Project::test(fs, None, cx).await;
4733 let (workspace, cx) =
4734 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4735 let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4736
4737 // Add A to pane A and pin
4738 let item_a = add_labeled_item(&pane_a, "A", false, cx);
4739 pane_a.update_in(cx, |pane, window, cx| {
4740 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4741 pane.pin_tab_at(ix, window, cx);
4742 });
4743 assert_item_labels(&pane_a, ["A*!"], cx);
4744
4745 // Add B to pane B
4746 let pane_b = workspace.update_in(cx, |workspace, window, cx| {
4747 workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx)
4748 });
4749 add_labeled_item(&pane_b, "B", false, cx);
4750 assert_item_labels(&pane_b, ["B*"], cx);
4751
4752 // Move A from pane A to position 0 in pane B, indicating it should stay pinned
4753 pane_b.update_in(cx, |pane, window, cx| {
4754 let dragged_tab = DraggedTab {
4755 pane: pane_a.clone(),
4756 item: item_a.boxed_clone(),
4757 ix: 0,
4758 detail: 0,
4759 is_active: true,
4760 };
4761 pane.handle_tab_drop(&dragged_tab, 0, window, cx);
4762 });
4763
4764 // A should stay pinned
4765 assert_item_labels(&pane_a, [], cx);
4766 assert_item_labels(&pane_b, ["A*!", "B"], cx);
4767 }
4768
4769 #[gpui::test]
4770 async fn test_drag_pinned_tab_into_existing_pane_at_max_capacity_closes_unpinned_tabs(
4771 cx: &mut TestAppContext,
4772 ) {
4773 init_test(cx);
4774 let fs = FakeFs::new(cx.executor());
4775
4776 let project = Project::test(fs, None, cx).await;
4777 let (workspace, cx) =
4778 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4779 let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4780 set_max_tabs(cx, Some(2));
4781
4782 // Add A, B to pane A. Pin both
4783 let item_a = add_labeled_item(&pane_a, "A", false, cx);
4784 let item_b = add_labeled_item(&pane_a, "B", false, cx);
4785 pane_a.update_in(cx, |pane, window, cx| {
4786 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4787 pane.pin_tab_at(ix, window, cx);
4788
4789 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4790 pane.pin_tab_at(ix, window, cx);
4791 });
4792 assert_item_labels(&pane_a, ["A!", "B*!"], cx);
4793
4794 // Add C, D to pane B. Pin both
4795 let pane_b = workspace.update_in(cx, |workspace, window, cx| {
4796 workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx)
4797 });
4798 let item_c = add_labeled_item(&pane_b, "C", false, cx);
4799 let item_d = add_labeled_item(&pane_b, "D", false, cx);
4800 pane_b.update_in(cx, |pane, window, cx| {
4801 let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4802 pane.pin_tab_at(ix, window, cx);
4803
4804 let ix = pane.index_for_item_id(item_d.item_id()).unwrap();
4805 pane.pin_tab_at(ix, window, cx);
4806 });
4807 assert_item_labels(&pane_b, ["C!", "D*!"], cx);
4808
4809 // Add a third unpinned item to pane B (exceeds max tabs), but is allowed,
4810 // as we allow 1 tab over max if the others are pinned or dirty
4811 add_labeled_item(&pane_b, "E", false, cx);
4812 assert_item_labels(&pane_b, ["C!", "D!", "E*"], cx);
4813
4814 // Drag pinned A from pane A to position 0 in pane B
4815 pane_b.update_in(cx, |pane, window, cx| {
4816 let dragged_tab = DraggedTab {
4817 pane: pane_a.clone(),
4818 item: item_a.boxed_clone(),
4819 ix: 0,
4820 detail: 0,
4821 is_active: true,
4822 };
4823 pane.handle_tab_drop(&dragged_tab, 0, window, cx);
4824 });
4825
4826 // E (unpinned) should be closed, leaving 3 pinned items
4827 assert_item_labels(&pane_a, ["B*!"], cx);
4828 assert_item_labels(&pane_b, ["A*!", "C!", "D!"], cx);
4829 }
4830
4831 #[gpui::test]
4832 async fn test_drag_last_pinned_tab_to_same_position_stays_pinned(cx: &mut TestAppContext) {
4833 init_test(cx);
4834 let fs = FakeFs::new(cx.executor());
4835
4836 let project = Project::test(fs, None, cx).await;
4837 let (workspace, cx) =
4838 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4839 let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4840
4841 // Add A to pane A and pin it
4842 let item_a = add_labeled_item(&pane_a, "A", false, cx);
4843 pane_a.update_in(cx, |pane, window, cx| {
4844 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4845 pane.pin_tab_at(ix, window, cx);
4846 });
4847 assert_item_labels(&pane_a, ["A*!"], cx);
4848
4849 // Drag pinned A to position 1 (directly to the right) in the same pane
4850 pane_a.update_in(cx, |pane, window, cx| {
4851 let dragged_tab = DraggedTab {
4852 pane: pane_a.clone(),
4853 item: item_a.boxed_clone(),
4854 ix: 0,
4855 detail: 0,
4856 is_active: true,
4857 };
4858 pane.handle_tab_drop(&dragged_tab, 1, window, cx);
4859 });
4860
4861 // A should still be pinned and active
4862 assert_item_labels(&pane_a, ["A*!"], cx);
4863 }
4864
4865 #[gpui::test]
4866 async fn test_drag_pinned_tab_beyond_last_pinned_tab_in_same_pane_stays_pinned(
4867 cx: &mut TestAppContext,
4868 ) {
4869 init_test(cx);
4870 let fs = FakeFs::new(cx.executor());
4871
4872 let project = Project::test(fs, None, cx).await;
4873 let (workspace, cx) =
4874 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4875 let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4876
4877 // Add A, B to pane A and pin both
4878 let item_a = add_labeled_item(&pane_a, "A", false, cx);
4879 let item_b = add_labeled_item(&pane_a, "B", false, cx);
4880 pane_a.update_in(cx, |pane, window, cx| {
4881 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4882 pane.pin_tab_at(ix, window, cx);
4883
4884 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4885 pane.pin_tab_at(ix, window, cx);
4886 });
4887 assert_item_labels(&pane_a, ["A!", "B*!"], cx);
4888
4889 // Drag pinned A right of B in the same pane
4890 pane_a.update_in(cx, |pane, window, cx| {
4891 let dragged_tab = DraggedTab {
4892 pane: pane_a.clone(),
4893 item: item_a.boxed_clone(),
4894 ix: 0,
4895 detail: 0,
4896 is_active: true,
4897 };
4898 pane.handle_tab_drop(&dragged_tab, 2, window, cx);
4899 });
4900
4901 // A stays pinned
4902 assert_item_labels(&pane_a, ["B!", "A*!"], cx);
4903 }
4904
4905 #[gpui::test]
4906 async fn test_drag_pinned_tab_beyond_unpinned_tab_in_same_pane_becomes_unpinned(
4907 cx: &mut TestAppContext,
4908 ) {
4909 init_test(cx);
4910 let fs = FakeFs::new(cx.executor());
4911
4912 let project = Project::test(fs, None, cx).await;
4913 let (workspace, cx) =
4914 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4915 let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4916
4917 // Add A, B to pane A and pin A
4918 let item_a = add_labeled_item(&pane_a, "A", false, cx);
4919 add_labeled_item(&pane_a, "B", false, cx);
4920 pane_a.update_in(cx, |pane, window, cx| {
4921 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4922 pane.pin_tab_at(ix, window, cx);
4923 });
4924 assert_item_labels(&pane_a, ["A!", "B*"], cx);
4925
4926 // Drag pinned A right of B in the same pane
4927 pane_a.update_in(cx, |pane, window, cx| {
4928 let dragged_tab = DraggedTab {
4929 pane: pane_a.clone(),
4930 item: item_a.boxed_clone(),
4931 ix: 0,
4932 detail: 0,
4933 is_active: true,
4934 };
4935 pane.handle_tab_drop(&dragged_tab, 2, window, cx);
4936 });
4937
4938 // A becomes unpinned
4939 assert_item_labels(&pane_a, ["B", "A*"], cx);
4940 }
4941
4942 #[gpui::test]
4943 async fn test_drag_unpinned_tab_in_front_of_pinned_tab_in_same_pane_becomes_pinned(
4944 cx: &mut TestAppContext,
4945 ) {
4946 init_test(cx);
4947 let fs = FakeFs::new(cx.executor());
4948
4949 let project = Project::test(fs, None, cx).await;
4950 let (workspace, cx) =
4951 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4952 let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4953
4954 // Add A, B to pane A and pin A
4955 let item_a = add_labeled_item(&pane_a, "A", false, cx);
4956 let item_b = add_labeled_item(&pane_a, "B", false, cx);
4957 pane_a.update_in(cx, |pane, window, cx| {
4958 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4959 pane.pin_tab_at(ix, window, cx);
4960 });
4961 assert_item_labels(&pane_a, ["A!", "B*"], cx);
4962
4963 // Drag pinned B left of A in the same pane
4964 pane_a.update_in(cx, |pane, window, cx| {
4965 let dragged_tab = DraggedTab {
4966 pane: pane_a.clone(),
4967 item: item_b.boxed_clone(),
4968 ix: 1,
4969 detail: 0,
4970 is_active: true,
4971 };
4972 pane.handle_tab_drop(&dragged_tab, 0, window, cx);
4973 });
4974
4975 // A becomes unpinned
4976 assert_item_labels(&pane_a, ["B*!", "A!"], cx);
4977 }
4978
4979 #[gpui::test]
4980 async fn test_drag_unpinned_tab_to_the_pinned_region_stays_pinned(cx: &mut TestAppContext) {
4981 init_test(cx);
4982 let fs = FakeFs::new(cx.executor());
4983
4984 let project = Project::test(fs, None, cx).await;
4985 let (workspace, cx) =
4986 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4987 let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4988
4989 // Add A, B, C to pane A and pin A
4990 let item_a = add_labeled_item(&pane_a, "A", false, cx);
4991 add_labeled_item(&pane_a, "B", false, cx);
4992 let item_c = add_labeled_item(&pane_a, "C", false, cx);
4993 pane_a.update_in(cx, |pane, window, cx| {
4994 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4995 pane.pin_tab_at(ix, window, cx);
4996 });
4997 assert_item_labels(&pane_a, ["A!", "B", "C*"], cx);
4998
4999 // Drag pinned C left of B in the same pane
5000 pane_a.update_in(cx, |pane, window, cx| {
5001 let dragged_tab = DraggedTab {
5002 pane: pane_a.clone(),
5003 item: item_c.boxed_clone(),
5004 ix: 2,
5005 detail: 0,
5006 is_active: true,
5007 };
5008 pane.handle_tab_drop(&dragged_tab, 1, window, cx);
5009 });
5010
5011 // A stays pinned, B and C remain unpinned
5012 assert_item_labels(&pane_a, ["A!", "C*", "B"], cx);
5013 }
5014
5015 #[gpui::test]
5016 async fn test_drag_unpinned_tab_into_existing_panes_pinned_region(cx: &mut TestAppContext) {
5017 init_test(cx);
5018 let fs = FakeFs::new(cx.executor());
5019
5020 let project = Project::test(fs, None, cx).await;
5021 let (workspace, cx) =
5022 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5023 let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5024
5025 // Add unpinned item A to pane A
5026 let item_a = add_labeled_item(&pane_a, "A", false, cx);
5027 assert_item_labels(&pane_a, ["A*"], cx);
5028
5029 // Create pane B with pinned item B
5030 let pane_b = workspace.update_in(cx, |workspace, window, cx| {
5031 workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx)
5032 });
5033 let item_b = add_labeled_item(&pane_b, "B", false, cx);
5034 pane_b.update_in(cx, |pane, window, cx| {
5035 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
5036 pane.pin_tab_at(ix, window, cx);
5037 });
5038 assert_item_labels(&pane_b, ["B*!"], cx);
5039
5040 // Move A from pane A to pane B's pinned region
5041 pane_b.update_in(cx, |pane, window, cx| {
5042 let dragged_tab = DraggedTab {
5043 pane: pane_a.clone(),
5044 item: item_a.boxed_clone(),
5045 ix: 0,
5046 detail: 0,
5047 is_active: true,
5048 };
5049 pane.handle_tab_drop(&dragged_tab, 0, window, cx);
5050 });
5051
5052 // A should become pinned since it was dropped in the pinned region
5053 assert_item_labels(&pane_a, [], cx);
5054 assert_item_labels(&pane_b, ["A*!", "B!"], cx);
5055 }
5056
5057 #[gpui::test]
5058 async fn test_drag_unpinned_tab_into_existing_panes_unpinned_region(cx: &mut TestAppContext) {
5059 init_test(cx);
5060 let fs = FakeFs::new(cx.executor());
5061
5062 let project = Project::test(fs, None, cx).await;
5063 let (workspace, cx) =
5064 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5065 let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5066
5067 // Add unpinned item A to pane A
5068 let item_a = add_labeled_item(&pane_a, "A", false, cx);
5069 assert_item_labels(&pane_a, ["A*"], cx);
5070
5071 // Create pane B with one pinned item B
5072 let pane_b = workspace.update_in(cx, |workspace, window, cx| {
5073 workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx)
5074 });
5075 let item_b = add_labeled_item(&pane_b, "B", false, cx);
5076 pane_b.update_in(cx, |pane, window, cx| {
5077 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
5078 pane.pin_tab_at(ix, window, cx);
5079 });
5080 assert_item_labels(&pane_b, ["B*!"], cx);
5081
5082 // Move A from pane A to pane B's unpinned region
5083 pane_b.update_in(cx, |pane, window, cx| {
5084 let dragged_tab = DraggedTab {
5085 pane: pane_a.clone(),
5086 item: item_a.boxed_clone(),
5087 ix: 0,
5088 detail: 0,
5089 is_active: true,
5090 };
5091 pane.handle_tab_drop(&dragged_tab, 1, window, cx);
5092 });
5093
5094 // A should remain unpinned since it was dropped outside the pinned region
5095 assert_item_labels(&pane_a, [], cx);
5096 assert_item_labels(&pane_b, ["B!", "A*"], cx);
5097 }
5098
5099 #[gpui::test]
5100 async fn test_drag_pinned_tab_throughout_entire_range_of_pinned_tabs_both_directions(
5101 cx: &mut TestAppContext,
5102 ) {
5103 init_test(cx);
5104 let fs = FakeFs::new(cx.executor());
5105
5106 let project = Project::test(fs, None, cx).await;
5107 let (workspace, cx) =
5108 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5109 let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5110
5111 // Add A, B, C and pin all
5112 let item_a = add_labeled_item(&pane_a, "A", false, cx);
5113 let item_b = add_labeled_item(&pane_a, "B", false, cx);
5114 let item_c = add_labeled_item(&pane_a, "C", false, cx);
5115 assert_item_labels(&pane_a, ["A", "B", "C*"], cx);
5116
5117 pane_a.update_in(cx, |pane, window, cx| {
5118 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
5119 pane.pin_tab_at(ix, window, cx);
5120
5121 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
5122 pane.pin_tab_at(ix, window, cx);
5123
5124 let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
5125 pane.pin_tab_at(ix, window, cx);
5126 });
5127 assert_item_labels(&pane_a, ["A!", "B!", "C*!"], cx);
5128
5129 // Move A to right of B
5130 pane_a.update_in(cx, |pane, window, cx| {
5131 let dragged_tab = DraggedTab {
5132 pane: pane_a.clone(),
5133 item: item_a.boxed_clone(),
5134 ix: 0,
5135 detail: 0,
5136 is_active: true,
5137 };
5138 pane.handle_tab_drop(&dragged_tab, 1, window, cx);
5139 });
5140
5141 // A should be after B and all are pinned
5142 assert_item_labels(&pane_a, ["B!", "A*!", "C!"], cx);
5143
5144 // Move A to right of C
5145 pane_a.update_in(cx, |pane, window, cx| {
5146 let dragged_tab = DraggedTab {
5147 pane: pane_a.clone(),
5148 item: item_a.boxed_clone(),
5149 ix: 1,
5150 detail: 0,
5151 is_active: true,
5152 };
5153 pane.handle_tab_drop(&dragged_tab, 2, window, cx);
5154 });
5155
5156 // A should be after C and all are pinned
5157 assert_item_labels(&pane_a, ["B!", "C!", "A*!"], cx);
5158
5159 // Move A to left of C
5160 pane_a.update_in(cx, |pane, window, cx| {
5161 let dragged_tab = DraggedTab {
5162 pane: pane_a.clone(),
5163 item: item_a.boxed_clone(),
5164 ix: 2,
5165 detail: 0,
5166 is_active: true,
5167 };
5168 pane.handle_tab_drop(&dragged_tab, 1, window, cx);
5169 });
5170
5171 // A should be before C and all are pinned
5172 assert_item_labels(&pane_a, ["B!", "A*!", "C!"], cx);
5173
5174 // Move A to left of B
5175 pane_a.update_in(cx, |pane, window, cx| {
5176 let dragged_tab = DraggedTab {
5177 pane: pane_a.clone(),
5178 item: item_a.boxed_clone(),
5179 ix: 1,
5180 detail: 0,
5181 is_active: true,
5182 };
5183 pane.handle_tab_drop(&dragged_tab, 0, window, cx);
5184 });
5185
5186 // A should be before B and all are pinned
5187 assert_item_labels(&pane_a, ["A*!", "B!", "C!"], cx);
5188 }
5189
5190 #[gpui::test]
5191 async fn test_drag_first_tab_to_last_position(cx: &mut TestAppContext) {
5192 init_test(cx);
5193 let fs = FakeFs::new(cx.executor());
5194
5195 let project = Project::test(fs, None, cx).await;
5196 let (workspace, cx) =
5197 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5198 let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5199
5200 // Add A, B, C
5201 let item_a = add_labeled_item(&pane_a, "A", false, cx);
5202 add_labeled_item(&pane_a, "B", false, cx);
5203 add_labeled_item(&pane_a, "C", false, cx);
5204 assert_item_labels(&pane_a, ["A", "B", "C*"], cx);
5205
5206 // Move A to the end
5207 pane_a.update_in(cx, |pane, window, cx| {
5208 let dragged_tab = DraggedTab {
5209 pane: pane_a.clone(),
5210 item: item_a.boxed_clone(),
5211 ix: 0,
5212 detail: 0,
5213 is_active: true,
5214 };
5215 pane.handle_tab_drop(&dragged_tab, 2, window, cx);
5216 });
5217
5218 // A should be at the end
5219 assert_item_labels(&pane_a, ["B", "C", "A*"], cx);
5220 }
5221
5222 #[gpui::test]
5223 async fn test_drag_last_tab_to_first_position(cx: &mut TestAppContext) {
5224 init_test(cx);
5225 let fs = FakeFs::new(cx.executor());
5226
5227 let project = Project::test(fs, None, cx).await;
5228 let (workspace, cx) =
5229 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5230 let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5231
5232 // Add A, B, C
5233 add_labeled_item(&pane_a, "A", false, cx);
5234 add_labeled_item(&pane_a, "B", false, cx);
5235 let item_c = add_labeled_item(&pane_a, "C", false, cx);
5236 assert_item_labels(&pane_a, ["A", "B", "C*"], cx);
5237
5238 // Move C to the beginning
5239 pane_a.update_in(cx, |pane, window, cx| {
5240 let dragged_tab = DraggedTab {
5241 pane: pane_a.clone(),
5242 item: item_c.boxed_clone(),
5243 ix: 2,
5244 detail: 0,
5245 is_active: true,
5246 };
5247 pane.handle_tab_drop(&dragged_tab, 0, window, cx);
5248 });
5249
5250 // C should be at the beginning
5251 assert_item_labels(&pane_a, ["C*", "A", "B"], cx);
5252 }
5253
5254 #[gpui::test]
5255 async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
5256 init_test(cx);
5257 let fs = FakeFs::new(cx.executor());
5258
5259 let project = Project::test(fs, None, cx).await;
5260 let (workspace, cx) =
5261 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5262 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5263
5264 // 1. Add with a destination index
5265 // a. Add before the active item
5266 set_labeled_items(&pane, ["A", "B*", "C"], cx);
5267 pane.update_in(cx, |pane, window, cx| {
5268 pane.add_item(
5269 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
5270 false,
5271 false,
5272 Some(0),
5273 window,
5274 cx,
5275 );
5276 });
5277 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
5278
5279 // b. Add after the active item
5280 set_labeled_items(&pane, ["A", "B*", "C"], cx);
5281 pane.update_in(cx, |pane, window, cx| {
5282 pane.add_item(
5283 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
5284 false,
5285 false,
5286 Some(2),
5287 window,
5288 cx,
5289 );
5290 });
5291 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
5292
5293 // c. Add at the end of the item list (including off the length)
5294 set_labeled_items(&pane, ["A", "B*", "C"], cx);
5295 pane.update_in(cx, |pane, window, cx| {
5296 pane.add_item(
5297 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
5298 false,
5299 false,
5300 Some(5),
5301 window,
5302 cx,
5303 );
5304 });
5305 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
5306
5307 // 2. Add without a destination index
5308 // a. Add with active item at the start of the item list
5309 set_labeled_items(&pane, ["A*", "B", "C"], cx);
5310 pane.update_in(cx, |pane, window, cx| {
5311 pane.add_item(
5312 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
5313 false,
5314 false,
5315 None,
5316 window,
5317 cx,
5318 );
5319 });
5320 set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
5321
5322 // b. Add with active item at the end of the item list
5323 set_labeled_items(&pane, ["A", "B", "C*"], cx);
5324 pane.update_in(cx, |pane, window, cx| {
5325 pane.add_item(
5326 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
5327 false,
5328 false,
5329 None,
5330 window,
5331 cx,
5332 );
5333 });
5334 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
5335 }
5336
5337 #[gpui::test]
5338 async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
5339 init_test(cx);
5340 let fs = FakeFs::new(cx.executor());
5341
5342 let project = Project::test(fs, None, cx).await;
5343 let (workspace, cx) =
5344 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5345 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5346
5347 // 1. Add with a destination index
5348 // 1a. Add before the active item
5349 let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
5350 pane.update_in(cx, |pane, window, cx| {
5351 pane.add_item(d, false, false, Some(0), window, cx);
5352 });
5353 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
5354
5355 // 1b. Add after the active item
5356 let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
5357 pane.update_in(cx, |pane, window, cx| {
5358 pane.add_item(d, false, false, Some(2), window, cx);
5359 });
5360 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
5361
5362 // 1c. Add at the end of the item list (including off the length)
5363 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
5364 pane.update_in(cx, |pane, window, cx| {
5365 pane.add_item(a, false, false, Some(5), window, cx);
5366 });
5367 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
5368
5369 // 1d. Add same item to active index
5370 let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
5371 pane.update_in(cx, |pane, window, cx| {
5372 pane.add_item(b, false, false, Some(1), window, cx);
5373 });
5374 assert_item_labels(&pane, ["A", "B*", "C"], cx);
5375
5376 // 1e. Add item to index after same item in last position
5377 let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
5378 pane.update_in(cx, |pane, window, cx| {
5379 pane.add_item(c, false, false, Some(2), window, cx);
5380 });
5381 assert_item_labels(&pane, ["A", "B", "C*"], cx);
5382
5383 // 2. Add without a destination index
5384 // 2a. Add with active item at the start of the item list
5385 let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
5386 pane.update_in(cx, |pane, window, cx| {
5387 pane.add_item(d, false, false, None, window, cx);
5388 });
5389 assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
5390
5391 // 2b. Add with active item at the end of the item list
5392 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
5393 pane.update_in(cx, |pane, window, cx| {
5394 pane.add_item(a, false, false, None, window, cx);
5395 });
5396 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
5397
5398 // 2c. Add active item to active item at end of list
5399 let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
5400 pane.update_in(cx, |pane, window, cx| {
5401 pane.add_item(c, false, false, None, window, cx);
5402 });
5403 assert_item_labels(&pane, ["A", "B", "C*"], cx);
5404
5405 // 2d. Add active item to active item at start of list
5406 let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
5407 pane.update_in(cx, |pane, window, cx| {
5408 pane.add_item(a, false, false, None, window, cx);
5409 });
5410 assert_item_labels(&pane, ["A*", "B", "C"], cx);
5411 }
5412
5413 #[gpui::test]
5414 async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
5415 init_test(cx);
5416 let fs = FakeFs::new(cx.executor());
5417
5418 let project = Project::test(fs, None, cx).await;
5419 let (workspace, cx) =
5420 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5421 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5422
5423 // singleton view
5424 pane.update_in(cx, |pane, window, cx| {
5425 pane.add_item(
5426 Box::new(cx.new(|cx| {
5427 TestItem::new(cx)
5428 .with_singleton(true)
5429 .with_label("buffer 1")
5430 .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
5431 })),
5432 false,
5433 false,
5434 None,
5435 window,
5436 cx,
5437 );
5438 });
5439 assert_item_labels(&pane, ["buffer 1*"], cx);
5440
5441 // new singleton view with the same project entry
5442 pane.update_in(cx, |pane, window, cx| {
5443 pane.add_item(
5444 Box::new(cx.new(|cx| {
5445 TestItem::new(cx)
5446 .with_singleton(true)
5447 .with_label("buffer 1")
5448 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
5449 })),
5450 false,
5451 false,
5452 None,
5453 window,
5454 cx,
5455 );
5456 });
5457 assert_item_labels(&pane, ["buffer 1*"], cx);
5458
5459 // new singleton view with different project entry
5460 pane.update_in(cx, |pane, window, cx| {
5461 pane.add_item(
5462 Box::new(cx.new(|cx| {
5463 TestItem::new(cx)
5464 .with_singleton(true)
5465 .with_label("buffer 2")
5466 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
5467 })),
5468 false,
5469 false,
5470 None,
5471 window,
5472 cx,
5473 );
5474 });
5475 assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
5476
5477 // new multibuffer view with the same project entry
5478 pane.update_in(cx, |pane, window, cx| {
5479 pane.add_item(
5480 Box::new(cx.new(|cx| {
5481 TestItem::new(cx)
5482 .with_singleton(false)
5483 .with_label("multibuffer 1")
5484 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
5485 })),
5486 false,
5487 false,
5488 None,
5489 window,
5490 cx,
5491 );
5492 });
5493 assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
5494
5495 // another multibuffer view with the same project entry
5496 pane.update_in(cx, |pane, window, cx| {
5497 pane.add_item(
5498 Box::new(cx.new(|cx| {
5499 TestItem::new(cx)
5500 .with_singleton(false)
5501 .with_label("multibuffer 1b")
5502 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
5503 })),
5504 false,
5505 false,
5506 None,
5507 window,
5508 cx,
5509 );
5510 });
5511 assert_item_labels(
5512 &pane,
5513 ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
5514 cx,
5515 );
5516 }
5517
5518 #[gpui::test]
5519 async fn test_remove_item_ordering_history(cx: &mut TestAppContext) {
5520 init_test(cx);
5521 let fs = FakeFs::new(cx.executor());
5522
5523 let project = Project::test(fs, None, cx).await;
5524 let (workspace, cx) =
5525 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5526 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5527
5528 add_labeled_item(&pane, "A", false, cx);
5529 add_labeled_item(&pane, "B", false, cx);
5530 add_labeled_item(&pane, "C", false, cx);
5531 add_labeled_item(&pane, "D", false, cx);
5532 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
5533
5534 pane.update_in(cx, |pane, window, cx| {
5535 pane.activate_item(1, false, false, window, cx)
5536 });
5537 add_labeled_item(&pane, "1", false, cx);
5538 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
5539
5540 pane.update_in(cx, |pane, window, cx| {
5541 pane.close_active_item(
5542 &CloseActiveItem {
5543 save_intent: None,
5544 close_pinned: false,
5545 },
5546 window,
5547 cx,
5548 )
5549 })
5550 .await
5551 .unwrap();
5552 assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
5553
5554 pane.update_in(cx, |pane, window, cx| {
5555 pane.activate_item(3, false, false, window, cx)
5556 });
5557 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
5558
5559 pane.update_in(cx, |pane, window, cx| {
5560 pane.close_active_item(
5561 &CloseActiveItem {
5562 save_intent: None,
5563 close_pinned: false,
5564 },
5565 window,
5566 cx,
5567 )
5568 })
5569 .await
5570 .unwrap();
5571 assert_item_labels(&pane, ["A", "B*", "C"], cx);
5572
5573 pane.update_in(cx, |pane, window, cx| {
5574 pane.close_active_item(
5575 &CloseActiveItem {
5576 save_intent: None,
5577 close_pinned: false,
5578 },
5579 window,
5580 cx,
5581 )
5582 })
5583 .await
5584 .unwrap();
5585 assert_item_labels(&pane, ["A", "C*"], cx);
5586
5587 pane.update_in(cx, |pane, window, cx| {
5588 pane.close_active_item(
5589 &CloseActiveItem {
5590 save_intent: None,
5591 close_pinned: false,
5592 },
5593 window,
5594 cx,
5595 )
5596 })
5597 .await
5598 .unwrap();
5599 assert_item_labels(&pane, ["A*"], cx);
5600 }
5601
5602 #[gpui::test]
5603 async fn test_remove_item_ordering_neighbour(cx: &mut TestAppContext) {
5604 init_test(cx);
5605 cx.update_global::<SettingsStore, ()>(|s, cx| {
5606 s.update_user_settings::<ItemSettings>(cx, |s| {
5607 s.activate_on_close = Some(ActivateOnClose::Neighbour);
5608 });
5609 });
5610 let fs = FakeFs::new(cx.executor());
5611
5612 let project = Project::test(fs, None, cx).await;
5613 let (workspace, cx) =
5614 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5615 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5616
5617 add_labeled_item(&pane, "A", false, cx);
5618 add_labeled_item(&pane, "B", false, cx);
5619 add_labeled_item(&pane, "C", false, cx);
5620 add_labeled_item(&pane, "D", false, cx);
5621 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
5622
5623 pane.update_in(cx, |pane, window, cx| {
5624 pane.activate_item(1, false, false, window, cx)
5625 });
5626 add_labeled_item(&pane, "1", false, cx);
5627 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
5628
5629 pane.update_in(cx, |pane, window, cx| {
5630 pane.close_active_item(
5631 &CloseActiveItem {
5632 save_intent: None,
5633 close_pinned: false,
5634 },
5635 window,
5636 cx,
5637 )
5638 })
5639 .await
5640 .unwrap();
5641 assert_item_labels(&pane, ["A", "B", "C*", "D"], cx);
5642
5643 pane.update_in(cx, |pane, window, cx| {
5644 pane.activate_item(3, false, false, window, cx)
5645 });
5646 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
5647
5648 pane.update_in(cx, |pane, window, cx| {
5649 pane.close_active_item(
5650 &CloseActiveItem {
5651 save_intent: None,
5652 close_pinned: false,
5653 },
5654 window,
5655 cx,
5656 )
5657 })
5658 .await
5659 .unwrap();
5660 assert_item_labels(&pane, ["A", "B", "C*"], cx);
5661
5662 pane.update_in(cx, |pane, window, cx| {
5663 pane.close_active_item(
5664 &CloseActiveItem {
5665 save_intent: None,
5666 close_pinned: false,
5667 },
5668 window,
5669 cx,
5670 )
5671 })
5672 .await
5673 .unwrap();
5674 assert_item_labels(&pane, ["A", "B*"], cx);
5675
5676 pane.update_in(cx, |pane, window, cx| {
5677 pane.close_active_item(
5678 &CloseActiveItem {
5679 save_intent: None,
5680 close_pinned: false,
5681 },
5682 window,
5683 cx,
5684 )
5685 })
5686 .await
5687 .unwrap();
5688 assert_item_labels(&pane, ["A*"], cx);
5689 }
5690
5691 #[gpui::test]
5692 async fn test_remove_item_ordering_left_neighbour(cx: &mut TestAppContext) {
5693 init_test(cx);
5694 cx.update_global::<SettingsStore, ()>(|s, cx| {
5695 s.update_user_settings::<ItemSettings>(cx, |s| {
5696 s.activate_on_close = Some(ActivateOnClose::LeftNeighbour);
5697 });
5698 });
5699 let fs = FakeFs::new(cx.executor());
5700
5701 let project = Project::test(fs, None, cx).await;
5702 let (workspace, cx) =
5703 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5704 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5705
5706 add_labeled_item(&pane, "A", false, cx);
5707 add_labeled_item(&pane, "B", false, cx);
5708 add_labeled_item(&pane, "C", false, cx);
5709 add_labeled_item(&pane, "D", false, cx);
5710 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
5711
5712 pane.update_in(cx, |pane, window, cx| {
5713 pane.activate_item(1, false, false, window, cx)
5714 });
5715 add_labeled_item(&pane, "1", false, cx);
5716 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
5717
5718 pane.update_in(cx, |pane, window, cx| {
5719 pane.close_active_item(
5720 &CloseActiveItem {
5721 save_intent: None,
5722 close_pinned: false,
5723 },
5724 window,
5725 cx,
5726 )
5727 })
5728 .await
5729 .unwrap();
5730 assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
5731
5732 pane.update_in(cx, |pane, window, cx| {
5733 pane.activate_item(3, false, false, window, cx)
5734 });
5735 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
5736
5737 pane.update_in(cx, |pane, window, cx| {
5738 pane.close_active_item(
5739 &CloseActiveItem {
5740 save_intent: None,
5741 close_pinned: false,
5742 },
5743 window,
5744 cx,
5745 )
5746 })
5747 .await
5748 .unwrap();
5749 assert_item_labels(&pane, ["A", "B", "C*"], cx);
5750
5751 pane.update_in(cx, |pane, window, cx| {
5752 pane.activate_item(0, false, false, window, cx)
5753 });
5754 assert_item_labels(&pane, ["A*", "B", "C"], cx);
5755
5756 pane.update_in(cx, |pane, window, cx| {
5757 pane.close_active_item(
5758 &CloseActiveItem {
5759 save_intent: None,
5760 close_pinned: false,
5761 },
5762 window,
5763 cx,
5764 )
5765 })
5766 .await
5767 .unwrap();
5768 assert_item_labels(&pane, ["B*", "C"], cx);
5769
5770 pane.update_in(cx, |pane, window, cx| {
5771 pane.close_active_item(
5772 &CloseActiveItem {
5773 save_intent: None,
5774 close_pinned: false,
5775 },
5776 window,
5777 cx,
5778 )
5779 })
5780 .await
5781 .unwrap();
5782 assert_item_labels(&pane, ["C*"], cx);
5783 }
5784
5785 #[gpui::test]
5786 async fn test_close_inactive_items(cx: &mut TestAppContext) {
5787 init_test(cx);
5788 let fs = FakeFs::new(cx.executor());
5789
5790 let project = Project::test(fs, None, cx).await;
5791 let (workspace, cx) =
5792 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5793 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5794
5795 let item_a = add_labeled_item(&pane, "A", false, cx);
5796 pane.update_in(cx, |pane, window, cx| {
5797 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
5798 pane.pin_tab_at(ix, window, cx);
5799 });
5800 assert_item_labels(&pane, ["A*!"], cx);
5801
5802 let item_b = add_labeled_item(&pane, "B", false, cx);
5803 pane.update_in(cx, |pane, window, cx| {
5804 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
5805 pane.pin_tab_at(ix, window, cx);
5806 });
5807 assert_item_labels(&pane, ["A!", "B*!"], cx);
5808
5809 add_labeled_item(&pane, "C", false, cx);
5810 assert_item_labels(&pane, ["A!", "B!", "C*"], cx);
5811
5812 add_labeled_item(&pane, "D", false, cx);
5813 add_labeled_item(&pane, "E", false, cx);
5814 assert_item_labels(&pane, ["A!", "B!", "C", "D", "E*"], cx);
5815
5816 pane.update_in(cx, |pane, window, cx| {
5817 pane.close_inactive_items(
5818 &CloseInactiveItems {
5819 save_intent: None,
5820 close_pinned: false,
5821 },
5822 window,
5823 cx,
5824 )
5825 })
5826 .await
5827 .unwrap();
5828 assert_item_labels(&pane, ["A!", "B!", "E*"], cx);
5829 }
5830
5831 #[gpui::test]
5832 async fn test_close_clean_items(cx: &mut TestAppContext) {
5833 init_test(cx);
5834 let fs = FakeFs::new(cx.executor());
5835
5836 let project = Project::test(fs, None, cx).await;
5837 let (workspace, cx) =
5838 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5839 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5840
5841 add_labeled_item(&pane, "A", true, cx);
5842 add_labeled_item(&pane, "B", false, cx);
5843 add_labeled_item(&pane, "C", true, cx);
5844 add_labeled_item(&pane, "D", false, cx);
5845 add_labeled_item(&pane, "E", false, cx);
5846 assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
5847
5848 pane.update_in(cx, |pane, window, cx| {
5849 pane.close_clean_items(
5850 &CloseCleanItems {
5851 close_pinned: false,
5852 },
5853 window,
5854 cx,
5855 )
5856 })
5857 .await
5858 .unwrap();
5859 assert_item_labels(&pane, ["A^", "C*^"], cx);
5860 }
5861
5862 #[gpui::test]
5863 async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
5864 init_test(cx);
5865 let fs = FakeFs::new(cx.executor());
5866
5867 let project = Project::test(fs, None, cx).await;
5868 let (workspace, cx) =
5869 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5870 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5871
5872 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
5873
5874 pane.update_in(cx, |pane, window, cx| {
5875 pane.close_items_to_the_left_by_id(
5876 None,
5877 &CloseItemsToTheLeft {
5878 close_pinned: false,
5879 },
5880 window,
5881 cx,
5882 )
5883 })
5884 .await
5885 .unwrap();
5886 assert_item_labels(&pane, ["C*", "D", "E"], cx);
5887 }
5888
5889 #[gpui::test]
5890 async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
5891 init_test(cx);
5892 let fs = FakeFs::new(cx.executor());
5893
5894 let project = Project::test(fs, None, cx).await;
5895 let (workspace, cx) =
5896 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5897 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5898
5899 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
5900
5901 pane.update_in(cx, |pane, window, cx| {
5902 pane.close_items_to_the_right_by_id(
5903 None,
5904 &CloseItemsToTheRight {
5905 close_pinned: false,
5906 },
5907 window,
5908 cx,
5909 )
5910 })
5911 .await
5912 .unwrap();
5913 assert_item_labels(&pane, ["A", "B", "C*"], cx);
5914 }
5915
5916 #[gpui::test]
5917 async fn test_close_all_items(cx: &mut TestAppContext) {
5918 init_test(cx);
5919 let fs = FakeFs::new(cx.executor());
5920
5921 let project = Project::test(fs, None, cx).await;
5922 let (workspace, cx) =
5923 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5924 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5925
5926 let item_a = add_labeled_item(&pane, "A", false, cx);
5927 add_labeled_item(&pane, "B", false, cx);
5928 add_labeled_item(&pane, "C", false, cx);
5929 assert_item_labels(&pane, ["A", "B", "C*"], cx);
5930
5931 pane.update_in(cx, |pane, window, cx| {
5932 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
5933 pane.pin_tab_at(ix, window, cx);
5934 pane.close_all_items(
5935 &CloseAllItems {
5936 save_intent: None,
5937 close_pinned: false,
5938 },
5939 window,
5940 cx,
5941 )
5942 })
5943 .await
5944 .unwrap();
5945 assert_item_labels(&pane, ["A*!"], cx);
5946
5947 pane.update_in(cx, |pane, window, cx| {
5948 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
5949 pane.unpin_tab_at(ix, window, cx);
5950 pane.close_all_items(
5951 &CloseAllItems {
5952 save_intent: None,
5953 close_pinned: false,
5954 },
5955 window,
5956 cx,
5957 )
5958 })
5959 .await
5960 .unwrap();
5961
5962 assert_item_labels(&pane, [], cx);
5963
5964 add_labeled_item(&pane, "A", true, cx).update(cx, |item, cx| {
5965 item.project_items
5966 .push(TestProjectItem::new_dirty(1, "A.txt", cx))
5967 });
5968 add_labeled_item(&pane, "B", true, cx).update(cx, |item, cx| {
5969 item.project_items
5970 .push(TestProjectItem::new_dirty(2, "B.txt", cx))
5971 });
5972 add_labeled_item(&pane, "C", true, cx).update(cx, |item, cx| {
5973 item.project_items
5974 .push(TestProjectItem::new_dirty(3, "C.txt", cx))
5975 });
5976 assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
5977
5978 let save = pane.update_in(cx, |pane, window, cx| {
5979 pane.close_all_items(
5980 &CloseAllItems {
5981 save_intent: None,
5982 close_pinned: false,
5983 },
5984 window,
5985 cx,
5986 )
5987 });
5988
5989 cx.executor().run_until_parked();
5990 cx.simulate_prompt_answer("Save all");
5991 save.await.unwrap();
5992 assert_item_labels(&pane, [], cx);
5993
5994 add_labeled_item(&pane, "A", true, cx);
5995 add_labeled_item(&pane, "B", true, cx);
5996 add_labeled_item(&pane, "C", true, cx);
5997 assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
5998 let save = pane.update_in(cx, |pane, window, cx| {
5999 pane.close_all_items(
6000 &CloseAllItems {
6001 save_intent: None,
6002 close_pinned: false,
6003 },
6004 window,
6005 cx,
6006 )
6007 });
6008
6009 cx.executor().run_until_parked();
6010 cx.simulate_prompt_answer("Discard all");
6011 save.await.unwrap();
6012 assert_item_labels(&pane, [], cx);
6013 }
6014
6015 #[gpui::test]
6016 async fn test_close_with_save_intent(cx: &mut TestAppContext) {
6017 init_test(cx);
6018 let fs = FakeFs::new(cx.executor());
6019
6020 let project = Project::test(fs, None, cx).await;
6021 let (workspace, cx) =
6022 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
6023 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
6024
6025 let a = cx.update(|_, cx| TestProjectItem::new_dirty(1, "A.txt", cx));
6026 let b = cx.update(|_, cx| TestProjectItem::new_dirty(1, "B.txt", cx));
6027 let c = cx.update(|_, cx| TestProjectItem::new_dirty(1, "C.txt", cx));
6028
6029 add_labeled_item(&pane, "AB", true, cx).update(cx, |item, _| {
6030 item.project_items.push(a.clone());
6031 item.project_items.push(b.clone());
6032 });
6033 add_labeled_item(&pane, "C", true, cx)
6034 .update(cx, |item, _| item.project_items.push(c.clone()));
6035 assert_item_labels(&pane, ["AB^", "C*^"], cx);
6036
6037 pane.update_in(cx, |pane, window, cx| {
6038 pane.close_all_items(
6039 &CloseAllItems {
6040 save_intent: Some(SaveIntent::Save),
6041 close_pinned: false,
6042 },
6043 window,
6044 cx,
6045 )
6046 })
6047 .await
6048 .unwrap();
6049
6050 assert_item_labels(&pane, [], cx);
6051 cx.update(|_, cx| {
6052 assert!(!a.read(cx).is_dirty);
6053 assert!(!b.read(cx).is_dirty);
6054 assert!(!c.read(cx).is_dirty);
6055 });
6056 }
6057
6058 #[gpui::test]
6059 async fn test_close_all_items_including_pinned(cx: &mut TestAppContext) {
6060 init_test(cx);
6061 let fs = FakeFs::new(cx.executor());
6062
6063 let project = Project::test(fs, None, cx).await;
6064 let (workspace, cx) =
6065 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
6066 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
6067
6068 let item_a = add_labeled_item(&pane, "A", false, cx);
6069 add_labeled_item(&pane, "B", false, cx);
6070 add_labeled_item(&pane, "C", false, cx);
6071 assert_item_labels(&pane, ["A", "B", "C*"], cx);
6072
6073 pane.update_in(cx, |pane, window, cx| {
6074 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
6075 pane.pin_tab_at(ix, window, cx);
6076 pane.close_all_items(
6077 &CloseAllItems {
6078 save_intent: None,
6079 close_pinned: true,
6080 },
6081 window,
6082 cx,
6083 )
6084 })
6085 .await
6086 .unwrap();
6087 assert_item_labels(&pane, [], cx);
6088 }
6089
6090 #[gpui::test]
6091 async fn test_close_pinned_tab_with_non_pinned_in_same_pane(cx: &mut TestAppContext) {
6092 init_test(cx);
6093 let fs = FakeFs::new(cx.executor());
6094 let project = Project::test(fs, None, cx).await;
6095 let (workspace, cx) =
6096 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
6097
6098 // Non-pinned tabs in same pane
6099 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
6100 add_labeled_item(&pane, "A", false, cx);
6101 add_labeled_item(&pane, "B", false, cx);
6102 add_labeled_item(&pane, "C", false, cx);
6103 pane.update_in(cx, |pane, window, cx| {
6104 pane.pin_tab_at(0, window, cx);
6105 });
6106 set_labeled_items(&pane, ["A*", "B", "C"], cx);
6107 pane.update_in(cx, |pane, window, cx| {
6108 pane.close_active_item(
6109 &CloseActiveItem {
6110 save_intent: None,
6111 close_pinned: false,
6112 },
6113 window,
6114 cx,
6115 )
6116 .unwrap();
6117 });
6118 // Non-pinned tab should be active
6119 assert_item_labels(&pane, ["A!", "B*", "C"], cx);
6120 }
6121
6122 #[gpui::test]
6123 async fn test_close_pinned_tab_with_non_pinned_in_different_pane(cx: &mut TestAppContext) {
6124 init_test(cx);
6125 let fs = FakeFs::new(cx.executor());
6126 let project = Project::test(fs, None, cx).await;
6127 let (workspace, cx) =
6128 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
6129
6130 // No non-pinned tabs in same pane, non-pinned tabs in another pane
6131 let pane1 = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
6132 let pane2 = workspace.update_in(cx, |workspace, window, cx| {
6133 workspace.split_pane(pane1.clone(), SplitDirection::Right, window, cx)
6134 });
6135 add_labeled_item(&pane1, "A", false, cx);
6136 pane1.update_in(cx, |pane, window, cx| {
6137 pane.pin_tab_at(0, window, cx);
6138 });
6139 set_labeled_items(&pane1, ["A*"], cx);
6140 add_labeled_item(&pane2, "B", false, cx);
6141 set_labeled_items(&pane2, ["B"], cx);
6142 pane1.update_in(cx, |pane, window, cx| {
6143 pane.close_active_item(
6144 &CloseActiveItem {
6145 save_intent: None,
6146 close_pinned: false,
6147 },
6148 window,
6149 cx,
6150 )
6151 .unwrap();
6152 });
6153 // Non-pinned tab of other pane should be active
6154 assert_item_labels(&pane2, ["B*"], cx);
6155 }
6156
6157 #[gpui::test]
6158 async fn ensure_item_closing_actions_do_not_panic_when_no_items_exist(cx: &mut TestAppContext) {
6159 init_test(cx);
6160 let fs = FakeFs::new(cx.executor());
6161 let project = Project::test(fs, None, cx).await;
6162 let (workspace, cx) =
6163 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
6164
6165 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
6166 assert_item_labels(&pane, [], cx);
6167
6168 pane.update_in(cx, |pane, window, cx| {
6169 pane.close_active_item(
6170 &CloseActiveItem {
6171 save_intent: None,
6172 close_pinned: false,
6173 },
6174 window,
6175 cx,
6176 )
6177 })
6178 .await
6179 .unwrap();
6180
6181 pane.update_in(cx, |pane, window, cx| {
6182 pane.close_inactive_items(
6183 &CloseInactiveItems {
6184 save_intent: None,
6185 close_pinned: false,
6186 },
6187 window,
6188 cx,
6189 )
6190 })
6191 .await
6192 .unwrap();
6193
6194 pane.update_in(cx, |pane, window, cx| {
6195 pane.close_all_items(
6196 &CloseAllItems {
6197 save_intent: None,
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_clean_items(
6209 &CloseCleanItems {
6210 close_pinned: false,
6211 },
6212 window,
6213 cx,
6214 )
6215 })
6216 .await
6217 .unwrap();
6218
6219 pane.update_in(cx, |pane, window, cx| {
6220 pane.close_items_to_the_right_by_id(
6221 None,
6222 &CloseItemsToTheRight {
6223 close_pinned: false,
6224 },
6225 window,
6226 cx,
6227 )
6228 })
6229 .await
6230 .unwrap();
6231
6232 pane.update_in(cx, |pane, window, cx| {
6233 pane.close_items_to_the_left_by_id(
6234 None,
6235 &CloseItemsToTheLeft {
6236 close_pinned: false,
6237 },
6238 window,
6239 cx,
6240 )
6241 })
6242 .await
6243 .unwrap();
6244 }
6245
6246 fn init_test(cx: &mut TestAppContext) {
6247 cx.update(|cx| {
6248 let settings_store = SettingsStore::test(cx);
6249 cx.set_global(settings_store);
6250 theme::init(LoadThemes::JustBase, cx);
6251 crate::init_settings(cx);
6252 Project::init_settings(cx);
6253 });
6254 }
6255
6256 fn set_max_tabs(cx: &mut TestAppContext, value: Option<usize>) {
6257 cx.update_global(|store: &mut SettingsStore, cx| {
6258 store.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6259 settings.max_tabs = value.map(|v| NonZero::new(v).unwrap())
6260 });
6261 });
6262 }
6263
6264 fn add_labeled_item(
6265 pane: &Entity<Pane>,
6266 label: &str,
6267 is_dirty: bool,
6268 cx: &mut VisualTestContext,
6269 ) -> Box<Entity<TestItem>> {
6270 pane.update_in(cx, |pane, window, cx| {
6271 let labeled_item =
6272 Box::new(cx.new(|cx| TestItem::new(cx).with_label(label).with_dirty(is_dirty)));
6273 pane.add_item(labeled_item.clone(), false, false, None, window, cx);
6274 labeled_item
6275 })
6276 }
6277
6278 fn set_labeled_items<const COUNT: usize>(
6279 pane: &Entity<Pane>,
6280 labels: [&str; COUNT],
6281 cx: &mut VisualTestContext,
6282 ) -> [Box<Entity<TestItem>>; COUNT] {
6283 pane.update_in(cx, |pane, window, cx| {
6284 pane.items.clear();
6285 let mut active_item_index = 0;
6286
6287 let mut index = 0;
6288 let items = labels.map(|mut label| {
6289 if label.ends_with('*') {
6290 label = label.trim_end_matches('*');
6291 active_item_index = index;
6292 }
6293
6294 let labeled_item = Box::new(cx.new(|cx| TestItem::new(cx).with_label(label)));
6295 pane.add_item(labeled_item.clone(), false, false, None, window, cx);
6296 index += 1;
6297 labeled_item
6298 });
6299
6300 pane.activate_item(active_item_index, false, false, window, cx);
6301
6302 items
6303 })
6304 }
6305
6306 // Assert the item label, with the active item label suffixed with a '*'
6307 #[track_caller]
6308 fn assert_item_labels<const COUNT: usize>(
6309 pane: &Entity<Pane>,
6310 expected_states: [&str; COUNT],
6311 cx: &mut VisualTestContext,
6312 ) {
6313 let actual_states = pane.update(cx, |pane, cx| {
6314 pane.items
6315 .iter()
6316 .enumerate()
6317 .map(|(ix, item)| {
6318 let mut state = item
6319 .to_any()
6320 .downcast::<TestItem>()
6321 .unwrap()
6322 .read(cx)
6323 .label
6324 .clone();
6325 if ix == pane.active_item_index {
6326 state.push('*');
6327 }
6328 if item.is_dirty(cx) {
6329 state.push('^');
6330 }
6331 if pane.is_tab_pinned(ix) {
6332 state.push('!');
6333 }
6334 state
6335 })
6336 .collect::<Vec<_>>()
6337 });
6338 assert_eq!(
6339 actual_states, expected_states,
6340 "pane items do not match expectation"
6341 );
6342 }
6343}