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