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