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