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