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 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 self.pinned_tab_count = tab_count;
2796 }
2797 let unpinned_tabs = tab_items.split_off(self.pinned_tab_count);
2798 let pinned_tabs = tab_items;
2799 TabBar::new("tab_bar")
2800 .when(
2801 self.display_nav_history_buttons.unwrap_or_default(),
2802 |tab_bar| {
2803 tab_bar
2804 .start_child(navigate_backward)
2805 .start_child(navigate_forward)
2806 },
2807 )
2808 .map(|tab_bar| {
2809 if self.show_tab_bar_buttons {
2810 let render_tab_buttons = self.render_tab_bar_buttons.clone();
2811 let (left_children, right_children) = render_tab_buttons(self, window, cx);
2812 tab_bar
2813 .start_children(left_children)
2814 .end_children(right_children)
2815 } else {
2816 tab_bar
2817 }
2818 })
2819 .children(pinned_tabs.len().ne(&0).then(|| {
2820 let content_width = self.tab_bar_scroll_handle.content_size().width;
2821 let viewport_width = self.tab_bar_scroll_handle.viewport().size.width;
2822 // We need to check both because offset returns delta values even when the scroll handle is not scrollable
2823 let is_scrollable = content_width > viewport_width;
2824 let is_scrolled = self.tab_bar_scroll_handle.offset().x < px(0.);
2825 let has_active_unpinned_tab = self.active_item_index >= self.pinned_tab_count;
2826 h_flex()
2827 .children(pinned_tabs)
2828 .when(is_scrollable && is_scrolled, |this| {
2829 this.when(has_active_unpinned_tab, |this| this.border_r_2())
2830 .when(!has_active_unpinned_tab, |this| this.border_r_1())
2831 .border_color(cx.theme().colors().border)
2832 })
2833 }))
2834 .child(
2835 h_flex()
2836 .id("unpinned tabs")
2837 .overflow_x_scroll()
2838 .w_full()
2839 .track_scroll(&self.tab_bar_scroll_handle)
2840 .children(unpinned_tabs)
2841 .child(
2842 div()
2843 .id("tab_bar_drop_target")
2844 .min_w_6()
2845 // HACK: This empty child is currently necessary to force the drop target to appear
2846 // despite us setting a min width above.
2847 .child("")
2848 .h_full()
2849 .flex_grow()
2850 .drag_over::<DraggedTab>(|bar, _, _, cx| {
2851 bar.bg(cx.theme().colors().drop_target_background)
2852 })
2853 .drag_over::<DraggedSelection>(|bar, _, _, cx| {
2854 bar.bg(cx.theme().colors().drop_target_background)
2855 })
2856 .on_drop(cx.listener(
2857 move |this, dragged_tab: &DraggedTab, window, cx| {
2858 this.drag_split_direction = None;
2859 this.handle_tab_drop(dragged_tab, this.items.len(), window, cx)
2860 },
2861 ))
2862 .on_drop(cx.listener(
2863 move |this, selection: &DraggedSelection, window, cx| {
2864 this.drag_split_direction = None;
2865 this.handle_project_entry_drop(
2866 &selection.active_selection.entry_id,
2867 Some(tab_count),
2868 window,
2869 cx,
2870 )
2871 },
2872 ))
2873 .on_drop(cx.listener(move |this, paths, window, cx| {
2874 this.drag_split_direction = None;
2875 this.handle_external_paths_drop(paths, window, cx)
2876 }))
2877 .on_click(cx.listener(move |this, event: &ClickEvent, window, cx| {
2878 if event.up.click_count == 2 {
2879 window.dispatch_action(
2880 this.double_click_dispatch_action.boxed_clone(),
2881 cx,
2882 );
2883 }
2884 })),
2885 ),
2886 )
2887 .into_any_element()
2888 }
2889
2890 pub fn render_menu_overlay(menu: &Entity<ContextMenu>) -> Div {
2891 div().absolute().bottom_0().right_0().size_0().child(
2892 deferred(anchored().anchor(Corner::TopRight).child(menu.clone())).with_priority(1),
2893 )
2894 }
2895
2896 pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut Context<Self>) {
2897 self.zoomed = zoomed;
2898 cx.notify();
2899 }
2900
2901 pub fn is_zoomed(&self) -> bool {
2902 self.zoomed
2903 }
2904
2905 fn handle_drag_move<T: 'static>(
2906 &mut self,
2907 event: &DragMoveEvent<T>,
2908 window: &mut Window,
2909 cx: &mut Context<Self>,
2910 ) {
2911 let can_split_predicate = self.can_split_predicate.take();
2912 let can_split = match &can_split_predicate {
2913 Some(can_split_predicate) => {
2914 can_split_predicate(self, event.dragged_item(), window, cx)
2915 }
2916 None => false,
2917 };
2918 self.can_split_predicate = can_split_predicate;
2919 if !can_split {
2920 return;
2921 }
2922
2923 let rect = event.bounds.size;
2924
2925 let size = event.bounds.size.width.min(event.bounds.size.height)
2926 * WorkspaceSettings::get_global(cx).drop_target_size;
2927
2928 let relative_cursor = Point::new(
2929 event.event.position.x - event.bounds.left(),
2930 event.event.position.y - event.bounds.top(),
2931 );
2932
2933 let direction = if relative_cursor.x < size
2934 || relative_cursor.x > rect.width - size
2935 || relative_cursor.y < size
2936 || relative_cursor.y > rect.height - size
2937 {
2938 [
2939 SplitDirection::Up,
2940 SplitDirection::Right,
2941 SplitDirection::Down,
2942 SplitDirection::Left,
2943 ]
2944 .iter()
2945 .min_by_key(|side| match side {
2946 SplitDirection::Up => relative_cursor.y,
2947 SplitDirection::Right => rect.width - relative_cursor.x,
2948 SplitDirection::Down => rect.height - relative_cursor.y,
2949 SplitDirection::Left => relative_cursor.x,
2950 })
2951 .cloned()
2952 } else {
2953 None
2954 };
2955
2956 if direction != self.drag_split_direction {
2957 self.drag_split_direction = direction;
2958 }
2959 }
2960
2961 pub fn handle_tab_drop(
2962 &mut self,
2963 dragged_tab: &DraggedTab,
2964 ix: usize,
2965 window: &mut Window,
2966 cx: &mut Context<Self>,
2967 ) {
2968 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2969 if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_tab, window, cx) {
2970 return;
2971 }
2972 }
2973 let mut to_pane = cx.entity().clone();
2974 let split_direction = self.drag_split_direction;
2975 let item_id = dragged_tab.item.item_id();
2976 if let Some(preview_item_id) = self.preview_item_id {
2977 if item_id == preview_item_id {
2978 self.set_preview_item_id(None, cx);
2979 }
2980 }
2981
2982 let is_clone = cfg!(target_os = "macos") && window.modifiers().alt
2983 || cfg!(not(target_os = "macos")) && window.modifiers().control;
2984
2985 let from_pane = dragged_tab.pane.clone();
2986 let from_ix = dragged_tab.ix;
2987 self.workspace
2988 .update(cx, |_, cx| {
2989 cx.defer_in(window, move |workspace, window, cx| {
2990 if let Some(split_direction) = split_direction {
2991 to_pane = workspace.split_pane(to_pane, split_direction, window, cx);
2992 }
2993 let database_id = workspace.database_id();
2994 let was_pinned_in_from_pane = from_pane.read_with(cx, |pane, _| {
2995 pane.index_for_item_id(item_id)
2996 .is_some_and(|ix| pane.is_tab_pinned(ix))
2997 });
2998 let to_pane_old_length = to_pane.read(cx).items.len();
2999 if is_clone {
3000 let Some(item) = from_pane
3001 .read(cx)
3002 .items()
3003 .find(|item| item.item_id() == item_id)
3004 .map(|item| item.clone())
3005 else {
3006 return;
3007 };
3008 if let Some(item) = item.clone_on_split(database_id, window, cx) {
3009 to_pane.update(cx, |pane, cx| {
3010 pane.add_item(item, true, true, None, window, cx);
3011 })
3012 }
3013 } else {
3014 move_item(&from_pane, &to_pane, item_id, ix, true, window, cx);
3015 }
3016 to_pane.update(cx, |this, _| {
3017 if to_pane == from_pane {
3018 let moved_right = ix > from_ix;
3019 let ix = if moved_right { ix - 1 } else { ix };
3020 let is_pinned_in_to_pane = this.is_tab_pinned(ix);
3021
3022 if !was_pinned_in_from_pane && is_pinned_in_to_pane {
3023 this.pinned_tab_count += 1;
3024 } else if was_pinned_in_from_pane && !is_pinned_in_to_pane {
3025 this.pinned_tab_count -= 1;
3026 }
3027 } else if this.items.len() >= to_pane_old_length {
3028 let is_pinned_in_to_pane = this.is_tab_pinned(ix);
3029 let item_created_pane = to_pane_old_length == 0;
3030 let is_first_position = ix == 0;
3031 let was_dropped_at_beginning = item_created_pane || is_first_position;
3032 let should_remain_pinned = is_pinned_in_to_pane
3033 || (was_pinned_in_from_pane && was_dropped_at_beginning);
3034
3035 if should_remain_pinned {
3036 this.pinned_tab_count += 1;
3037 }
3038 }
3039 });
3040 });
3041 })
3042 .log_err();
3043 }
3044
3045 fn handle_dragged_selection_drop(
3046 &mut self,
3047 dragged_selection: &DraggedSelection,
3048 dragged_onto: Option<usize>,
3049 window: &mut Window,
3050 cx: &mut Context<Self>,
3051 ) {
3052 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
3053 if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_selection, window, cx)
3054 {
3055 return;
3056 }
3057 }
3058 self.handle_project_entry_drop(
3059 &dragged_selection.active_selection.entry_id,
3060 dragged_onto,
3061 window,
3062 cx,
3063 );
3064 }
3065
3066 fn handle_project_entry_drop(
3067 &mut self,
3068 project_entry_id: &ProjectEntryId,
3069 target: Option<usize>,
3070 window: &mut Window,
3071 cx: &mut Context<Self>,
3072 ) {
3073 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
3074 if let ControlFlow::Break(()) = custom_drop_handle(self, project_entry_id, window, cx) {
3075 return;
3076 }
3077 }
3078 let mut to_pane = cx.entity().clone();
3079 let split_direction = self.drag_split_direction;
3080 let project_entry_id = *project_entry_id;
3081 self.workspace
3082 .update(cx, |_, cx| {
3083 cx.defer_in(window, move |workspace, window, cx| {
3084 if let Some(project_path) = workspace
3085 .project()
3086 .read(cx)
3087 .path_for_entry(project_entry_id, cx)
3088 {
3089 let load_path_task = workspace.load_path(project_path.clone(), window, cx);
3090 cx.spawn_in(window, async move |workspace, cx| {
3091 if let Some((project_entry_id, build_item)) =
3092 load_path_task.await.notify_async_err(cx)
3093 {
3094 let (to_pane, new_item_handle) = workspace
3095 .update_in(cx, |workspace, window, cx| {
3096 if let Some(split_direction) = split_direction {
3097 to_pane = workspace.split_pane(
3098 to_pane,
3099 split_direction,
3100 window,
3101 cx,
3102 );
3103 }
3104 let new_item_handle = to_pane.update(cx, |pane, cx| {
3105 pane.open_item(
3106 project_entry_id,
3107 project_path,
3108 true,
3109 false,
3110 true,
3111 target,
3112 window,
3113 cx,
3114 build_item,
3115 )
3116 });
3117 (to_pane, new_item_handle)
3118 })
3119 .log_err()?;
3120 to_pane
3121 .update_in(cx, |this, window, cx| {
3122 let Some(index) = this.index_for_item(&*new_item_handle)
3123 else {
3124 return;
3125 };
3126
3127 if target.map_or(false, |target| this.is_tab_pinned(target))
3128 {
3129 this.pin_tab_at(index, window, cx);
3130 }
3131 })
3132 .ok()?
3133 }
3134 Some(())
3135 })
3136 .detach();
3137 };
3138 });
3139 })
3140 .log_err();
3141 }
3142
3143 fn handle_external_paths_drop(
3144 &mut self,
3145 paths: &ExternalPaths,
3146 window: &mut Window,
3147 cx: &mut Context<Self>,
3148 ) {
3149 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
3150 if let ControlFlow::Break(()) = custom_drop_handle(self, paths, window, cx) {
3151 return;
3152 }
3153 }
3154 let mut to_pane = cx.entity().clone();
3155 let mut split_direction = self.drag_split_direction;
3156 let paths = paths.paths().to_vec();
3157 let is_remote = self
3158 .workspace
3159 .update(cx, |workspace, cx| {
3160 if workspace.project().read(cx).is_via_collab() {
3161 workspace.show_error(
3162 &anyhow::anyhow!("Cannot drop files on a remote project"),
3163 cx,
3164 );
3165 true
3166 } else {
3167 false
3168 }
3169 })
3170 .unwrap_or(true);
3171 if is_remote {
3172 return;
3173 }
3174
3175 self.workspace
3176 .update(cx, |workspace, cx| {
3177 let fs = Arc::clone(workspace.project().read(cx).fs());
3178 cx.spawn_in(window, async move |workspace, cx| {
3179 let mut is_file_checks = FuturesUnordered::new();
3180 for path in &paths {
3181 is_file_checks.push(fs.is_file(path))
3182 }
3183 let mut has_files_to_open = false;
3184 while let Some(is_file) = is_file_checks.next().await {
3185 if is_file {
3186 has_files_to_open = true;
3187 break;
3188 }
3189 }
3190 drop(is_file_checks);
3191 if !has_files_to_open {
3192 split_direction = None;
3193 }
3194
3195 if let Ok(open_task) = workspace.update_in(cx, |workspace, window, cx| {
3196 if let Some(split_direction) = split_direction {
3197 to_pane = workspace.split_pane(to_pane, split_direction, window, cx);
3198 }
3199 workspace.open_paths(
3200 paths,
3201 OpenOptions {
3202 visible: Some(OpenVisible::OnlyDirectories),
3203 ..Default::default()
3204 },
3205 Some(to_pane.downgrade()),
3206 window,
3207 cx,
3208 )
3209 }) {
3210 let opened_items: Vec<_> = open_task.await;
3211 _ = workspace.update(cx, |workspace, cx| {
3212 for item in opened_items.into_iter().flatten() {
3213 if let Err(e) = item {
3214 workspace.show_error(&e, cx);
3215 }
3216 }
3217 });
3218 }
3219 })
3220 .detach();
3221 })
3222 .log_err();
3223 }
3224
3225 pub fn display_nav_history_buttons(&mut self, display: Option<bool>) {
3226 self.display_nav_history_buttons = display;
3227 }
3228
3229 fn pinned_item_ids(&self) -> Vec<EntityId> {
3230 self.items
3231 .iter()
3232 .enumerate()
3233 .filter_map(|(index, item)| {
3234 if self.is_tab_pinned(index) {
3235 return Some(item.item_id());
3236 }
3237
3238 None
3239 })
3240 .collect()
3241 }
3242
3243 fn clean_item_ids(&self, cx: &mut Context<Pane>) -> Vec<EntityId> {
3244 self.items()
3245 .filter_map(|item| {
3246 if !item.is_dirty(cx) {
3247 return Some(item.item_id());
3248 }
3249
3250 None
3251 })
3252 .collect()
3253 }
3254
3255 fn to_the_side_item_ids(&self, item_id: EntityId, side: Side) -> Vec<EntityId> {
3256 match side {
3257 Side::Left => self
3258 .items()
3259 .take_while(|item| item.item_id() != item_id)
3260 .map(|item| item.item_id())
3261 .collect(),
3262 Side::Right => self
3263 .items()
3264 .rev()
3265 .take_while(|item| item.item_id() != item_id)
3266 .map(|item| item.item_id())
3267 .collect(),
3268 }
3269 }
3270
3271 pub fn drag_split_direction(&self) -> Option<SplitDirection> {
3272 self.drag_split_direction
3273 }
3274
3275 pub fn set_zoom_out_on_close(&mut self, zoom_out_on_close: bool) {
3276 self.zoom_out_on_close = zoom_out_on_close;
3277 }
3278}
3279
3280fn default_render_tab_bar_buttons(
3281 pane: &mut Pane,
3282 window: &mut Window,
3283 cx: &mut Context<Pane>,
3284) -> (Option<AnyElement>, Option<AnyElement>) {
3285 if !pane.has_focus(window, cx) && !pane.context_menu_focused(window, cx) {
3286 return (None, None);
3287 }
3288 // Ideally we would return a vec of elements here to pass directly to the [TabBar]'s
3289 // `end_slot`, but due to needing a view here that isn't possible.
3290 let right_children = h_flex()
3291 // Instead we need to replicate the spacing from the [TabBar]'s `end_slot` here.
3292 .gap(DynamicSpacing::Base04.rems(cx))
3293 .child(
3294 PopoverMenu::new("pane-tab-bar-popover-menu")
3295 .trigger_with_tooltip(
3296 IconButton::new("plus", IconName::Plus).icon_size(IconSize::Small),
3297 Tooltip::text("New..."),
3298 )
3299 .anchor(Corner::TopRight)
3300 .with_handle(pane.new_item_context_menu_handle.clone())
3301 .menu(move |window, cx| {
3302 Some(ContextMenu::build(window, cx, |menu, _, _| {
3303 menu.action("New File", NewFile.boxed_clone())
3304 .action("Open File", ToggleFileFinder::default().boxed_clone())
3305 .separator()
3306 .action(
3307 "Search Project",
3308 DeploySearch {
3309 replace_enabled: false,
3310 included_files: None,
3311 excluded_files: None,
3312 }
3313 .boxed_clone(),
3314 )
3315 .action("Search Symbols", ToggleProjectSymbols.boxed_clone())
3316 .separator()
3317 .action("New Terminal", NewTerminal.boxed_clone())
3318 }))
3319 }),
3320 )
3321 .child(
3322 PopoverMenu::new("pane-tab-bar-split")
3323 .trigger_with_tooltip(
3324 IconButton::new("split", IconName::Split).icon_size(IconSize::Small),
3325 Tooltip::text("Split Pane"),
3326 )
3327 .anchor(Corner::TopRight)
3328 .with_handle(pane.split_item_context_menu_handle.clone())
3329 .menu(move |window, cx| {
3330 ContextMenu::build(window, cx, |menu, _, _| {
3331 menu.action("Split Right", SplitRight.boxed_clone())
3332 .action("Split Left", SplitLeft.boxed_clone())
3333 .action("Split Up", SplitUp.boxed_clone())
3334 .action("Split Down", SplitDown.boxed_clone())
3335 })
3336 .into()
3337 }),
3338 )
3339 .child({
3340 let zoomed = pane.is_zoomed();
3341 IconButton::new("toggle_zoom", IconName::Maximize)
3342 .icon_size(IconSize::Small)
3343 .toggle_state(zoomed)
3344 .selected_icon(IconName::Minimize)
3345 .on_click(cx.listener(|pane, _, window, cx| {
3346 pane.toggle_zoom(&crate::ToggleZoom, window, cx);
3347 }))
3348 .tooltip(move |window, cx| {
3349 Tooltip::for_action(
3350 if zoomed { "Zoom Out" } else { "Zoom In" },
3351 &ToggleZoom,
3352 window,
3353 cx,
3354 )
3355 })
3356 })
3357 .into_any_element()
3358 .into();
3359 (None, right_children)
3360}
3361
3362impl Focusable for Pane {
3363 fn focus_handle(&self, _cx: &App) -> FocusHandle {
3364 self.focus_handle.clone()
3365 }
3366}
3367
3368impl Render for Pane {
3369 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3370 let mut key_context = KeyContext::new_with_defaults();
3371 key_context.add("Pane");
3372 if self.active_item().is_none() {
3373 key_context.add("EmptyPane");
3374 }
3375
3376 let should_display_tab_bar = self.should_display_tab_bar.clone();
3377 let display_tab_bar = should_display_tab_bar(window, cx);
3378 let Some(project) = self.project.upgrade() else {
3379 return div().track_focus(&self.focus_handle(cx));
3380 };
3381 let is_local = project.read(cx).is_local();
3382
3383 v_flex()
3384 .key_context(key_context)
3385 .track_focus(&self.focus_handle(cx))
3386 .size_full()
3387 .flex_none()
3388 .overflow_hidden()
3389 .on_action(cx.listener(|pane, _: &AlternateFile, window, cx| {
3390 pane.alternate_file(window, cx);
3391 }))
3392 .on_action(
3393 cx.listener(|pane, _: &SplitLeft, _, cx| pane.split(SplitDirection::Left, cx)),
3394 )
3395 .on_action(cx.listener(|pane, _: &SplitUp, _, cx| pane.split(SplitDirection::Up, cx)))
3396 .on_action(cx.listener(|pane, _: &SplitHorizontal, _, cx| {
3397 pane.split(SplitDirection::horizontal(cx), cx)
3398 }))
3399 .on_action(cx.listener(|pane, _: &SplitVertical, _, cx| {
3400 pane.split(SplitDirection::vertical(cx), cx)
3401 }))
3402 .on_action(
3403 cx.listener(|pane, _: &SplitRight, _, cx| pane.split(SplitDirection::Right, cx)),
3404 )
3405 .on_action(
3406 cx.listener(|pane, _: &SplitDown, _, cx| pane.split(SplitDirection::Down, cx)),
3407 )
3408 .on_action(
3409 cx.listener(|pane, _: &GoBack, window, cx| pane.navigate_backward(window, cx)),
3410 )
3411 .on_action(
3412 cx.listener(|pane, _: &GoForward, window, cx| pane.navigate_forward(window, cx)),
3413 )
3414 .on_action(cx.listener(|_, _: &JoinIntoNext, _, cx| {
3415 cx.emit(Event::JoinIntoNext);
3416 }))
3417 .on_action(cx.listener(|_, _: &JoinAll, _, cx| {
3418 cx.emit(Event::JoinAll);
3419 }))
3420 .on_action(cx.listener(Pane::toggle_zoom))
3421 .on_action(
3422 cx.listener(|pane: &mut Pane, action: &ActivateItem, window, cx| {
3423 pane.activate_item(
3424 action.0.min(pane.items.len().saturating_sub(1)),
3425 true,
3426 true,
3427 window,
3428 cx,
3429 );
3430 }),
3431 )
3432 .on_action(
3433 cx.listener(|pane: &mut Pane, _: &ActivateLastItem, window, cx| {
3434 pane.activate_item(pane.items.len().saturating_sub(1), true, true, window, cx);
3435 }),
3436 )
3437 .on_action(
3438 cx.listener(|pane: &mut Pane, _: &ActivatePreviousItem, window, cx| {
3439 pane.activate_prev_item(true, window, cx);
3440 }),
3441 )
3442 .on_action(
3443 cx.listener(|pane: &mut Pane, _: &ActivateNextItem, window, cx| {
3444 pane.activate_next_item(true, window, cx);
3445 }),
3446 )
3447 .on_action(
3448 cx.listener(|pane, _: &SwapItemLeft, window, cx| pane.swap_item_left(window, cx)),
3449 )
3450 .on_action(
3451 cx.listener(|pane, _: &SwapItemRight, window, cx| pane.swap_item_right(window, cx)),
3452 )
3453 .on_action(cx.listener(|pane, action, window, cx| {
3454 pane.toggle_pin_tab(action, window, cx);
3455 }))
3456 .on_action(cx.listener(|pane, action, window, cx| {
3457 pane.unpin_all_tabs(action, window, cx);
3458 }))
3459 .when(PreviewTabsSettings::get_global(cx).enabled, |this| {
3460 this.on_action(cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, _, cx| {
3461 if let Some(active_item_id) = pane.active_item().map(|i| i.item_id()) {
3462 if pane.is_active_preview_item(active_item_id) {
3463 pane.set_preview_item_id(None, cx);
3464 } else {
3465 pane.set_preview_item_id(Some(active_item_id), cx);
3466 }
3467 }
3468 }))
3469 })
3470 .on_action(
3471 cx.listener(|pane: &mut Self, action: &CloseActiveItem, window, cx| {
3472 pane.close_active_item(action, window, cx)
3473 .detach_and_log_err(cx)
3474 }),
3475 )
3476 .on_action(
3477 cx.listener(|pane: &mut Self, action: &CloseInactiveItems, window, cx| {
3478 pane.close_inactive_items(action, window, cx)
3479 .detach_and_log_err(cx);
3480 }),
3481 )
3482 .on_action(
3483 cx.listener(|pane: &mut Self, action: &CloseCleanItems, window, cx| {
3484 pane.close_clean_items(action, window, cx)
3485 .detach_and_log_err(cx)
3486 }),
3487 )
3488 .on_action(cx.listener(
3489 |pane: &mut Self, action: &CloseItemsToTheLeft, window, cx| {
3490 pane.close_items_to_the_left_by_id(None, action, window, cx)
3491 .detach_and_log_err(cx)
3492 },
3493 ))
3494 .on_action(cx.listener(
3495 |pane: &mut Self, action: &CloseItemsToTheRight, window, cx| {
3496 pane.close_items_to_the_right_by_id(None, action, window, cx)
3497 .detach_and_log_err(cx)
3498 },
3499 ))
3500 .on_action(
3501 cx.listener(|pane: &mut Self, action: &CloseAllItems, window, cx| {
3502 pane.close_all_items(action, window, cx)
3503 .detach_and_log_err(cx)
3504 }),
3505 )
3506 .on_action(
3507 cx.listener(|pane: &mut Self, action: &RevealInProjectPanel, _, cx| {
3508 let entry_id = action
3509 .entry_id
3510 .map(ProjectEntryId::from_proto)
3511 .or_else(|| pane.active_item()?.project_entry_ids(cx).first().copied());
3512 if let Some(entry_id) = entry_id {
3513 pane.project
3514 .update(cx, |_, cx| {
3515 cx.emit(project::Event::RevealInProjectPanel(entry_id))
3516 })
3517 .ok();
3518 }
3519 }),
3520 )
3521 .on_action(cx.listener(|_, _: &menu::Cancel, window, cx| {
3522 if cx.stop_active_drag(window) {
3523 return;
3524 } else {
3525 cx.propagate();
3526 }
3527 }))
3528 .when(self.active_item().is_some() && display_tab_bar, |pane| {
3529 pane.child((self.render_tab_bar.clone())(self, window, cx))
3530 })
3531 .child({
3532 let has_worktrees = project.read(cx).visible_worktrees(cx).next().is_some();
3533 // main content
3534 div()
3535 .flex_1()
3536 .relative()
3537 .group("")
3538 .overflow_hidden()
3539 .on_drag_move::<DraggedTab>(cx.listener(Self::handle_drag_move))
3540 .on_drag_move::<DraggedSelection>(cx.listener(Self::handle_drag_move))
3541 .when(is_local, |div| {
3542 div.on_drag_move::<ExternalPaths>(cx.listener(Self::handle_drag_move))
3543 })
3544 .map(|div| {
3545 if let Some(item) = self.active_item() {
3546 div.id("pane_placeholder")
3547 .v_flex()
3548 .size_full()
3549 .overflow_hidden()
3550 .child(self.toolbar.clone())
3551 .child(item.to_any())
3552 } else {
3553 let placeholder = div
3554 .id("pane_placeholder")
3555 .h_flex()
3556 .size_full()
3557 .justify_center()
3558 .on_click(cx.listener(
3559 move |this, event: &ClickEvent, window, cx| {
3560 if event.up.click_count == 2 {
3561 window.dispatch_action(
3562 this.double_click_dispatch_action.boxed_clone(),
3563 cx,
3564 );
3565 }
3566 },
3567 ));
3568 if has_worktrees {
3569 placeholder
3570 } else {
3571 placeholder.child(
3572 Label::new("Open a file or project to get started.")
3573 .color(Color::Muted),
3574 )
3575 }
3576 }
3577 })
3578 .child(
3579 // drag target
3580 div()
3581 .invisible()
3582 .absolute()
3583 .bg(cx.theme().colors().drop_target_background)
3584 .group_drag_over::<DraggedTab>("", |style| style.visible())
3585 .group_drag_over::<DraggedSelection>("", |style| style.visible())
3586 .when(is_local, |div| {
3587 div.group_drag_over::<ExternalPaths>("", |style| style.visible())
3588 })
3589 .when_some(self.can_drop_predicate.clone(), |this, p| {
3590 this.can_drop(move |a, window, cx| p(a, window, cx))
3591 })
3592 .on_drop(cx.listener(move |this, dragged_tab, window, cx| {
3593 this.handle_tab_drop(
3594 dragged_tab,
3595 this.active_item_index(),
3596 window,
3597 cx,
3598 )
3599 }))
3600 .on_drop(cx.listener(
3601 move |this, selection: &DraggedSelection, window, cx| {
3602 this.handle_dragged_selection_drop(selection, None, window, cx)
3603 },
3604 ))
3605 .on_drop(cx.listener(move |this, paths, window, cx| {
3606 this.handle_external_paths_drop(paths, window, cx)
3607 }))
3608 .map(|div| {
3609 let size = DefiniteLength::Fraction(0.5);
3610 match self.drag_split_direction {
3611 None => div.top_0().right_0().bottom_0().left_0(),
3612 Some(SplitDirection::Up) => {
3613 div.top_0().left_0().right_0().h(size)
3614 }
3615 Some(SplitDirection::Down) => {
3616 div.left_0().bottom_0().right_0().h(size)
3617 }
3618 Some(SplitDirection::Left) => {
3619 div.top_0().left_0().bottom_0().w(size)
3620 }
3621 Some(SplitDirection::Right) => {
3622 div.top_0().bottom_0().right_0().w(size)
3623 }
3624 }
3625 }),
3626 )
3627 })
3628 .on_mouse_down(
3629 MouseButton::Navigate(NavigationDirection::Back),
3630 cx.listener(|pane, _, window, cx| {
3631 if let Some(workspace) = pane.workspace.upgrade() {
3632 let pane = cx.entity().downgrade();
3633 window.defer(cx, move |window, cx| {
3634 workspace.update(cx, |workspace, cx| {
3635 workspace.go_back(pane, window, cx).detach_and_log_err(cx)
3636 })
3637 })
3638 }
3639 }),
3640 )
3641 .on_mouse_down(
3642 MouseButton::Navigate(NavigationDirection::Forward),
3643 cx.listener(|pane, _, window, cx| {
3644 if let Some(workspace) = pane.workspace.upgrade() {
3645 let pane = cx.entity().downgrade();
3646 window.defer(cx, move |window, cx| {
3647 workspace.update(cx, |workspace, cx| {
3648 workspace
3649 .go_forward(pane, window, cx)
3650 .detach_and_log_err(cx)
3651 })
3652 })
3653 }
3654 }),
3655 )
3656 }
3657}
3658
3659impl ItemNavHistory {
3660 pub fn push<D: 'static + Send + Any>(&mut self, data: Option<D>, cx: &mut App) {
3661 if self
3662 .item
3663 .upgrade()
3664 .is_some_and(|item| item.include_in_nav_history())
3665 {
3666 self.history
3667 .push(data, self.item.clone(), self.is_preview, cx);
3668 }
3669 }
3670
3671 pub fn pop_backward(&mut self, cx: &mut App) -> Option<NavigationEntry> {
3672 self.history.pop(NavigationMode::GoingBack, cx)
3673 }
3674
3675 pub fn pop_forward(&mut self, cx: &mut App) -> Option<NavigationEntry> {
3676 self.history.pop(NavigationMode::GoingForward, cx)
3677 }
3678}
3679
3680impl NavHistory {
3681 pub fn for_each_entry(
3682 &self,
3683 cx: &App,
3684 mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
3685 ) {
3686 let borrowed_history = self.0.lock();
3687 borrowed_history
3688 .forward_stack
3689 .iter()
3690 .chain(borrowed_history.backward_stack.iter())
3691 .chain(borrowed_history.closed_stack.iter())
3692 .for_each(|entry| {
3693 if let Some(project_and_abs_path) =
3694 borrowed_history.paths_by_item.get(&entry.item.id())
3695 {
3696 f(entry, project_and_abs_path.clone());
3697 } else if let Some(item) = entry.item.upgrade() {
3698 if let Some(path) = item.project_path(cx) {
3699 f(entry, (path, None));
3700 }
3701 }
3702 })
3703 }
3704
3705 pub fn set_mode(&mut self, mode: NavigationMode) {
3706 self.0.lock().mode = mode;
3707 }
3708
3709 pub fn mode(&self) -> NavigationMode {
3710 self.0.lock().mode
3711 }
3712
3713 pub fn disable(&mut self) {
3714 self.0.lock().mode = NavigationMode::Disabled;
3715 }
3716
3717 pub fn enable(&mut self) {
3718 self.0.lock().mode = NavigationMode::Normal;
3719 }
3720
3721 pub fn pop(&mut self, mode: NavigationMode, cx: &mut App) -> Option<NavigationEntry> {
3722 let mut state = self.0.lock();
3723 let entry = match mode {
3724 NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
3725 return None;
3726 }
3727 NavigationMode::GoingBack => &mut state.backward_stack,
3728 NavigationMode::GoingForward => &mut state.forward_stack,
3729 NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
3730 }
3731 .pop_back();
3732 if entry.is_some() {
3733 state.did_update(cx);
3734 }
3735 entry
3736 }
3737
3738 pub fn push<D: 'static + Send + Any>(
3739 &mut self,
3740 data: Option<D>,
3741 item: Arc<dyn WeakItemHandle>,
3742 is_preview: bool,
3743 cx: &mut App,
3744 ) {
3745 let state = &mut *self.0.lock();
3746 match state.mode {
3747 NavigationMode::Disabled => {}
3748 NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
3749 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3750 state.backward_stack.pop_front();
3751 }
3752 state.backward_stack.push_back(NavigationEntry {
3753 item,
3754 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3755 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3756 is_preview,
3757 });
3758 state.forward_stack.clear();
3759 }
3760 NavigationMode::GoingBack => {
3761 if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3762 state.forward_stack.pop_front();
3763 }
3764 state.forward_stack.push_back(NavigationEntry {
3765 item,
3766 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3767 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3768 is_preview,
3769 });
3770 }
3771 NavigationMode::GoingForward => {
3772 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3773 state.backward_stack.pop_front();
3774 }
3775 state.backward_stack.push_back(NavigationEntry {
3776 item,
3777 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3778 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3779 is_preview,
3780 });
3781 }
3782 NavigationMode::ClosingItem => {
3783 if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3784 state.closed_stack.pop_front();
3785 }
3786 state.closed_stack.push_back(NavigationEntry {
3787 item,
3788 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3789 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3790 is_preview,
3791 });
3792 }
3793 }
3794 state.did_update(cx);
3795 }
3796
3797 pub fn remove_item(&mut self, item_id: EntityId) {
3798 let mut state = self.0.lock();
3799 state.paths_by_item.remove(&item_id);
3800 state
3801 .backward_stack
3802 .retain(|entry| entry.item.id() != item_id);
3803 state
3804 .forward_stack
3805 .retain(|entry| entry.item.id() != item_id);
3806 state
3807 .closed_stack
3808 .retain(|entry| entry.item.id() != item_id);
3809 }
3810
3811 pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option<PathBuf>)> {
3812 self.0.lock().paths_by_item.get(&item_id).cloned()
3813 }
3814}
3815
3816impl NavHistoryState {
3817 pub fn did_update(&self, cx: &mut App) {
3818 if let Some(pane) = self.pane.upgrade() {
3819 cx.defer(move |cx| {
3820 pane.update(cx, |pane, cx| pane.history_updated(cx));
3821 });
3822 }
3823 }
3824}
3825
3826fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
3827 let path = buffer_path
3828 .as_ref()
3829 .and_then(|p| {
3830 p.path
3831 .to_str()
3832 .and_then(|s| if s.is_empty() { None } else { Some(s) })
3833 })
3834 .unwrap_or("This buffer");
3835 let path = truncate_and_remove_front(path, 80);
3836 format!("{path} contains unsaved edits. Do you want to save it?")
3837}
3838
3839pub fn tab_details(items: &[Box<dyn ItemHandle>], _window: &Window, cx: &App) -> Vec<usize> {
3840 let mut tab_details = items.iter().map(|_| 0).collect::<Vec<_>>();
3841 let mut tab_descriptions = HashMap::default();
3842 let mut done = false;
3843 while !done {
3844 done = true;
3845
3846 // Store item indices by their tab description.
3847 for (ix, (item, detail)) in items.iter().zip(&tab_details).enumerate() {
3848 let description = item.tab_content_text(*detail, cx);
3849 if *detail == 0 || description != item.tab_content_text(detail - 1, cx) {
3850 tab_descriptions
3851 .entry(description)
3852 .or_insert(Vec::new())
3853 .push(ix);
3854 }
3855 }
3856
3857 // If two or more items have the same tab description, increase their level
3858 // of detail and try again.
3859 for (_, item_ixs) in tab_descriptions.drain() {
3860 if item_ixs.len() > 1 {
3861 done = false;
3862 for ix in item_ixs {
3863 tab_details[ix] += 1;
3864 }
3865 }
3866 }
3867 }
3868
3869 tab_details
3870}
3871
3872pub fn render_item_indicator(item: Box<dyn ItemHandle>, cx: &App) -> Option<Indicator> {
3873 maybe!({
3874 let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
3875 (true, _) => Color::Warning,
3876 (_, true) => Color::Accent,
3877 (false, false) => return None,
3878 };
3879
3880 Some(Indicator::dot().color(indicator_color))
3881 })
3882}
3883
3884impl Render for DraggedTab {
3885 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3886 let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
3887 let label = self.item.tab_content(
3888 TabContentParams {
3889 detail: Some(self.detail),
3890 selected: false,
3891 preview: false,
3892 deemphasized: false,
3893 },
3894 window,
3895 cx,
3896 );
3897 Tab::new("")
3898 .toggle_state(self.is_active)
3899 .child(label)
3900 .render(window, cx)
3901 .font(ui_font)
3902 }
3903}
3904
3905#[cfg(test)]
3906mod tests {
3907 use std::num::NonZero;
3908
3909 use super::*;
3910 use crate::item::test::{TestItem, TestProjectItem};
3911 use gpui::{TestAppContext, VisualTestContext};
3912 use project::FakeFs;
3913 use settings::SettingsStore;
3914 use theme::LoadThemes;
3915 use util::TryFutureExt;
3916
3917 #[gpui::test]
3918 async fn test_add_item_capped_to_max_tabs(cx: &mut TestAppContext) {
3919 init_test(cx);
3920 let fs = FakeFs::new(cx.executor());
3921
3922 let project = Project::test(fs, None, cx).await;
3923 let (workspace, cx) =
3924 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3925 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3926
3927 for i in 0..7 {
3928 add_labeled_item(&pane, format!("{}", i).as_str(), false, cx);
3929 }
3930
3931 set_max_tabs(cx, Some(5));
3932 add_labeled_item(&pane, "7", false, cx);
3933 // Remove items to respect the max tab cap.
3934 assert_item_labels(&pane, ["3", "4", "5", "6", "7*"], cx);
3935 pane.update_in(cx, |pane, window, cx| {
3936 pane.activate_item(0, false, false, window, cx);
3937 });
3938 add_labeled_item(&pane, "X", false, cx);
3939 // Respect activation order.
3940 assert_item_labels(&pane, ["3", "X*", "5", "6", "7"], cx);
3941
3942 for i in 0..7 {
3943 add_labeled_item(&pane, format!("D{}", i).as_str(), true, cx);
3944 }
3945 // Keeps dirty items, even over max tab cap.
3946 assert_item_labels(
3947 &pane,
3948 ["D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6*^"],
3949 cx,
3950 );
3951
3952 set_max_tabs(cx, None);
3953 for i in 0..7 {
3954 add_labeled_item(&pane, format!("N{}", i).as_str(), false, cx);
3955 }
3956 // No cap when max tabs is None.
3957 assert_item_labels(
3958 &pane,
3959 [
3960 "D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6^", "N0", "N1", "N2", "N3", "N4",
3961 "N5", "N6*",
3962 ],
3963 cx,
3964 );
3965 }
3966
3967 #[gpui::test]
3968 async fn test_reduce_max_tabs_closes_existing_items(cx: &mut TestAppContext) {
3969 init_test(cx);
3970 let fs = FakeFs::new(cx.executor());
3971
3972 let project = Project::test(fs, None, cx).await;
3973 let (workspace, cx) =
3974 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3975 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3976
3977 add_labeled_item(&pane, "A", false, cx);
3978 add_labeled_item(&pane, "B", false, cx);
3979 let item_c = add_labeled_item(&pane, "C", false, cx);
3980 let item_d = add_labeled_item(&pane, "D", false, cx);
3981 add_labeled_item(&pane, "E", false, cx);
3982 add_labeled_item(&pane, "Settings", false, cx);
3983 assert_item_labels(&pane, ["A", "B", "C", "D", "E", "Settings*"], cx);
3984
3985 set_max_tabs(cx, Some(5));
3986 assert_item_labels(&pane, ["B", "C", "D", "E", "Settings*"], cx);
3987
3988 set_max_tabs(cx, Some(4));
3989 assert_item_labels(&pane, ["C", "D", "E", "Settings*"], cx);
3990
3991 pane.update_in(cx, |pane, window, cx| {
3992 let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
3993 pane.pin_tab_at(ix, window, cx);
3994
3995 let ix = pane.index_for_item_id(item_d.item_id()).unwrap();
3996 pane.pin_tab_at(ix, window, cx);
3997 });
3998 assert_item_labels(&pane, ["C!", "D!", "E", "Settings*"], cx);
3999
4000 set_max_tabs(cx, Some(2));
4001 assert_item_labels(&pane, ["C!", "D!", "Settings*"], cx);
4002 }
4003
4004 #[gpui::test]
4005 async fn test_allow_pinning_dirty_item_at_max_tabs(cx: &mut TestAppContext) {
4006 init_test(cx);
4007 let fs = FakeFs::new(cx.executor());
4008
4009 let project = Project::test(fs, None, cx).await;
4010 let (workspace, cx) =
4011 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4012 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4013
4014 set_max_tabs(cx, Some(1));
4015 let item_a = add_labeled_item(&pane, "A", true, cx);
4016
4017 pane.update_in(cx, |pane, window, cx| {
4018 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4019 pane.pin_tab_at(ix, window, cx);
4020 });
4021 assert_item_labels(&pane, ["A*^!"], cx);
4022 }
4023
4024 #[gpui::test]
4025 async fn test_allow_pinning_non_dirty_item_at_max_tabs(cx: &mut TestAppContext) {
4026 init_test(cx);
4027 let fs = FakeFs::new(cx.executor());
4028
4029 let project = Project::test(fs, None, cx).await;
4030 let (workspace, cx) =
4031 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4032 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4033
4034 set_max_tabs(cx, Some(1));
4035 let item_a = add_labeled_item(&pane, "A", false, cx);
4036
4037 pane.update_in(cx, |pane, window, cx| {
4038 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4039 pane.pin_tab_at(ix, window, cx);
4040 });
4041 assert_item_labels(&pane, ["A*!"], cx);
4042 }
4043
4044 #[gpui::test]
4045 async fn test_pin_tabs_incrementally_at_max_capacity(cx: &mut TestAppContext) {
4046 init_test(cx);
4047 let fs = FakeFs::new(cx.executor());
4048
4049 let project = Project::test(fs, None, cx).await;
4050 let (workspace, cx) =
4051 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4052 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4053
4054 set_max_tabs(cx, Some(3));
4055
4056 let item_a = add_labeled_item(&pane, "A", false, cx);
4057 assert_item_labels(&pane, ["A*"], cx);
4058
4059 pane.update_in(cx, |pane, window, cx| {
4060 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4061 pane.pin_tab_at(ix, window, cx);
4062 });
4063 assert_item_labels(&pane, ["A*!"], cx);
4064
4065 let item_b = add_labeled_item(&pane, "B", false, cx);
4066 assert_item_labels(&pane, ["A!", "B*"], cx);
4067
4068 pane.update_in(cx, |pane, window, cx| {
4069 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4070 pane.pin_tab_at(ix, window, cx);
4071 });
4072 assert_item_labels(&pane, ["A!", "B*!"], cx);
4073
4074 let item_c = add_labeled_item(&pane, "C", false, cx);
4075 assert_item_labels(&pane, ["A!", "B!", "C*"], cx);
4076
4077 pane.update_in(cx, |pane, window, cx| {
4078 let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4079 pane.pin_tab_at(ix, window, cx);
4080 });
4081 assert_item_labels(&pane, ["A!", "B!", "C*!"], cx);
4082 }
4083
4084 #[gpui::test]
4085 async fn test_pin_tabs_left_to_right_after_opening_at_max_capacity(cx: &mut TestAppContext) {
4086 init_test(cx);
4087 let fs = FakeFs::new(cx.executor());
4088
4089 let project = Project::test(fs, None, cx).await;
4090 let (workspace, cx) =
4091 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4092 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4093
4094 set_max_tabs(cx, Some(3));
4095
4096 let item_a = add_labeled_item(&pane, "A", false, cx);
4097 assert_item_labels(&pane, ["A*"], cx);
4098
4099 let item_b = add_labeled_item(&pane, "B", false, cx);
4100 assert_item_labels(&pane, ["A", "B*"], cx);
4101
4102 let item_c = add_labeled_item(&pane, "C", false, cx);
4103 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4104
4105 pane.update_in(cx, |pane, window, cx| {
4106 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4107 pane.pin_tab_at(ix, window, cx);
4108 });
4109 assert_item_labels(&pane, ["A!", "B", "C*"], cx);
4110
4111 pane.update_in(cx, |pane, window, cx| {
4112 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4113 pane.pin_tab_at(ix, window, cx);
4114 });
4115 assert_item_labels(&pane, ["A!", "B!", "C*"], cx);
4116
4117 pane.update_in(cx, |pane, window, cx| {
4118 let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4119 pane.pin_tab_at(ix, window, cx);
4120 });
4121 assert_item_labels(&pane, ["A!", "B!", "C*!"], cx);
4122 }
4123
4124 #[gpui::test]
4125 async fn test_pin_tabs_right_to_left_after_opening_at_max_capacity(cx: &mut TestAppContext) {
4126 init_test(cx);
4127 let fs = FakeFs::new(cx.executor());
4128
4129 let project = Project::test(fs, None, cx).await;
4130 let (workspace, cx) =
4131 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4132 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4133
4134 set_max_tabs(cx, Some(3));
4135
4136 let item_a = add_labeled_item(&pane, "A", false, cx);
4137 assert_item_labels(&pane, ["A*"], cx);
4138
4139 let item_b = add_labeled_item(&pane, "B", false, cx);
4140 assert_item_labels(&pane, ["A", "B*"], cx);
4141
4142 let item_c = add_labeled_item(&pane, "C", false, cx);
4143 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4144
4145 pane.update_in(cx, |pane, window, cx| {
4146 let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4147 pane.pin_tab_at(ix, window, cx);
4148 });
4149 assert_item_labels(&pane, ["C*!", "A", "B"], cx);
4150
4151 pane.update_in(cx, |pane, window, cx| {
4152 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4153 pane.pin_tab_at(ix, window, cx);
4154 });
4155 assert_item_labels(&pane, ["C*!", "B!", "A"], cx);
4156
4157 pane.update_in(cx, |pane, window, cx| {
4158 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4159 pane.pin_tab_at(ix, window, cx);
4160 });
4161 assert_item_labels(&pane, ["C*!", "B!", "A!"], cx);
4162 }
4163
4164 #[gpui::test]
4165 async fn test_pinned_tabs_never_closed_at_max_tabs(cx: &mut TestAppContext) {
4166 init_test(cx);
4167 let fs = FakeFs::new(cx.executor());
4168
4169 let project = Project::test(fs, None, cx).await;
4170 let (workspace, cx) =
4171 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4172 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4173
4174 let item_a = add_labeled_item(&pane, "A", false, cx);
4175 pane.update_in(cx, |pane, window, cx| {
4176 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4177 pane.pin_tab_at(ix, window, cx);
4178 });
4179
4180 let item_b = add_labeled_item(&pane, "B", false, cx);
4181 pane.update_in(cx, |pane, window, cx| {
4182 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4183 pane.pin_tab_at(ix, window, cx);
4184 });
4185
4186 add_labeled_item(&pane, "C", false, cx);
4187 add_labeled_item(&pane, "D", false, cx);
4188 add_labeled_item(&pane, "E", false, cx);
4189 assert_item_labels(&pane, ["A!", "B!", "C", "D", "E*"], cx);
4190
4191 set_max_tabs(cx, Some(3));
4192 add_labeled_item(&pane, "F", false, cx);
4193 assert_item_labels(&pane, ["A!", "B!", "F*"], cx);
4194
4195 add_labeled_item(&pane, "G", false, cx);
4196 assert_item_labels(&pane, ["A!", "B!", "G*"], cx);
4197
4198 add_labeled_item(&pane, "H", false, cx);
4199 assert_item_labels(&pane, ["A!", "B!", "H*"], cx);
4200 }
4201
4202 #[gpui::test]
4203 async fn test_always_allows_one_unpinned_item_over_max_tabs_regardless_of_pinned_count(
4204 cx: &mut TestAppContext,
4205 ) {
4206 init_test(cx);
4207 let fs = FakeFs::new(cx.executor());
4208
4209 let project = Project::test(fs, None, cx).await;
4210 let (workspace, cx) =
4211 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4212 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4213
4214 set_max_tabs(cx, Some(3));
4215
4216 let item_a = add_labeled_item(&pane, "A", false, cx);
4217 pane.update_in(cx, |pane, window, cx| {
4218 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4219 pane.pin_tab_at(ix, window, cx);
4220 });
4221
4222 let item_b = add_labeled_item(&pane, "B", false, cx);
4223 pane.update_in(cx, |pane, window, cx| {
4224 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4225 pane.pin_tab_at(ix, window, cx);
4226 });
4227
4228 let item_c = add_labeled_item(&pane, "C", false, cx);
4229 pane.update_in(cx, |pane, window, cx| {
4230 let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4231 pane.pin_tab_at(ix, window, cx);
4232 });
4233
4234 assert_item_labels(&pane, ["A!", "B!", "C*!"], cx);
4235
4236 let item_d = add_labeled_item(&pane, "D", false, cx);
4237 assert_item_labels(&pane, ["A!", "B!", "C!", "D*"], cx);
4238
4239 pane.update_in(cx, |pane, window, cx| {
4240 let ix = pane.index_for_item_id(item_d.item_id()).unwrap();
4241 pane.pin_tab_at(ix, window, cx);
4242 });
4243 assert_item_labels(&pane, ["A!", "B!", "C!", "D*!"], cx);
4244
4245 add_labeled_item(&pane, "E", false, cx);
4246 assert_item_labels(&pane, ["A!", "B!", "C!", "D!", "E*"], cx);
4247
4248 add_labeled_item(&pane, "F", false, cx);
4249 assert_item_labels(&pane, ["A!", "B!", "C!", "D!", "F*"], cx);
4250 }
4251
4252 #[gpui::test]
4253 async fn test_can_open_one_item_when_all_tabs_are_dirty_at_max(cx: &mut TestAppContext) {
4254 init_test(cx);
4255 let fs = FakeFs::new(cx.executor());
4256
4257 let project = Project::test(fs, None, cx).await;
4258 let (workspace, cx) =
4259 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4260 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4261
4262 set_max_tabs(cx, Some(3));
4263
4264 add_labeled_item(&pane, "A", true, cx);
4265 assert_item_labels(&pane, ["A*^"], cx);
4266
4267 add_labeled_item(&pane, "B", true, cx);
4268 assert_item_labels(&pane, ["A^", "B*^"], cx);
4269
4270 add_labeled_item(&pane, "C", true, cx);
4271 assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
4272
4273 add_labeled_item(&pane, "D", false, cx);
4274 assert_item_labels(&pane, ["A^", "B^", "C^", "D*"], cx);
4275
4276 add_labeled_item(&pane, "E", false, cx);
4277 assert_item_labels(&pane, ["A^", "B^", "C^", "E*"], cx);
4278
4279 add_labeled_item(&pane, "F", false, cx);
4280 assert_item_labels(&pane, ["A^", "B^", "C^", "F*"], cx);
4281
4282 add_labeled_item(&pane, "G", true, cx);
4283 assert_item_labels(&pane, ["A^", "B^", "C^", "G*^"], cx);
4284 }
4285
4286 #[gpui::test]
4287 async fn test_toggle_pin_tab(cx: &mut TestAppContext) {
4288 init_test(cx);
4289 let fs = FakeFs::new(cx.executor());
4290
4291 let project = Project::test(fs, None, cx).await;
4292 let (workspace, cx) =
4293 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4294 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4295
4296 set_labeled_items(&pane, ["A", "B*", "C"], cx);
4297 assert_item_labels(&pane, ["A", "B*", "C"], cx);
4298
4299 pane.update_in(cx, |pane, window, cx| {
4300 pane.toggle_pin_tab(&TogglePinTab, window, cx);
4301 });
4302 assert_item_labels(&pane, ["B*!", "A", "C"], cx);
4303
4304 pane.update_in(cx, |pane, window, cx| {
4305 pane.toggle_pin_tab(&TogglePinTab, window, cx);
4306 });
4307 assert_item_labels(&pane, ["B*", "A", "C"], cx);
4308 }
4309
4310 #[gpui::test]
4311 async fn test_unpin_all_tabs(cx: &mut TestAppContext) {
4312 init_test(cx);
4313 let fs = FakeFs::new(cx.executor());
4314
4315 let project = Project::test(fs, None, cx).await;
4316 let (workspace, cx) =
4317 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4318 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4319
4320 // Unpin all, in an empty pane
4321 pane.update_in(cx, |pane, window, cx| {
4322 pane.unpin_all_tabs(&UnpinAllTabs, window, cx);
4323 });
4324
4325 assert_item_labels(&pane, [], cx);
4326
4327 let item_a = add_labeled_item(&pane, "A", false, cx);
4328 let item_b = add_labeled_item(&pane, "B", false, cx);
4329 let item_c = add_labeled_item(&pane, "C", false, cx);
4330 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4331
4332 // Unpin all, when no tabs are pinned
4333 pane.update_in(cx, |pane, window, cx| {
4334 pane.unpin_all_tabs(&UnpinAllTabs, window, cx);
4335 });
4336
4337 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4338
4339 // Pin inactive tabs only
4340 pane.update_in(cx, |pane, window, cx| {
4341 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4342 pane.pin_tab_at(ix, window, cx);
4343
4344 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4345 pane.pin_tab_at(ix, window, cx);
4346 });
4347 assert_item_labels(&pane, ["A!", "B!", "C*"], cx);
4348
4349 pane.update_in(cx, |pane, window, cx| {
4350 pane.unpin_all_tabs(&UnpinAllTabs, window, cx);
4351 });
4352
4353 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4354
4355 // Pin all tabs
4356 pane.update_in(cx, |pane, window, cx| {
4357 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4358 pane.pin_tab_at(ix, window, cx);
4359
4360 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4361 pane.pin_tab_at(ix, window, cx);
4362
4363 let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4364 pane.pin_tab_at(ix, window, cx);
4365 });
4366 assert_item_labels(&pane, ["A!", "B!", "C*!"], cx);
4367
4368 // Activate middle tab
4369 pane.update_in(cx, |pane, window, cx| {
4370 pane.activate_item(1, false, false, window, cx);
4371 });
4372 assert_item_labels(&pane, ["A!", "B*!", "C!"], cx);
4373
4374 pane.update_in(cx, |pane, window, cx| {
4375 pane.unpin_all_tabs(&UnpinAllTabs, window, cx);
4376 });
4377
4378 // Order has not changed
4379 assert_item_labels(&pane, ["A", "B*", "C"], cx);
4380 }
4381
4382 #[gpui::test]
4383 async fn test_pinning_active_tab_without_position_change_maintains_focus(
4384 cx: &mut TestAppContext,
4385 ) {
4386 init_test(cx);
4387 let fs = FakeFs::new(cx.executor());
4388
4389 let project = Project::test(fs, None, cx).await;
4390 let (workspace, cx) =
4391 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4392 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4393
4394 // Add A
4395 let item_a = add_labeled_item(&pane, "A", false, cx);
4396 assert_item_labels(&pane, ["A*"], cx);
4397
4398 // Add B
4399 add_labeled_item(&pane, "B", false, cx);
4400 assert_item_labels(&pane, ["A", "B*"], cx);
4401
4402 // Activate A again
4403 pane.update_in(cx, |pane, window, cx| {
4404 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4405 pane.activate_item(ix, true, true, window, cx);
4406 });
4407 assert_item_labels(&pane, ["A*", "B"], cx);
4408
4409 // Pin A - remains active
4410 pane.update_in(cx, |pane, window, cx| {
4411 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4412 pane.pin_tab_at(ix, window, cx);
4413 });
4414 assert_item_labels(&pane, ["A*!", "B"], cx);
4415
4416 // Unpin A - remain active
4417 pane.update_in(cx, |pane, window, cx| {
4418 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4419 pane.unpin_tab_at(ix, window, cx);
4420 });
4421 assert_item_labels(&pane, ["A*", "B"], cx);
4422 }
4423
4424 #[gpui::test]
4425 async fn test_pinning_active_tab_with_position_change_maintains_focus(cx: &mut TestAppContext) {
4426 init_test(cx);
4427 let fs = FakeFs::new(cx.executor());
4428
4429 let project = Project::test(fs, None, cx).await;
4430 let (workspace, cx) =
4431 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4432 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4433
4434 // Add A, B, C
4435 add_labeled_item(&pane, "A", false, cx);
4436 add_labeled_item(&pane, "B", false, cx);
4437 let item_c = add_labeled_item(&pane, "C", false, cx);
4438 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4439
4440 // Pin C - moves to pinned area, remains active
4441 pane.update_in(cx, |pane, window, cx| {
4442 let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4443 pane.pin_tab_at(ix, window, cx);
4444 });
4445 assert_item_labels(&pane, ["C*!", "A", "B"], cx);
4446
4447 // Unpin C - moves after pinned area, remains active
4448 pane.update_in(cx, |pane, window, cx| {
4449 let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4450 pane.unpin_tab_at(ix, window, cx);
4451 });
4452 assert_item_labels(&pane, ["C*", "A", "B"], cx);
4453 }
4454
4455 #[gpui::test]
4456 async fn test_pinning_inactive_tab_without_position_change_preserves_existing_focus(
4457 cx: &mut TestAppContext,
4458 ) {
4459 init_test(cx);
4460 let fs = FakeFs::new(cx.executor());
4461
4462 let project = Project::test(fs, None, cx).await;
4463 let (workspace, cx) =
4464 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4465 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4466
4467 // Add A, B
4468 let item_a = add_labeled_item(&pane, "A", false, cx);
4469 add_labeled_item(&pane, "B", false, cx);
4470 assert_item_labels(&pane, ["A", "B*"], cx);
4471
4472 // Pin A - already in pinned area, B remains active
4473 pane.update_in(cx, |pane, window, cx| {
4474 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4475 pane.pin_tab_at(ix, window, cx);
4476 });
4477 assert_item_labels(&pane, ["A!", "B*"], cx);
4478
4479 // Unpin A - stays in place, B remains active
4480 pane.update_in(cx, |pane, window, cx| {
4481 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4482 pane.unpin_tab_at(ix, window, cx);
4483 });
4484 assert_item_labels(&pane, ["A", "B*"], cx);
4485 }
4486
4487 #[gpui::test]
4488 async fn test_pinning_inactive_tab_with_position_change_preserves_existing_focus(
4489 cx: &mut TestAppContext,
4490 ) {
4491 init_test(cx);
4492 let fs = FakeFs::new(cx.executor());
4493
4494 let project = Project::test(fs, None, cx).await;
4495 let (workspace, cx) =
4496 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4497 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4498
4499 // Add A, B, C
4500 add_labeled_item(&pane, "A", false, cx);
4501 let item_b = add_labeled_item(&pane, "B", false, cx);
4502 let item_c = add_labeled_item(&pane, "C", false, cx);
4503 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4504
4505 // Activate B
4506 pane.update_in(cx, |pane, window, cx| {
4507 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4508 pane.activate_item(ix, true, true, window, cx);
4509 });
4510 assert_item_labels(&pane, ["A", "B*", "C"], cx);
4511
4512 // Pin C - moves to pinned area, B remains active
4513 pane.update_in(cx, |pane, window, cx| {
4514 let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4515 pane.pin_tab_at(ix, window, cx);
4516 });
4517 assert_item_labels(&pane, ["C!", "A", "B*"], cx);
4518
4519 // Unpin C - moves after pinned area, B remains active
4520 pane.update_in(cx, |pane, window, cx| {
4521 let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4522 pane.unpin_tab_at(ix, window, cx);
4523 });
4524 assert_item_labels(&pane, ["C", "A", "B*"], cx);
4525 }
4526
4527 #[gpui::test]
4528 async fn test_drag_unpinned_tab_to_split_creates_pane_with_unpinned_tab(
4529 cx: &mut TestAppContext,
4530 ) {
4531 init_test(cx);
4532 let fs = FakeFs::new(cx.executor());
4533
4534 let project = Project::test(fs, None, cx).await;
4535 let (workspace, cx) =
4536 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4537 let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4538
4539 // Add A, B. Pin B. Activate A
4540 let item_a = add_labeled_item(&pane_a, "A", false, cx);
4541 let item_b = add_labeled_item(&pane_a, "B", false, cx);
4542
4543 pane_a.update_in(cx, |pane, window, cx| {
4544 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4545 pane.pin_tab_at(ix, window, cx);
4546
4547 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4548 pane.activate_item(ix, true, true, window, cx);
4549 });
4550
4551 // Drag A to create new split
4552 pane_a.update_in(cx, |pane, window, cx| {
4553 pane.drag_split_direction = Some(SplitDirection::Right);
4554
4555 let dragged_tab = DraggedTab {
4556 pane: pane_a.clone(),
4557 item: item_a.boxed_clone(),
4558 ix: 0,
4559 detail: 0,
4560 is_active: true,
4561 };
4562 pane.handle_tab_drop(&dragged_tab, 0, window, cx);
4563 });
4564
4565 // A should be moved to new pane. B should remain pinned, A should not be pinned
4566 let (pane_a, pane_b) = workspace.read_with(cx, |workspace, _| {
4567 let panes = workspace.panes();
4568 (panes[0].clone(), panes[1].clone())
4569 });
4570 assert_item_labels(&pane_a, ["B*!"], cx);
4571 assert_item_labels(&pane_b, ["A*"], cx);
4572 }
4573
4574 #[gpui::test]
4575 async fn test_drag_pinned_tab_to_split_creates_pane_with_pinned_tab(cx: &mut TestAppContext) {
4576 init_test(cx);
4577 let fs = FakeFs::new(cx.executor());
4578
4579 let project = Project::test(fs, None, cx).await;
4580 let (workspace, cx) =
4581 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4582 let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4583
4584 // Add A, B. Pin both. Activate A
4585 let item_a = add_labeled_item(&pane_a, "A", false, cx);
4586 let item_b = add_labeled_item(&pane_a, "B", false, cx);
4587
4588 pane_a.update_in(cx, |pane, window, cx| {
4589 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4590 pane.pin_tab_at(ix, window, cx);
4591
4592 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4593 pane.pin_tab_at(ix, window, cx);
4594
4595 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4596 pane.activate_item(ix, true, true, window, cx);
4597 });
4598 assert_item_labels(&pane_a, ["A*!", "B!"], cx);
4599
4600 // Drag A to create new split
4601 pane_a.update_in(cx, |pane, window, cx| {
4602 pane.drag_split_direction = Some(SplitDirection::Right);
4603
4604 let dragged_tab = DraggedTab {
4605 pane: pane_a.clone(),
4606 item: item_a.boxed_clone(),
4607 ix: 0,
4608 detail: 0,
4609 is_active: true,
4610 };
4611 pane.handle_tab_drop(&dragged_tab, 0, window, cx);
4612 });
4613
4614 // A should be moved to new pane. Both A and B should still be pinned
4615 let (pane_a, pane_b) = workspace.read_with(cx, |workspace, _| {
4616 let panes = workspace.panes();
4617 (panes[0].clone(), panes[1].clone())
4618 });
4619 assert_item_labels(&pane_a, ["B*!"], cx);
4620 assert_item_labels(&pane_b, ["A*!"], cx);
4621 }
4622
4623 #[gpui::test]
4624 async fn test_drag_pinned_tab_into_existing_panes_pinned_region(cx: &mut TestAppContext) {
4625 init_test(cx);
4626 let fs = FakeFs::new(cx.executor());
4627
4628 let project = Project::test(fs, None, cx).await;
4629 let (workspace, cx) =
4630 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4631 let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4632
4633 // Add A to pane A and pin
4634 let item_a = add_labeled_item(&pane_a, "A", false, cx);
4635 pane_a.update_in(cx, |pane, window, cx| {
4636 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4637 pane.pin_tab_at(ix, window, cx);
4638 });
4639 assert_item_labels(&pane_a, ["A*!"], cx);
4640
4641 // Add B to pane B and pin
4642 let pane_b = workspace.update_in(cx, |workspace, window, cx| {
4643 workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx)
4644 });
4645 let item_b = add_labeled_item(&pane_b, "B", false, cx);
4646 pane_b.update_in(cx, |pane, window, cx| {
4647 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4648 pane.pin_tab_at(ix, window, cx);
4649 });
4650 assert_item_labels(&pane_b, ["B*!"], cx);
4651
4652 // Move A from pane A to pane B's pinned region
4653 pane_b.update_in(cx, |pane, window, cx| {
4654 let dragged_tab = DraggedTab {
4655 pane: pane_a.clone(),
4656 item: item_a.boxed_clone(),
4657 ix: 0,
4658 detail: 0,
4659 is_active: true,
4660 };
4661 pane.handle_tab_drop(&dragged_tab, 0, window, cx);
4662 });
4663
4664 // A should stay pinned
4665 assert_item_labels(&pane_a, [], cx);
4666 assert_item_labels(&pane_b, ["A*!", "B!"], cx);
4667 }
4668
4669 #[gpui::test]
4670 async fn test_drag_pinned_tab_into_existing_panes_unpinned_region(cx: &mut TestAppContext) {
4671 init_test(cx);
4672 let fs = FakeFs::new(cx.executor());
4673
4674 let project = Project::test(fs, None, cx).await;
4675 let (workspace, cx) =
4676 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4677 let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4678
4679 // Add A to pane A and pin
4680 let item_a = add_labeled_item(&pane_a, "A", false, cx);
4681 pane_a.update_in(cx, |pane, window, cx| {
4682 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4683 pane.pin_tab_at(ix, window, cx);
4684 });
4685 assert_item_labels(&pane_a, ["A*!"], cx);
4686
4687 // Create pane B with pinned item B
4688 let pane_b = workspace.update_in(cx, |workspace, window, cx| {
4689 workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx)
4690 });
4691 let item_b = add_labeled_item(&pane_b, "B", false, cx);
4692 assert_item_labels(&pane_b, ["B*"], cx);
4693
4694 pane_b.update_in(cx, |pane, window, cx| {
4695 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4696 pane.pin_tab_at(ix, window, cx);
4697 });
4698 assert_item_labels(&pane_b, ["B*!"], cx);
4699
4700 // Move A from pane A to pane B's unpinned region
4701 pane_b.update_in(cx, |pane, window, cx| {
4702 let dragged_tab = DraggedTab {
4703 pane: pane_a.clone(),
4704 item: item_a.boxed_clone(),
4705 ix: 0,
4706 detail: 0,
4707 is_active: true,
4708 };
4709 pane.handle_tab_drop(&dragged_tab, 1, window, cx);
4710 });
4711
4712 // A should become pinned
4713 assert_item_labels(&pane_a, [], cx);
4714 assert_item_labels(&pane_b, ["B!", "A*"], cx);
4715 }
4716
4717 #[gpui::test]
4718 async fn test_drag_pinned_tab_into_existing_panes_first_position_with_no_pinned_tabs(
4719 cx: &mut TestAppContext,
4720 ) {
4721 init_test(cx);
4722 let fs = FakeFs::new(cx.executor());
4723
4724 let project = Project::test(fs, None, cx).await;
4725 let (workspace, cx) =
4726 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4727 let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4728
4729 // Add A to pane A and pin
4730 let item_a = add_labeled_item(&pane_a, "A", false, cx);
4731 pane_a.update_in(cx, |pane, window, cx| {
4732 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4733 pane.pin_tab_at(ix, window, cx);
4734 });
4735 assert_item_labels(&pane_a, ["A*!"], cx);
4736
4737 // Add B to pane B
4738 let pane_b = workspace.update_in(cx, |workspace, window, cx| {
4739 workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx)
4740 });
4741 add_labeled_item(&pane_b, "B", false, cx);
4742 assert_item_labels(&pane_b, ["B*"], cx);
4743
4744 // Move A from pane A to position 0 in pane B, indicating it should stay pinned
4745 pane_b.update_in(cx, |pane, window, cx| {
4746 let dragged_tab = DraggedTab {
4747 pane: pane_a.clone(),
4748 item: item_a.boxed_clone(),
4749 ix: 0,
4750 detail: 0,
4751 is_active: true,
4752 };
4753 pane.handle_tab_drop(&dragged_tab, 0, window, cx);
4754 });
4755
4756 // A should stay pinned
4757 assert_item_labels(&pane_a, [], cx);
4758 assert_item_labels(&pane_b, ["A*!", "B"], cx);
4759 }
4760
4761 #[gpui::test]
4762 async fn test_drag_pinned_tab_into_existing_pane_at_max_capacity_closes_unpinned_tabs(
4763 cx: &mut TestAppContext,
4764 ) {
4765 init_test(cx);
4766 let fs = FakeFs::new(cx.executor());
4767
4768 let project = Project::test(fs, None, cx).await;
4769 let (workspace, cx) =
4770 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4771 let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4772 set_max_tabs(cx, Some(2));
4773
4774 // Add A, B to pane A. Pin both
4775 let item_a = add_labeled_item(&pane_a, "A", false, cx);
4776 let item_b = add_labeled_item(&pane_a, "B", false, cx);
4777 pane_a.update_in(cx, |pane, window, cx| {
4778 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4779 pane.pin_tab_at(ix, window, cx);
4780
4781 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4782 pane.pin_tab_at(ix, window, cx);
4783 });
4784 assert_item_labels(&pane_a, ["A!", "B*!"], cx);
4785
4786 // Add C, D to pane B. Pin both
4787 let pane_b = workspace.update_in(cx, |workspace, window, cx| {
4788 workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx)
4789 });
4790 let item_c = add_labeled_item(&pane_b, "C", false, cx);
4791 let item_d = add_labeled_item(&pane_b, "D", false, cx);
4792 pane_b.update_in(cx, |pane, window, cx| {
4793 let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4794 pane.pin_tab_at(ix, window, cx);
4795
4796 let ix = pane.index_for_item_id(item_d.item_id()).unwrap();
4797 pane.pin_tab_at(ix, window, cx);
4798 });
4799 assert_item_labels(&pane_b, ["C!", "D*!"], cx);
4800
4801 // Add a third unpinned item to pane B (exceeds max tabs), but is allowed,
4802 // as we allow 1 tab over max if the others are pinned or dirty
4803 add_labeled_item(&pane_b, "E", false, cx);
4804 assert_item_labels(&pane_b, ["C!", "D!", "E*"], cx);
4805
4806 // Drag pinned A from pane A to position 0 in pane B
4807 pane_b.update_in(cx, |pane, window, cx| {
4808 let dragged_tab = DraggedTab {
4809 pane: pane_a.clone(),
4810 item: item_a.boxed_clone(),
4811 ix: 0,
4812 detail: 0,
4813 is_active: true,
4814 };
4815 pane.handle_tab_drop(&dragged_tab, 0, window, cx);
4816 });
4817
4818 // E (unpinned) should be closed, leaving 3 pinned items
4819 assert_item_labels(&pane_a, ["B*!"], cx);
4820 assert_item_labels(&pane_b, ["A*!", "C!", "D!"], cx);
4821 }
4822
4823 #[gpui::test]
4824 async fn test_drag_last_pinned_tab_to_same_position_stays_pinned(cx: &mut TestAppContext) {
4825 init_test(cx);
4826 let fs = FakeFs::new(cx.executor());
4827
4828 let project = Project::test(fs, None, cx).await;
4829 let (workspace, cx) =
4830 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4831 let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4832
4833 // Add A to pane A and pin it
4834 let item_a = add_labeled_item(&pane_a, "A", false, cx);
4835 pane_a.update_in(cx, |pane, window, cx| {
4836 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4837 pane.pin_tab_at(ix, window, cx);
4838 });
4839 assert_item_labels(&pane_a, ["A*!"], cx);
4840
4841 // Drag pinned A to position 1 (directly to the right) in the same pane
4842 pane_a.update_in(cx, |pane, window, cx| {
4843 let dragged_tab = DraggedTab {
4844 pane: pane_a.clone(),
4845 item: item_a.boxed_clone(),
4846 ix: 0,
4847 detail: 0,
4848 is_active: true,
4849 };
4850 pane.handle_tab_drop(&dragged_tab, 1, window, cx);
4851 });
4852
4853 // A should still be pinned and active
4854 assert_item_labels(&pane_a, ["A*!"], cx);
4855 }
4856
4857 #[gpui::test]
4858 async fn test_drag_pinned_tab_beyond_last_pinned_tab_in_same_pane_stays_pinned(
4859 cx: &mut TestAppContext,
4860 ) {
4861 init_test(cx);
4862 let fs = FakeFs::new(cx.executor());
4863
4864 let project = Project::test(fs, None, cx).await;
4865 let (workspace, cx) =
4866 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4867 let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4868
4869 // Add A, B to pane A and pin both
4870 let item_a = add_labeled_item(&pane_a, "A", false, cx);
4871 let item_b = add_labeled_item(&pane_a, "B", false, cx);
4872 pane_a.update_in(cx, |pane, window, cx| {
4873 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4874 pane.pin_tab_at(ix, window, cx);
4875
4876 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4877 pane.pin_tab_at(ix, window, cx);
4878 });
4879 assert_item_labels(&pane_a, ["A!", "B*!"], cx);
4880
4881 // Drag pinned A right of B in the same pane
4882 pane_a.update_in(cx, |pane, window, cx| {
4883 let dragged_tab = DraggedTab {
4884 pane: pane_a.clone(),
4885 item: item_a.boxed_clone(),
4886 ix: 0,
4887 detail: 0,
4888 is_active: true,
4889 };
4890 pane.handle_tab_drop(&dragged_tab, 2, window, cx);
4891 });
4892
4893 // A stays pinned
4894 assert_item_labels(&pane_a, ["B!", "A*!"], cx);
4895 }
4896
4897 #[gpui::test]
4898 async fn test_drag_pinned_tab_beyond_unpinned_tab_in_same_pane_becomes_unpinned(
4899 cx: &mut TestAppContext,
4900 ) {
4901 init_test(cx);
4902 let fs = FakeFs::new(cx.executor());
4903
4904 let project = Project::test(fs, None, cx).await;
4905 let (workspace, cx) =
4906 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4907 let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4908
4909 // Add A, B to pane A and pin A
4910 let item_a = add_labeled_item(&pane_a, "A", false, cx);
4911 add_labeled_item(&pane_a, "B", false, cx);
4912 pane_a.update_in(cx, |pane, window, cx| {
4913 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4914 pane.pin_tab_at(ix, window, cx);
4915 });
4916 assert_item_labels(&pane_a, ["A!", "B*"], cx);
4917
4918 // Drag pinned A right of B in the same pane
4919 pane_a.update_in(cx, |pane, window, cx| {
4920 let dragged_tab = DraggedTab {
4921 pane: pane_a.clone(),
4922 item: item_a.boxed_clone(),
4923 ix: 0,
4924 detail: 0,
4925 is_active: true,
4926 };
4927 pane.handle_tab_drop(&dragged_tab, 2, window, cx);
4928 });
4929
4930 // A becomes unpinned
4931 assert_item_labels(&pane_a, ["B", "A*"], cx);
4932 }
4933
4934 #[gpui::test]
4935 async fn test_drag_unpinned_tab_in_front_of_pinned_tab_in_same_pane_becomes_pinned(
4936 cx: &mut TestAppContext,
4937 ) {
4938 init_test(cx);
4939 let fs = FakeFs::new(cx.executor());
4940
4941 let project = Project::test(fs, None, cx).await;
4942 let (workspace, cx) =
4943 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4944 let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4945
4946 // Add A, B to pane A and pin A
4947 let item_a = add_labeled_item(&pane_a, "A", false, cx);
4948 let item_b = add_labeled_item(&pane_a, "B", false, cx);
4949 pane_a.update_in(cx, |pane, window, cx| {
4950 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4951 pane.pin_tab_at(ix, window, cx);
4952 });
4953 assert_item_labels(&pane_a, ["A!", "B*"], cx);
4954
4955 // Drag pinned B left of A in the same pane
4956 pane_a.update_in(cx, |pane, window, cx| {
4957 let dragged_tab = DraggedTab {
4958 pane: pane_a.clone(),
4959 item: item_b.boxed_clone(),
4960 ix: 1,
4961 detail: 0,
4962 is_active: true,
4963 };
4964 pane.handle_tab_drop(&dragged_tab, 0, window, cx);
4965 });
4966
4967 // A becomes unpinned
4968 assert_item_labels(&pane_a, ["B*!", "A!"], cx);
4969 }
4970
4971 #[gpui::test]
4972 async fn test_drag_unpinned_tab_to_the_pinned_region_stays_pinned(cx: &mut TestAppContext) {
4973 init_test(cx);
4974 let fs = FakeFs::new(cx.executor());
4975
4976 let project = Project::test(fs, None, cx).await;
4977 let (workspace, cx) =
4978 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4979 let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4980
4981 // Add A, B, C to pane A and pin A
4982 let item_a = add_labeled_item(&pane_a, "A", false, cx);
4983 add_labeled_item(&pane_a, "B", false, cx);
4984 let item_c = add_labeled_item(&pane_a, "C", false, cx);
4985 pane_a.update_in(cx, |pane, window, cx| {
4986 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4987 pane.pin_tab_at(ix, window, cx);
4988 });
4989 assert_item_labels(&pane_a, ["A!", "B", "C*"], cx);
4990
4991 // Drag pinned C left of B in the same pane
4992 pane_a.update_in(cx, |pane, window, cx| {
4993 let dragged_tab = DraggedTab {
4994 pane: pane_a.clone(),
4995 item: item_c.boxed_clone(),
4996 ix: 2,
4997 detail: 0,
4998 is_active: true,
4999 };
5000 pane.handle_tab_drop(&dragged_tab, 1, window, cx);
5001 });
5002
5003 // A stays pinned, B and C remain unpinned
5004 assert_item_labels(&pane_a, ["A!", "C*", "B"], cx);
5005 }
5006
5007 #[gpui::test]
5008 async fn test_drag_unpinned_tab_into_existing_panes_pinned_region(cx: &mut TestAppContext) {
5009 init_test(cx);
5010 let fs = FakeFs::new(cx.executor());
5011
5012 let project = Project::test(fs, None, cx).await;
5013 let (workspace, cx) =
5014 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5015 let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5016
5017 // Add unpinned item A to pane A
5018 let item_a = add_labeled_item(&pane_a, "A", false, cx);
5019 assert_item_labels(&pane_a, ["A*"], cx);
5020
5021 // Create pane B with pinned item B
5022 let pane_b = workspace.update_in(cx, |workspace, window, cx| {
5023 workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx)
5024 });
5025 let item_b = add_labeled_item(&pane_b, "B", false, cx);
5026 pane_b.update_in(cx, |pane, window, cx| {
5027 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
5028 pane.pin_tab_at(ix, window, cx);
5029 });
5030 assert_item_labels(&pane_b, ["B*!"], cx);
5031
5032 // Move A from pane A to pane B's pinned region
5033 pane_b.update_in(cx, |pane, window, cx| {
5034 let dragged_tab = DraggedTab {
5035 pane: pane_a.clone(),
5036 item: item_a.boxed_clone(),
5037 ix: 0,
5038 detail: 0,
5039 is_active: true,
5040 };
5041 pane.handle_tab_drop(&dragged_tab, 0, window, cx);
5042 });
5043
5044 // A should become pinned since it was dropped in the pinned region
5045 assert_item_labels(&pane_a, [], cx);
5046 assert_item_labels(&pane_b, ["A*!", "B!"], cx);
5047 }
5048
5049 #[gpui::test]
5050 async fn test_drag_unpinned_tab_into_existing_panes_unpinned_region(cx: &mut TestAppContext) {
5051 init_test(cx);
5052 let fs = FakeFs::new(cx.executor());
5053
5054 let project = Project::test(fs, None, cx).await;
5055 let (workspace, cx) =
5056 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5057 let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5058
5059 // Add unpinned item A to pane A
5060 let item_a = add_labeled_item(&pane_a, "A", false, cx);
5061 assert_item_labels(&pane_a, ["A*"], cx);
5062
5063 // Create pane B with one pinned item B
5064 let pane_b = workspace.update_in(cx, |workspace, window, cx| {
5065 workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx)
5066 });
5067 let item_b = add_labeled_item(&pane_b, "B", false, cx);
5068 pane_b.update_in(cx, |pane, window, cx| {
5069 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
5070 pane.pin_tab_at(ix, window, cx);
5071 });
5072 assert_item_labels(&pane_b, ["B*!"], cx);
5073
5074 // Move A from pane A to pane B's unpinned region
5075 pane_b.update_in(cx, |pane, window, cx| {
5076 let dragged_tab = DraggedTab {
5077 pane: pane_a.clone(),
5078 item: item_a.boxed_clone(),
5079 ix: 0,
5080 detail: 0,
5081 is_active: true,
5082 };
5083 pane.handle_tab_drop(&dragged_tab, 1, window, cx);
5084 });
5085
5086 // A should remain unpinned since it was dropped outside the pinned region
5087 assert_item_labels(&pane_a, [], cx);
5088 assert_item_labels(&pane_b, ["B!", "A*"], cx);
5089 }
5090
5091 #[gpui::test]
5092 async fn test_drag_pinned_tab_throughout_entire_range_of_pinned_tabs_both_directions(
5093 cx: &mut TestAppContext,
5094 ) {
5095 init_test(cx);
5096 let fs = FakeFs::new(cx.executor());
5097
5098 let project = Project::test(fs, None, cx).await;
5099 let (workspace, cx) =
5100 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5101 let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5102
5103 // Add A, B, C and pin all
5104 let item_a = add_labeled_item(&pane_a, "A", false, cx);
5105 let item_b = add_labeled_item(&pane_a, "B", false, cx);
5106 let item_c = add_labeled_item(&pane_a, "C", false, cx);
5107 assert_item_labels(&pane_a, ["A", "B", "C*"], cx);
5108
5109 pane_a.update_in(cx, |pane, window, cx| {
5110 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
5111 pane.pin_tab_at(ix, window, cx);
5112
5113 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
5114 pane.pin_tab_at(ix, window, cx);
5115
5116 let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
5117 pane.pin_tab_at(ix, window, cx);
5118 });
5119 assert_item_labels(&pane_a, ["A!", "B!", "C*!"], cx);
5120
5121 // Move A to right of B
5122 pane_a.update_in(cx, |pane, window, cx| {
5123 let dragged_tab = DraggedTab {
5124 pane: pane_a.clone(),
5125 item: item_a.boxed_clone(),
5126 ix: 0,
5127 detail: 0,
5128 is_active: true,
5129 };
5130 pane.handle_tab_drop(&dragged_tab, 1, window, cx);
5131 });
5132
5133 // A should be after B and all are pinned
5134 assert_item_labels(&pane_a, ["B!", "A*!", "C!"], cx);
5135
5136 // Move A to right of C
5137 pane_a.update_in(cx, |pane, window, cx| {
5138 let dragged_tab = DraggedTab {
5139 pane: pane_a.clone(),
5140 item: item_a.boxed_clone(),
5141 ix: 1,
5142 detail: 0,
5143 is_active: true,
5144 };
5145 pane.handle_tab_drop(&dragged_tab, 2, window, cx);
5146 });
5147
5148 // A should be after C and all are pinned
5149 assert_item_labels(&pane_a, ["B!", "C!", "A*!"], cx);
5150
5151 // Move A to left of C
5152 pane_a.update_in(cx, |pane, window, cx| {
5153 let dragged_tab = DraggedTab {
5154 pane: pane_a.clone(),
5155 item: item_a.boxed_clone(),
5156 ix: 2,
5157 detail: 0,
5158 is_active: true,
5159 };
5160 pane.handle_tab_drop(&dragged_tab, 1, window, cx);
5161 });
5162
5163 // A should be before C and all are pinned
5164 assert_item_labels(&pane_a, ["B!", "A*!", "C!"], cx);
5165
5166 // Move A to left of B
5167 pane_a.update_in(cx, |pane, window, cx| {
5168 let dragged_tab = DraggedTab {
5169 pane: pane_a.clone(),
5170 item: item_a.boxed_clone(),
5171 ix: 1,
5172 detail: 0,
5173 is_active: true,
5174 };
5175 pane.handle_tab_drop(&dragged_tab, 0, window, cx);
5176 });
5177
5178 // A should be before B and all are pinned
5179 assert_item_labels(&pane_a, ["A*!", "B!", "C!"], cx);
5180 }
5181
5182 #[gpui::test]
5183 async fn test_drag_first_tab_to_last_position(cx: &mut TestAppContext) {
5184 init_test(cx);
5185 let fs = FakeFs::new(cx.executor());
5186
5187 let project = Project::test(fs, None, cx).await;
5188 let (workspace, cx) =
5189 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5190 let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5191
5192 // Add A, B, C
5193 let item_a = add_labeled_item(&pane_a, "A", false, cx);
5194 add_labeled_item(&pane_a, "B", false, cx);
5195 add_labeled_item(&pane_a, "C", false, cx);
5196 assert_item_labels(&pane_a, ["A", "B", "C*"], cx);
5197
5198 // Move A to the end
5199 pane_a.update_in(cx, |pane, window, cx| {
5200 let dragged_tab = DraggedTab {
5201 pane: pane_a.clone(),
5202 item: item_a.boxed_clone(),
5203 ix: 0,
5204 detail: 0,
5205 is_active: true,
5206 };
5207 pane.handle_tab_drop(&dragged_tab, 2, window, cx);
5208 });
5209
5210 // A should be at the end
5211 assert_item_labels(&pane_a, ["B", "C", "A*"], cx);
5212 }
5213
5214 #[gpui::test]
5215 async fn test_drag_last_tab_to_first_position(cx: &mut TestAppContext) {
5216 init_test(cx);
5217 let fs = FakeFs::new(cx.executor());
5218
5219 let project = Project::test(fs, None, cx).await;
5220 let (workspace, cx) =
5221 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5222 let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5223
5224 // Add A, B, C
5225 add_labeled_item(&pane_a, "A", false, cx);
5226 add_labeled_item(&pane_a, "B", false, cx);
5227 let item_c = add_labeled_item(&pane_a, "C", false, cx);
5228 assert_item_labels(&pane_a, ["A", "B", "C*"], cx);
5229
5230 // Move C to the beginning
5231 pane_a.update_in(cx, |pane, window, cx| {
5232 let dragged_tab = DraggedTab {
5233 pane: pane_a.clone(),
5234 item: item_c.boxed_clone(),
5235 ix: 2,
5236 detail: 0,
5237 is_active: true,
5238 };
5239 pane.handle_tab_drop(&dragged_tab, 0, window, cx);
5240 });
5241
5242 // C should be at the beginning
5243 assert_item_labels(&pane_a, ["C*", "A", "B"], cx);
5244 }
5245
5246 #[gpui::test]
5247 async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
5248 init_test(cx);
5249 let fs = FakeFs::new(cx.executor());
5250
5251 let project = Project::test(fs, None, cx).await;
5252 let (workspace, cx) =
5253 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5254 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5255
5256 // 1. Add with a destination index
5257 // a. Add before the active item
5258 set_labeled_items(&pane, ["A", "B*", "C"], cx);
5259 pane.update_in(cx, |pane, window, cx| {
5260 pane.add_item(
5261 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
5262 false,
5263 false,
5264 Some(0),
5265 window,
5266 cx,
5267 );
5268 });
5269 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
5270
5271 // b. Add after the active item
5272 set_labeled_items(&pane, ["A", "B*", "C"], cx);
5273 pane.update_in(cx, |pane, window, cx| {
5274 pane.add_item(
5275 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
5276 false,
5277 false,
5278 Some(2),
5279 window,
5280 cx,
5281 );
5282 });
5283 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
5284
5285 // c. Add at the end of the item list (including off the length)
5286 set_labeled_items(&pane, ["A", "B*", "C"], cx);
5287 pane.update_in(cx, |pane, window, cx| {
5288 pane.add_item(
5289 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
5290 false,
5291 false,
5292 Some(5),
5293 window,
5294 cx,
5295 );
5296 });
5297 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
5298
5299 // 2. Add without a destination index
5300 // a. Add with active item at the start of the item list
5301 set_labeled_items(&pane, ["A*", "B", "C"], cx);
5302 pane.update_in(cx, |pane, window, cx| {
5303 pane.add_item(
5304 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
5305 false,
5306 false,
5307 None,
5308 window,
5309 cx,
5310 );
5311 });
5312 set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
5313
5314 // b. Add with active item at the end of the item list
5315 set_labeled_items(&pane, ["A", "B", "C*"], cx);
5316 pane.update_in(cx, |pane, window, cx| {
5317 pane.add_item(
5318 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
5319 false,
5320 false,
5321 None,
5322 window,
5323 cx,
5324 );
5325 });
5326 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
5327 }
5328
5329 #[gpui::test]
5330 async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
5331 init_test(cx);
5332 let fs = FakeFs::new(cx.executor());
5333
5334 let project = Project::test(fs, None, cx).await;
5335 let (workspace, cx) =
5336 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5337 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5338
5339 // 1. Add with a destination index
5340 // 1a. Add before the active item
5341 let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
5342 pane.update_in(cx, |pane, window, cx| {
5343 pane.add_item(d, false, false, Some(0), window, cx);
5344 });
5345 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
5346
5347 // 1b. Add after the active item
5348 let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
5349 pane.update_in(cx, |pane, window, cx| {
5350 pane.add_item(d, false, false, Some(2), window, cx);
5351 });
5352 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
5353
5354 // 1c. Add at the end of the item list (including off the length)
5355 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
5356 pane.update_in(cx, |pane, window, cx| {
5357 pane.add_item(a, false, false, Some(5), window, cx);
5358 });
5359 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
5360
5361 // 1d. Add same item to active index
5362 let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
5363 pane.update_in(cx, |pane, window, cx| {
5364 pane.add_item(b, false, false, Some(1), window, cx);
5365 });
5366 assert_item_labels(&pane, ["A", "B*", "C"], cx);
5367
5368 // 1e. Add item to index after same item in last position
5369 let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
5370 pane.update_in(cx, |pane, window, cx| {
5371 pane.add_item(c, false, false, Some(2), window, cx);
5372 });
5373 assert_item_labels(&pane, ["A", "B", "C*"], cx);
5374
5375 // 2. Add without a destination index
5376 // 2a. Add with active item at the start of the item list
5377 let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
5378 pane.update_in(cx, |pane, window, cx| {
5379 pane.add_item(d, false, false, None, window, cx);
5380 });
5381 assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
5382
5383 // 2b. Add with active item at the end of the item list
5384 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
5385 pane.update_in(cx, |pane, window, cx| {
5386 pane.add_item(a, false, false, None, window, cx);
5387 });
5388 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
5389
5390 // 2c. Add active item to active item at end of list
5391 let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
5392 pane.update_in(cx, |pane, window, cx| {
5393 pane.add_item(c, false, false, None, window, cx);
5394 });
5395 assert_item_labels(&pane, ["A", "B", "C*"], cx);
5396
5397 // 2d. Add active item to active item at start of list
5398 let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
5399 pane.update_in(cx, |pane, window, cx| {
5400 pane.add_item(a, false, false, None, window, cx);
5401 });
5402 assert_item_labels(&pane, ["A*", "B", "C"], cx);
5403 }
5404
5405 #[gpui::test]
5406 async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
5407 init_test(cx);
5408 let fs = FakeFs::new(cx.executor());
5409
5410 let project = Project::test(fs, None, cx).await;
5411 let (workspace, cx) =
5412 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5413 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5414
5415 // singleton view
5416 pane.update_in(cx, |pane, window, cx| {
5417 pane.add_item(
5418 Box::new(cx.new(|cx| {
5419 TestItem::new(cx)
5420 .with_singleton(true)
5421 .with_label("buffer 1")
5422 .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
5423 })),
5424 false,
5425 false,
5426 None,
5427 window,
5428 cx,
5429 );
5430 });
5431 assert_item_labels(&pane, ["buffer 1*"], cx);
5432
5433 // new singleton view with the same project entry
5434 pane.update_in(cx, |pane, window, cx| {
5435 pane.add_item(
5436 Box::new(cx.new(|cx| {
5437 TestItem::new(cx)
5438 .with_singleton(true)
5439 .with_label("buffer 1")
5440 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
5441 })),
5442 false,
5443 false,
5444 None,
5445 window,
5446 cx,
5447 );
5448 });
5449 assert_item_labels(&pane, ["buffer 1*"], cx);
5450
5451 // new singleton view with different project entry
5452 pane.update_in(cx, |pane, window, cx| {
5453 pane.add_item(
5454 Box::new(cx.new(|cx| {
5455 TestItem::new(cx)
5456 .with_singleton(true)
5457 .with_label("buffer 2")
5458 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
5459 })),
5460 false,
5461 false,
5462 None,
5463 window,
5464 cx,
5465 );
5466 });
5467 assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
5468
5469 // new multibuffer view with the same project entry
5470 pane.update_in(cx, |pane, window, cx| {
5471 pane.add_item(
5472 Box::new(cx.new(|cx| {
5473 TestItem::new(cx)
5474 .with_singleton(false)
5475 .with_label("multibuffer 1")
5476 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
5477 })),
5478 false,
5479 false,
5480 None,
5481 window,
5482 cx,
5483 );
5484 });
5485 assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
5486
5487 // another multibuffer view with the same project entry
5488 pane.update_in(cx, |pane, window, cx| {
5489 pane.add_item(
5490 Box::new(cx.new(|cx| {
5491 TestItem::new(cx)
5492 .with_singleton(false)
5493 .with_label("multibuffer 1b")
5494 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
5495 })),
5496 false,
5497 false,
5498 None,
5499 window,
5500 cx,
5501 );
5502 });
5503 assert_item_labels(
5504 &pane,
5505 ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
5506 cx,
5507 );
5508 }
5509
5510 #[gpui::test]
5511 async fn test_remove_item_ordering_history(cx: &mut TestAppContext) {
5512 init_test(cx);
5513 let fs = FakeFs::new(cx.executor());
5514
5515 let project = Project::test(fs, None, cx).await;
5516 let (workspace, cx) =
5517 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5518 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5519
5520 add_labeled_item(&pane, "A", false, cx);
5521 add_labeled_item(&pane, "B", false, cx);
5522 add_labeled_item(&pane, "C", false, cx);
5523 add_labeled_item(&pane, "D", false, cx);
5524 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
5525
5526 pane.update_in(cx, |pane, window, cx| {
5527 pane.activate_item(1, false, false, window, cx)
5528 });
5529 add_labeled_item(&pane, "1", false, cx);
5530 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
5531
5532 pane.update_in(cx, |pane, window, cx| {
5533 pane.close_active_item(
5534 &CloseActiveItem {
5535 save_intent: None,
5536 close_pinned: false,
5537 },
5538 window,
5539 cx,
5540 )
5541 })
5542 .await
5543 .unwrap();
5544 assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
5545
5546 pane.update_in(cx, |pane, window, cx| {
5547 pane.activate_item(3, false, false, window, cx)
5548 });
5549 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
5550
5551 pane.update_in(cx, |pane, window, cx| {
5552 pane.close_active_item(
5553 &CloseActiveItem {
5554 save_intent: None,
5555 close_pinned: false,
5556 },
5557 window,
5558 cx,
5559 )
5560 })
5561 .await
5562 .unwrap();
5563 assert_item_labels(&pane, ["A", "B*", "C"], cx);
5564
5565 pane.update_in(cx, |pane, window, cx| {
5566 pane.close_active_item(
5567 &CloseActiveItem {
5568 save_intent: None,
5569 close_pinned: false,
5570 },
5571 window,
5572 cx,
5573 )
5574 })
5575 .await
5576 .unwrap();
5577 assert_item_labels(&pane, ["A", "C*"], cx);
5578
5579 pane.update_in(cx, |pane, window, cx| {
5580 pane.close_active_item(
5581 &CloseActiveItem {
5582 save_intent: None,
5583 close_pinned: false,
5584 },
5585 window,
5586 cx,
5587 )
5588 })
5589 .await
5590 .unwrap();
5591 assert_item_labels(&pane, ["A*"], cx);
5592 }
5593
5594 #[gpui::test]
5595 async fn test_remove_item_ordering_neighbour(cx: &mut TestAppContext) {
5596 init_test(cx);
5597 cx.update_global::<SettingsStore, ()>(|s, cx| {
5598 s.update_user_settings::<ItemSettings>(cx, |s| {
5599 s.activate_on_close = Some(ActivateOnClose::Neighbour);
5600 });
5601 });
5602 let fs = FakeFs::new(cx.executor());
5603
5604 let project = Project::test(fs, None, cx).await;
5605 let (workspace, cx) =
5606 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5607 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5608
5609 add_labeled_item(&pane, "A", false, cx);
5610 add_labeled_item(&pane, "B", false, cx);
5611 add_labeled_item(&pane, "C", false, cx);
5612 add_labeled_item(&pane, "D", false, cx);
5613 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
5614
5615 pane.update_in(cx, |pane, window, cx| {
5616 pane.activate_item(1, false, false, window, cx)
5617 });
5618 add_labeled_item(&pane, "1", false, cx);
5619 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
5620
5621 pane.update_in(cx, |pane, window, cx| {
5622 pane.close_active_item(
5623 &CloseActiveItem {
5624 save_intent: None,
5625 close_pinned: false,
5626 },
5627 window,
5628 cx,
5629 )
5630 })
5631 .await
5632 .unwrap();
5633 assert_item_labels(&pane, ["A", "B", "C*", "D"], cx);
5634
5635 pane.update_in(cx, |pane, window, cx| {
5636 pane.activate_item(3, false, false, window, cx)
5637 });
5638 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
5639
5640 pane.update_in(cx, |pane, window, cx| {
5641 pane.close_active_item(
5642 &CloseActiveItem {
5643 save_intent: None,
5644 close_pinned: false,
5645 },
5646 window,
5647 cx,
5648 )
5649 })
5650 .await
5651 .unwrap();
5652 assert_item_labels(&pane, ["A", "B", "C*"], cx);
5653
5654 pane.update_in(cx, |pane, window, cx| {
5655 pane.close_active_item(
5656 &CloseActiveItem {
5657 save_intent: None,
5658 close_pinned: false,
5659 },
5660 window,
5661 cx,
5662 )
5663 })
5664 .await
5665 .unwrap();
5666 assert_item_labels(&pane, ["A", "B*"], cx);
5667
5668 pane.update_in(cx, |pane, window, cx| {
5669 pane.close_active_item(
5670 &CloseActiveItem {
5671 save_intent: None,
5672 close_pinned: false,
5673 },
5674 window,
5675 cx,
5676 )
5677 })
5678 .await
5679 .unwrap();
5680 assert_item_labels(&pane, ["A*"], cx);
5681 }
5682
5683 #[gpui::test]
5684 async fn test_remove_item_ordering_left_neighbour(cx: &mut TestAppContext) {
5685 init_test(cx);
5686 cx.update_global::<SettingsStore, ()>(|s, cx| {
5687 s.update_user_settings::<ItemSettings>(cx, |s| {
5688 s.activate_on_close = Some(ActivateOnClose::LeftNeighbour);
5689 });
5690 });
5691 let fs = FakeFs::new(cx.executor());
5692
5693 let project = Project::test(fs, None, cx).await;
5694 let (workspace, cx) =
5695 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5696 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5697
5698 add_labeled_item(&pane, "A", false, cx);
5699 add_labeled_item(&pane, "B", false, cx);
5700 add_labeled_item(&pane, "C", false, cx);
5701 add_labeled_item(&pane, "D", false, cx);
5702 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
5703
5704 pane.update_in(cx, |pane, window, cx| {
5705 pane.activate_item(1, false, false, window, cx)
5706 });
5707 add_labeled_item(&pane, "1", false, cx);
5708 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
5709
5710 pane.update_in(cx, |pane, window, cx| {
5711 pane.close_active_item(
5712 &CloseActiveItem {
5713 save_intent: None,
5714 close_pinned: false,
5715 },
5716 window,
5717 cx,
5718 )
5719 })
5720 .await
5721 .unwrap();
5722 assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
5723
5724 pane.update_in(cx, |pane, window, cx| {
5725 pane.activate_item(3, false, false, window, cx)
5726 });
5727 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
5728
5729 pane.update_in(cx, |pane, window, cx| {
5730 pane.close_active_item(
5731 &CloseActiveItem {
5732 save_intent: None,
5733 close_pinned: false,
5734 },
5735 window,
5736 cx,
5737 )
5738 })
5739 .await
5740 .unwrap();
5741 assert_item_labels(&pane, ["A", "B", "C*"], cx);
5742
5743 pane.update_in(cx, |pane, window, cx| {
5744 pane.activate_item(0, false, false, window, cx)
5745 });
5746 assert_item_labels(&pane, ["A*", "B", "C"], cx);
5747
5748 pane.update_in(cx, |pane, window, cx| {
5749 pane.close_active_item(
5750 &CloseActiveItem {
5751 save_intent: None,
5752 close_pinned: false,
5753 },
5754 window,
5755 cx,
5756 )
5757 })
5758 .await
5759 .unwrap();
5760 assert_item_labels(&pane, ["B*", "C"], cx);
5761
5762 pane.update_in(cx, |pane, window, cx| {
5763 pane.close_active_item(
5764 &CloseActiveItem {
5765 save_intent: None,
5766 close_pinned: false,
5767 },
5768 window,
5769 cx,
5770 )
5771 })
5772 .await
5773 .unwrap();
5774 assert_item_labels(&pane, ["C*"], cx);
5775 }
5776
5777 #[gpui::test]
5778 async fn test_close_inactive_items(cx: &mut TestAppContext) {
5779 init_test(cx);
5780 let fs = FakeFs::new(cx.executor());
5781
5782 let project = Project::test(fs, None, cx).await;
5783 let (workspace, cx) =
5784 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5785 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5786
5787 let item_a = add_labeled_item(&pane, "A", false, cx);
5788 pane.update_in(cx, |pane, window, cx| {
5789 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
5790 pane.pin_tab_at(ix, window, cx);
5791 });
5792 assert_item_labels(&pane, ["A*!"], cx);
5793
5794 let item_b = add_labeled_item(&pane, "B", false, cx);
5795 pane.update_in(cx, |pane, window, cx| {
5796 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
5797 pane.pin_tab_at(ix, window, cx);
5798 });
5799 assert_item_labels(&pane, ["A!", "B*!"], cx);
5800
5801 add_labeled_item(&pane, "C", false, cx);
5802 assert_item_labels(&pane, ["A!", "B!", "C*"], cx);
5803
5804 add_labeled_item(&pane, "D", false, cx);
5805 add_labeled_item(&pane, "E", false, cx);
5806 assert_item_labels(&pane, ["A!", "B!", "C", "D", "E*"], cx);
5807
5808 pane.update_in(cx, |pane, window, cx| {
5809 pane.close_inactive_items(
5810 &CloseInactiveItems {
5811 save_intent: None,
5812 close_pinned: false,
5813 },
5814 window,
5815 cx,
5816 )
5817 })
5818 .await
5819 .unwrap();
5820 assert_item_labels(&pane, ["A!", "B!", "E*"], cx);
5821 }
5822
5823 #[gpui::test]
5824 async fn test_close_clean_items(cx: &mut TestAppContext) {
5825 init_test(cx);
5826 let fs = FakeFs::new(cx.executor());
5827
5828 let project = Project::test(fs, None, cx).await;
5829 let (workspace, cx) =
5830 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5831 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5832
5833 add_labeled_item(&pane, "A", true, cx);
5834 add_labeled_item(&pane, "B", false, cx);
5835 add_labeled_item(&pane, "C", true, cx);
5836 add_labeled_item(&pane, "D", false, cx);
5837 add_labeled_item(&pane, "E", false, cx);
5838 assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
5839
5840 pane.update_in(cx, |pane, window, cx| {
5841 pane.close_clean_items(
5842 &CloseCleanItems {
5843 close_pinned: false,
5844 },
5845 window,
5846 cx,
5847 )
5848 })
5849 .await
5850 .unwrap();
5851 assert_item_labels(&pane, ["A^", "C*^"], cx);
5852 }
5853
5854 #[gpui::test]
5855 async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
5856 init_test(cx);
5857 let fs = FakeFs::new(cx.executor());
5858
5859 let project = Project::test(fs, None, cx).await;
5860 let (workspace, cx) =
5861 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5862 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5863
5864 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
5865
5866 pane.update_in(cx, |pane, window, cx| {
5867 pane.close_items_to_the_left_by_id(
5868 None,
5869 &CloseItemsToTheLeft {
5870 close_pinned: false,
5871 },
5872 window,
5873 cx,
5874 )
5875 })
5876 .await
5877 .unwrap();
5878 assert_item_labels(&pane, ["C*", "D", "E"], cx);
5879 }
5880
5881 #[gpui::test]
5882 async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
5883 init_test(cx);
5884 let fs = FakeFs::new(cx.executor());
5885
5886 let project = Project::test(fs, None, cx).await;
5887 let (workspace, cx) =
5888 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5889 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5890
5891 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
5892
5893 pane.update_in(cx, |pane, window, cx| {
5894 pane.close_items_to_the_right_by_id(
5895 None,
5896 &CloseItemsToTheRight {
5897 close_pinned: false,
5898 },
5899 window,
5900 cx,
5901 )
5902 })
5903 .await
5904 .unwrap();
5905 assert_item_labels(&pane, ["A", "B", "C*"], cx);
5906 }
5907
5908 #[gpui::test]
5909 async fn test_close_all_items(cx: &mut TestAppContext) {
5910 init_test(cx);
5911 let fs = FakeFs::new(cx.executor());
5912
5913 let project = Project::test(fs, None, cx).await;
5914 let (workspace, cx) =
5915 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5916 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5917
5918 let item_a = add_labeled_item(&pane, "A", false, cx);
5919 add_labeled_item(&pane, "B", false, cx);
5920 add_labeled_item(&pane, "C", false, cx);
5921 assert_item_labels(&pane, ["A", "B", "C*"], cx);
5922
5923 pane.update_in(cx, |pane, window, cx| {
5924 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
5925 pane.pin_tab_at(ix, window, cx);
5926 pane.close_all_items(
5927 &CloseAllItems {
5928 save_intent: None,
5929 close_pinned: false,
5930 },
5931 window,
5932 cx,
5933 )
5934 })
5935 .await
5936 .unwrap();
5937 assert_item_labels(&pane, ["A*!"], cx);
5938
5939 pane.update_in(cx, |pane, window, cx| {
5940 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
5941 pane.unpin_tab_at(ix, window, cx);
5942 pane.close_all_items(
5943 &CloseAllItems {
5944 save_intent: None,
5945 close_pinned: false,
5946 },
5947 window,
5948 cx,
5949 )
5950 })
5951 .await
5952 .unwrap();
5953
5954 assert_item_labels(&pane, [], cx);
5955
5956 add_labeled_item(&pane, "A", true, cx).update(cx, |item, cx| {
5957 item.project_items
5958 .push(TestProjectItem::new_dirty(1, "A.txt", cx))
5959 });
5960 add_labeled_item(&pane, "B", true, cx).update(cx, |item, cx| {
5961 item.project_items
5962 .push(TestProjectItem::new_dirty(2, "B.txt", cx))
5963 });
5964 add_labeled_item(&pane, "C", true, cx).update(cx, |item, cx| {
5965 item.project_items
5966 .push(TestProjectItem::new_dirty(3, "C.txt", cx))
5967 });
5968 assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
5969
5970 let save = pane.update_in(cx, |pane, window, cx| {
5971 pane.close_all_items(
5972 &CloseAllItems {
5973 save_intent: None,
5974 close_pinned: false,
5975 },
5976 window,
5977 cx,
5978 )
5979 });
5980
5981 cx.executor().run_until_parked();
5982 cx.simulate_prompt_answer("Save all");
5983 save.await.unwrap();
5984 assert_item_labels(&pane, [], cx);
5985
5986 add_labeled_item(&pane, "A", true, cx);
5987 add_labeled_item(&pane, "B", true, cx);
5988 add_labeled_item(&pane, "C", true, cx);
5989 assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
5990 let save = pane.update_in(cx, |pane, window, cx| {
5991 pane.close_all_items(
5992 &CloseAllItems {
5993 save_intent: None,
5994 close_pinned: false,
5995 },
5996 window,
5997 cx,
5998 )
5999 });
6000
6001 cx.executor().run_until_parked();
6002 cx.simulate_prompt_answer("Discard all");
6003 save.await.unwrap();
6004 assert_item_labels(&pane, [], cx);
6005 }
6006
6007 #[gpui::test]
6008 async fn test_close_with_save_intent(cx: &mut TestAppContext) {
6009 init_test(cx);
6010 let fs = FakeFs::new(cx.executor());
6011
6012 let project = Project::test(fs, None, cx).await;
6013 let (workspace, cx) =
6014 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
6015 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
6016
6017 let a = cx.update(|_, cx| TestProjectItem::new_dirty(1, "A.txt", cx));
6018 let b = cx.update(|_, cx| TestProjectItem::new_dirty(1, "B.txt", cx));
6019 let c = cx.update(|_, cx| TestProjectItem::new_dirty(1, "C.txt", cx));
6020
6021 add_labeled_item(&pane, "AB", true, cx).update(cx, |item, _| {
6022 item.project_items.push(a.clone());
6023 item.project_items.push(b.clone());
6024 });
6025 add_labeled_item(&pane, "C", true, cx)
6026 .update(cx, |item, _| item.project_items.push(c.clone()));
6027 assert_item_labels(&pane, ["AB^", "C*^"], cx);
6028
6029 pane.update_in(cx, |pane, window, cx| {
6030 pane.close_all_items(
6031 &CloseAllItems {
6032 save_intent: Some(SaveIntent::Save),
6033 close_pinned: false,
6034 },
6035 window,
6036 cx,
6037 )
6038 })
6039 .await
6040 .unwrap();
6041
6042 assert_item_labels(&pane, [], cx);
6043 cx.update(|_, cx| {
6044 assert!(!a.read(cx).is_dirty);
6045 assert!(!b.read(cx).is_dirty);
6046 assert!(!c.read(cx).is_dirty);
6047 });
6048 }
6049
6050 #[gpui::test]
6051 async fn test_close_all_items_including_pinned(cx: &mut TestAppContext) {
6052 init_test(cx);
6053 let fs = FakeFs::new(cx.executor());
6054
6055 let project = Project::test(fs, None, cx).await;
6056 let (workspace, cx) =
6057 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
6058 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
6059
6060 let item_a = add_labeled_item(&pane, "A", false, cx);
6061 add_labeled_item(&pane, "B", false, cx);
6062 add_labeled_item(&pane, "C", false, cx);
6063 assert_item_labels(&pane, ["A", "B", "C*"], cx);
6064
6065 pane.update_in(cx, |pane, window, cx| {
6066 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
6067 pane.pin_tab_at(ix, window, cx);
6068 pane.close_all_items(
6069 &CloseAllItems {
6070 save_intent: None,
6071 close_pinned: true,
6072 },
6073 window,
6074 cx,
6075 )
6076 })
6077 .await
6078 .unwrap();
6079 assert_item_labels(&pane, [], cx);
6080 }
6081
6082 #[gpui::test]
6083 async fn test_close_pinned_tab_with_non_pinned_in_same_pane(cx: &mut TestAppContext) {
6084 init_test(cx);
6085 let fs = FakeFs::new(cx.executor());
6086 let project = Project::test(fs, None, cx).await;
6087 let (workspace, cx) =
6088 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
6089
6090 // Non-pinned tabs in same pane
6091 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
6092 add_labeled_item(&pane, "A", false, cx);
6093 add_labeled_item(&pane, "B", false, cx);
6094 add_labeled_item(&pane, "C", false, cx);
6095 pane.update_in(cx, |pane, window, cx| {
6096 pane.pin_tab_at(0, window, cx);
6097 });
6098 set_labeled_items(&pane, ["A*", "B", "C"], cx);
6099 pane.update_in(cx, |pane, window, cx| {
6100 pane.close_active_item(
6101 &CloseActiveItem {
6102 save_intent: None,
6103 close_pinned: false,
6104 },
6105 window,
6106 cx,
6107 )
6108 .unwrap();
6109 });
6110 // Non-pinned tab should be active
6111 assert_item_labels(&pane, ["A!", "B*", "C"], cx);
6112 }
6113
6114 #[gpui::test]
6115 async fn test_close_pinned_tab_with_non_pinned_in_different_pane(cx: &mut TestAppContext) {
6116 init_test(cx);
6117 let fs = FakeFs::new(cx.executor());
6118 let project = Project::test(fs, None, cx).await;
6119 let (workspace, cx) =
6120 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
6121
6122 // No non-pinned tabs in same pane, non-pinned tabs in another pane
6123 let pane1 = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
6124 let pane2 = workspace.update_in(cx, |workspace, window, cx| {
6125 workspace.split_pane(pane1.clone(), SplitDirection::Right, window, cx)
6126 });
6127 add_labeled_item(&pane1, "A", false, cx);
6128 pane1.update_in(cx, |pane, window, cx| {
6129 pane.pin_tab_at(0, window, cx);
6130 });
6131 set_labeled_items(&pane1, ["A*"], cx);
6132 add_labeled_item(&pane2, "B", false, cx);
6133 set_labeled_items(&pane2, ["B"], cx);
6134 pane1.update_in(cx, |pane, window, cx| {
6135 pane.close_active_item(
6136 &CloseActiveItem {
6137 save_intent: None,
6138 close_pinned: false,
6139 },
6140 window,
6141 cx,
6142 )
6143 .unwrap();
6144 });
6145 // Non-pinned tab of other pane should be active
6146 assert_item_labels(&pane2, ["B*"], cx);
6147 }
6148
6149 #[gpui::test]
6150 async fn ensure_item_closing_actions_do_not_panic_when_no_items_exist(cx: &mut TestAppContext) {
6151 init_test(cx);
6152 let fs = FakeFs::new(cx.executor());
6153 let project = Project::test(fs, None, cx).await;
6154 let (workspace, cx) =
6155 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
6156
6157 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
6158 assert_item_labels(&pane, [], cx);
6159
6160 pane.update_in(cx, |pane, window, cx| {
6161 pane.close_active_item(
6162 &CloseActiveItem {
6163 save_intent: None,
6164 close_pinned: false,
6165 },
6166 window,
6167 cx,
6168 )
6169 })
6170 .await
6171 .unwrap();
6172
6173 pane.update_in(cx, |pane, window, cx| {
6174 pane.close_inactive_items(
6175 &CloseInactiveItems {
6176 save_intent: None,
6177 close_pinned: false,
6178 },
6179 window,
6180 cx,
6181 )
6182 })
6183 .await
6184 .unwrap();
6185
6186 pane.update_in(cx, |pane, window, cx| {
6187 pane.close_all_items(
6188 &CloseAllItems {
6189 save_intent: None,
6190 close_pinned: false,
6191 },
6192 window,
6193 cx,
6194 )
6195 })
6196 .await
6197 .unwrap();
6198
6199 pane.update_in(cx, |pane, window, cx| {
6200 pane.close_clean_items(
6201 &CloseCleanItems {
6202 close_pinned: false,
6203 },
6204 window,
6205 cx,
6206 )
6207 })
6208 .await
6209 .unwrap();
6210
6211 pane.update_in(cx, |pane, window, cx| {
6212 pane.close_items_to_the_right_by_id(
6213 None,
6214 &CloseItemsToTheRight {
6215 close_pinned: false,
6216 },
6217 window,
6218 cx,
6219 )
6220 })
6221 .await
6222 .unwrap();
6223
6224 pane.update_in(cx, |pane, window, cx| {
6225 pane.close_items_to_the_left_by_id(
6226 None,
6227 &CloseItemsToTheLeft {
6228 close_pinned: false,
6229 },
6230 window,
6231 cx,
6232 )
6233 })
6234 .await
6235 .unwrap();
6236 }
6237
6238 fn init_test(cx: &mut TestAppContext) {
6239 cx.update(|cx| {
6240 let settings_store = SettingsStore::test(cx);
6241 cx.set_global(settings_store);
6242 theme::init(LoadThemes::JustBase, cx);
6243 crate::init_settings(cx);
6244 Project::init_settings(cx);
6245 });
6246 }
6247
6248 fn set_max_tabs(cx: &mut TestAppContext, value: Option<usize>) {
6249 cx.update_global(|store: &mut SettingsStore, cx| {
6250 store.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6251 settings.max_tabs = value.map(|v| NonZero::new(v).unwrap())
6252 });
6253 });
6254 }
6255
6256 fn add_labeled_item(
6257 pane: &Entity<Pane>,
6258 label: &str,
6259 is_dirty: bool,
6260 cx: &mut VisualTestContext,
6261 ) -> Box<Entity<TestItem>> {
6262 pane.update_in(cx, |pane, window, cx| {
6263 let labeled_item =
6264 Box::new(cx.new(|cx| TestItem::new(cx).with_label(label).with_dirty(is_dirty)));
6265 pane.add_item(labeled_item.clone(), false, false, None, window, cx);
6266 labeled_item
6267 })
6268 }
6269
6270 fn set_labeled_items<const COUNT: usize>(
6271 pane: &Entity<Pane>,
6272 labels: [&str; COUNT],
6273 cx: &mut VisualTestContext,
6274 ) -> [Box<Entity<TestItem>>; COUNT] {
6275 pane.update_in(cx, |pane, window, cx| {
6276 pane.items.clear();
6277 let mut active_item_index = 0;
6278
6279 let mut index = 0;
6280 let items = labels.map(|mut label| {
6281 if label.ends_with('*') {
6282 label = label.trim_end_matches('*');
6283 active_item_index = index;
6284 }
6285
6286 let labeled_item = Box::new(cx.new(|cx| TestItem::new(cx).with_label(label)));
6287 pane.add_item(labeled_item.clone(), false, false, None, window, cx);
6288 index += 1;
6289 labeled_item
6290 });
6291
6292 pane.activate_item(active_item_index, false, false, window, cx);
6293
6294 items
6295 })
6296 }
6297
6298 // Assert the item label, with the active item label suffixed with a '*'
6299 #[track_caller]
6300 fn assert_item_labels<const COUNT: usize>(
6301 pane: &Entity<Pane>,
6302 expected_states: [&str; COUNT],
6303 cx: &mut VisualTestContext,
6304 ) {
6305 let actual_states = pane.update(cx, |pane, cx| {
6306 pane.items
6307 .iter()
6308 .enumerate()
6309 .map(|(ix, item)| {
6310 let mut state = item
6311 .to_any()
6312 .downcast::<TestItem>()
6313 .unwrap()
6314 .read(cx)
6315 .label
6316 .clone();
6317 if ix == pane.active_item_index {
6318 state.push('*');
6319 }
6320 if item.is_dirty(cx) {
6321 state.push('^');
6322 }
6323 if pane.is_tab_pinned(ix) {
6324 state.push('!');
6325 }
6326 state
6327 })
6328 .collect::<Vec<_>>()
6329 });
6330 assert_eq!(
6331 actual_states, expected_states,
6332 "pane items do not match expectation"
6333 );
6334 }
6335}