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