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