1use crate::{
2 item::{
3 ActivateOnClose, ClosePosition, Item, ItemHandle, ItemSettings, PreviewTabsSettings,
4 ShowDiagnostics, TabContentParams, WeakItemHandle,
5 },
6 move_item,
7 notifications::NotifyResultExt,
8 toolbar::Toolbar,
9 workspace_settings::{AutosaveSetting, TabBarSettings, WorkspaceSettings},
10 CloseWindow, CopyPath, CopyRelativePath, NewFile, NewTerminal, OpenInTerminal, OpenTerminal,
11 OpenVisible, SplitDirection, ToggleFileFinder, ToggleProjectSymbols, ToggleZoom, Workspace,
12};
13use anyhow::Result;
14use collections::{BTreeSet, HashMap, HashSet, VecDeque};
15use futures::{stream::FuturesUnordered, StreamExt};
16use gpui::{
17 actions, anchored, deferred, impl_actions, prelude::*, Action, AnyElement, AppContext,
18 AsyncWindowContext, ClickEvent, ClipboardItem, Corner, Div, DragMoveEvent, EntityId,
19 EventEmitter, ExternalPaths, FocusHandle, FocusOutEvent, FocusableView, KeyContext, Model,
20 MouseButton, MouseDownEvent, NavigationDirection, Pixels, Point, PromptLevel, Render,
21 ScrollHandle, Subscription, Task, View, ViewContext, VisualContext, WeakFocusHandle, WeakView,
22 WindowContext,
23};
24use itertools::Itertools;
25use language::DiagnosticSeverity;
26use parking_lot::Mutex;
27use project::{Project, ProjectEntryId, ProjectPath, WorktreeId};
28use serde::Deserialize;
29use settings::{Settings, SettingsStore};
30use std::{
31 any::Any,
32 cmp, fmt, mem,
33 ops::ControlFlow,
34 path::PathBuf,
35 rc::Rc,
36 sync::{
37 atomic::{AtomicUsize, Ordering},
38 Arc,
39 },
40};
41use theme::ThemeSettings;
42use ui::{
43 prelude::*, right_click_menu, ButtonSize, Color, DecoratedIcon, IconButton, IconButtonShape,
44 IconDecoration, IconDecorationKind, IconName, IconSize, Indicator, Label, PopoverMenu,
45 PopoverMenuHandle, Tab, TabBar, TabPosition, Tooltip,
46};
47use ui::{v_flex, ContextMenu};
48use util::{debug_panic, maybe, truncate_and_remove_front, ResultExt};
49
50/// A selected entry in e.g. project panel.
51#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
52pub struct SelectedEntry {
53 pub worktree_id: WorktreeId,
54 pub entry_id: ProjectEntryId,
55}
56
57/// A group of selected entries from project panel.
58#[derive(Debug)]
59pub struct DraggedSelection {
60 pub active_selection: SelectedEntry,
61 pub marked_selections: Arc<BTreeSet<SelectedEntry>>,
62}
63
64impl DraggedSelection {
65 pub fn items<'a>(&'a self) -> Box<dyn Iterator<Item = &'a SelectedEntry> + 'a> {
66 if self.marked_selections.contains(&self.active_selection) {
67 Box::new(self.marked_selections.iter())
68 } else {
69 Box::new(std::iter::once(&self.active_selection))
70 }
71 }
72}
73
74#[derive(PartialEq, Clone, Copy, Deserialize, Debug)]
75#[serde(rename_all = "camelCase")]
76pub enum SaveIntent {
77 /// write all files (even if unchanged)
78 /// prompt before overwriting on-disk changes
79 Save,
80 /// same as Save, but without auto formatting
81 SaveWithoutFormat,
82 /// write any files that have local changes
83 /// prompt before overwriting on-disk changes
84 SaveAll,
85 /// always prompt for a new path
86 SaveAs,
87 /// prompt "you have unsaved changes" before writing
88 Close,
89 /// write all dirty files, don't prompt on conflict
90 Overwrite,
91 /// skip all save-related behavior
92 Skip,
93}
94
95#[derive(Clone, Deserialize, PartialEq, Debug)]
96pub struct ActivateItem(pub usize);
97
98#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
99#[serde(rename_all = "camelCase")]
100pub struct CloseActiveItem {
101 pub save_intent: Option<SaveIntent>,
102}
103
104#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
105#[serde(rename_all = "camelCase")]
106pub struct CloseInactiveItems {
107 pub save_intent: Option<SaveIntent>,
108 #[serde(default)]
109 pub close_pinned: bool,
110}
111
112#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
113#[serde(rename_all = "camelCase")]
114pub struct CloseAllItems {
115 pub save_intent: Option<SaveIntent>,
116 #[serde(default)]
117 pub close_pinned: bool,
118}
119
120#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
121#[serde(rename_all = "camelCase")]
122pub struct CloseCleanItems {
123 #[serde(default)]
124 pub close_pinned: bool,
125}
126
127#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
128#[serde(rename_all = "camelCase")]
129pub struct CloseItemsToTheRight {
130 #[serde(default)]
131 pub close_pinned: bool,
132}
133
134#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
135#[serde(rename_all = "camelCase")]
136pub struct CloseItemsToTheLeft {
137 #[serde(default)]
138 pub close_pinned: bool,
139}
140
141#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
142#[serde(rename_all = "camelCase")]
143pub struct RevealInProjectPanel {
144 pub entry_id: Option<u64>,
145}
146
147#[derive(Default, PartialEq, Clone, Deserialize)]
148pub struct DeploySearch {
149 #[serde(default)]
150 pub replace_enabled: bool,
151}
152
153impl_actions!(
154 pane,
155 [
156 CloseAllItems,
157 CloseActiveItem,
158 CloseCleanItems,
159 CloseItemsToTheLeft,
160 CloseItemsToTheRight,
161 CloseInactiveItems,
162 ActivateItem,
163 RevealInProjectPanel,
164 DeploySearch,
165 ]
166);
167
168actions!(
169 pane,
170 [
171 ActivatePrevItem,
172 ActivateNextItem,
173 ActivateLastItem,
174 AlternateFile,
175 GoBack,
176 GoForward,
177 JoinIntoNext,
178 JoinAll,
179 ReopenClosedItem,
180 SplitLeft,
181 SplitUp,
182 SplitRight,
183 SplitDown,
184 SplitHorizontal,
185 SplitVertical,
186 SwapItemLeft,
187 SwapItemRight,
188 TogglePreviewTab,
189 TogglePinTab,
190 ]
191);
192
193impl DeploySearch {
194 pub fn find() -> Self {
195 Self {
196 replace_enabled: false,
197 }
198 }
199}
200
201const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
202
203pub enum Event {
204 AddItem {
205 item: Box<dyn ItemHandle>,
206 },
207 ActivateItem {
208 local: bool,
209 },
210 Remove {
211 focus_on_pane: Option<View<Pane>>,
212 },
213 RemoveItem {
214 idx: usize,
215 },
216 RemovedItem {
217 item_id: EntityId,
218 },
219 Split(SplitDirection),
220 JoinAll,
221 JoinIntoNext,
222 ChangeItemTitle,
223 Focus,
224 ZoomIn,
225 ZoomOut,
226 UserSavedItem {
227 item: Box<dyn WeakItemHandle>,
228 save_intent: SaveIntent,
229 },
230}
231
232impl fmt::Debug for Event {
233 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
234 match self {
235 Event::AddItem { item } => f
236 .debug_struct("AddItem")
237 .field("item", &item.item_id())
238 .finish(),
239 Event::ActivateItem { local } => f
240 .debug_struct("ActivateItem")
241 .field("local", local)
242 .finish(),
243 Event::Remove { .. } => f.write_str("Remove"),
244 Event::RemoveItem { idx } => f.debug_struct("RemoveItem").field("idx", idx).finish(),
245 Event::RemovedItem { item_id } => f
246 .debug_struct("RemovedItem")
247 .field("item_id", item_id)
248 .finish(),
249 Event::Split(direction) => f
250 .debug_struct("Split")
251 .field("direction", direction)
252 .finish(),
253 Event::JoinAll => f.write_str("JoinAll"),
254 Event::JoinIntoNext => f.write_str("JoinIntoNext"),
255 Event::ChangeItemTitle => f.write_str("ChangeItemTitle"),
256 Event::Focus => f.write_str("Focus"),
257 Event::ZoomIn => f.write_str("ZoomIn"),
258 Event::ZoomOut => f.write_str("ZoomOut"),
259 Event::UserSavedItem { item, save_intent } => f
260 .debug_struct("UserSavedItem")
261 .field("item", &item.id())
262 .field("save_intent", save_intent)
263 .finish(),
264 }
265 }
266}
267
268/// A container for 0 to many items that are open in the workspace.
269/// Treats all items uniformly via the [`ItemHandle`] trait, whether it's an editor, search results multibuffer, terminal or something else,
270/// responsible for managing item tabs, focus and zoom states and drag and drop features.
271/// Can be split, see `PaneGroup` for more details.
272pub struct Pane {
273 alternate_file_items: (
274 Option<Box<dyn WeakItemHandle>>,
275 Option<Box<dyn WeakItemHandle>>,
276 ),
277 focus_handle: FocusHandle,
278 items: Vec<Box<dyn ItemHandle>>,
279 activation_history: Vec<ActivationHistoryEntry>,
280 next_activation_timestamp: Arc<AtomicUsize>,
281 zoomed: bool,
282 was_focused: bool,
283 active_item_index: usize,
284 preview_item_id: Option<EntityId>,
285 last_focus_handle_by_item: HashMap<EntityId, WeakFocusHandle>,
286 nav_history: NavHistory,
287 toolbar: View<Toolbar>,
288 pub(crate) workspace: WeakView<Workspace>,
289 project: Model<Project>,
290 drag_split_direction: Option<SplitDirection>,
291 can_drop_predicate: Option<Arc<dyn Fn(&dyn Any, &mut WindowContext) -> bool>>,
292 custom_drop_handle:
293 Option<Arc<dyn Fn(&mut Pane, &dyn Any, &mut ViewContext<Pane>) -> ControlFlow<(), ()>>>,
294 can_split_predicate: Option<Arc<dyn Fn(&mut Self, &dyn Any, &mut ViewContext<Self>) -> bool>>,
295 should_display_tab_bar: Rc<dyn Fn(&ViewContext<Pane>) -> bool>,
296 render_tab_bar_buttons:
297 Rc<dyn Fn(&mut Pane, &mut ViewContext<Pane>) -> (Option<AnyElement>, Option<AnyElement>)>,
298 _subscriptions: Vec<Subscription>,
299 tab_bar_scroll_handle: ScrollHandle,
300 /// Is None if navigation buttons are permanently turned off (and should not react to setting changes).
301 /// Otherwise, when `display_nav_history_buttons` is Some, it determines whether nav buttons should be displayed.
302 display_nav_history_buttons: Option<bool>,
303 double_click_dispatch_action: Box<dyn Action>,
304 save_modals_spawned: HashSet<EntityId>,
305 pub new_item_context_menu_handle: PopoverMenuHandle<ContextMenu>,
306 pub split_item_context_menu_handle: PopoverMenuHandle<ContextMenu>,
307 pinned_tab_count: usize,
308 diagnostics: HashMap<ProjectPath, DiagnosticSeverity>,
309 zoom_out_on_close: bool,
310}
311
312pub struct ActivationHistoryEntry {
313 pub entity_id: EntityId,
314 pub timestamp: usize,
315}
316
317pub struct ItemNavHistory {
318 history: NavHistory,
319 item: Arc<dyn WeakItemHandle>,
320 is_preview: bool,
321}
322
323#[derive(Clone)]
324pub struct NavHistory(Arc<Mutex<NavHistoryState>>);
325
326struct NavHistoryState {
327 mode: NavigationMode,
328 backward_stack: VecDeque<NavigationEntry>,
329 forward_stack: VecDeque<NavigationEntry>,
330 closed_stack: VecDeque<NavigationEntry>,
331 paths_by_item: HashMap<EntityId, (ProjectPath, Option<PathBuf>)>,
332 pane: WeakView<Pane>,
333 next_timestamp: Arc<AtomicUsize>,
334}
335
336#[derive(Debug, Copy, Clone)]
337pub enum NavigationMode {
338 Normal,
339 GoingBack,
340 GoingForward,
341 ClosingItem,
342 ReopeningClosedItem,
343 Disabled,
344}
345
346impl Default for NavigationMode {
347 fn default() -> Self {
348 Self::Normal
349 }
350}
351
352pub struct NavigationEntry {
353 pub item: Arc<dyn WeakItemHandle>,
354 pub data: Option<Box<dyn Any + Send>>,
355 pub timestamp: usize,
356 pub is_preview: bool,
357}
358
359#[derive(Clone)]
360pub struct DraggedTab {
361 pub pane: View<Pane>,
362 pub item: Box<dyn ItemHandle>,
363 pub ix: usize,
364 pub detail: usize,
365 pub is_active: bool,
366}
367
368impl EventEmitter<Event> for Pane {}
369
370impl Pane {
371 pub fn new(
372 workspace: WeakView<Workspace>,
373 project: Model<Project>,
374 next_timestamp: Arc<AtomicUsize>,
375 can_drop_predicate: Option<Arc<dyn Fn(&dyn Any, &mut WindowContext) -> bool + 'static>>,
376 double_click_dispatch_action: Box<dyn Action>,
377 cx: &mut ViewContext<Self>,
378 ) -> Self {
379 let focus_handle = cx.focus_handle();
380
381 let subscriptions = vec![
382 cx.on_focus(&focus_handle, Pane::focus_in),
383 cx.on_focus_in(&focus_handle, Pane::focus_in),
384 cx.on_focus_out(&focus_handle, Pane::focus_out),
385 cx.observe_global::<SettingsStore>(Self::settings_changed),
386 cx.subscribe(&project, Self::project_events),
387 ];
388
389 let handle = cx.view().downgrade();
390 Self {
391 alternate_file_items: (None, None),
392 focus_handle,
393 items: Vec::new(),
394 activation_history: Vec::new(),
395 next_activation_timestamp: next_timestamp.clone(),
396 was_focused: false,
397 zoomed: false,
398 active_item_index: 0,
399 preview_item_id: None,
400 last_focus_handle_by_item: Default::default(),
401 nav_history: NavHistory(Arc::new(Mutex::new(NavHistoryState {
402 mode: NavigationMode::Normal,
403 backward_stack: Default::default(),
404 forward_stack: Default::default(),
405 closed_stack: Default::default(),
406 paths_by_item: Default::default(),
407 pane: handle.clone(),
408 next_timestamp,
409 }))),
410 toolbar: cx.new_view(|_| Toolbar::new()),
411 tab_bar_scroll_handle: ScrollHandle::new(),
412 drag_split_direction: None,
413 workspace,
414 project,
415 can_drop_predicate,
416 custom_drop_handle: None,
417 can_split_predicate: None,
418 should_display_tab_bar: Rc::new(|cx| TabBarSettings::get_global(cx).show),
419 render_tab_bar_buttons: Rc::new(move |pane, cx| {
420 if !pane.has_focus(cx) && !pane.context_menu_focused(cx) {
421 return (None, None);
422 }
423 // Ideally we would return a vec of elements here to pass directly to the [TabBar]'s
424 // `end_slot`, but due to needing a view here that isn't possible.
425 let right_children = h_flex()
426 // Instead we need to replicate the spacing from the [TabBar]'s `end_slot` here.
427 .gap(DynamicSpacing::Base04.rems(cx))
428 .child(
429 PopoverMenu::new("pane-tab-bar-popover-menu")
430 .trigger(
431 IconButton::new("plus", IconName::Plus)
432 .icon_size(IconSize::Small)
433 .tooltip(|cx| Tooltip::text("New...", cx)),
434 )
435 .anchor(Corner::TopRight)
436 .with_handle(pane.new_item_context_menu_handle.clone())
437 .menu(move |cx| {
438 Some(ContextMenu::build(cx, |menu, _| {
439 menu.action("New File", NewFile.boxed_clone())
440 .action(
441 "Open File",
442 ToggleFileFinder::default().boxed_clone(),
443 )
444 .separator()
445 .action(
446 "Search Project",
447 DeploySearch {
448 replace_enabled: false,
449 }
450 .boxed_clone(),
451 )
452 .action(
453 "Search Symbols",
454 ToggleProjectSymbols.boxed_clone(),
455 )
456 .separator()
457 .action("New Terminal", NewTerminal.boxed_clone())
458 }))
459 }),
460 )
461 .child(
462 PopoverMenu::new("pane-tab-bar-split")
463 .trigger(
464 IconButton::new("split", IconName::Split)
465 .icon_size(IconSize::Small)
466 .tooltip(|cx| Tooltip::text("Split Pane", cx)),
467 )
468 .anchor(Corner::TopRight)
469 .with_handle(pane.split_item_context_menu_handle.clone())
470 .menu(move |cx| {
471 ContextMenu::build(cx, |menu, _| {
472 menu.action("Split Right", SplitRight.boxed_clone())
473 .action("Split Left", SplitLeft.boxed_clone())
474 .action("Split Up", SplitUp.boxed_clone())
475 .action("Split Down", SplitDown.boxed_clone())
476 })
477 .into()
478 }),
479 )
480 .child({
481 let zoomed = pane.is_zoomed();
482 IconButton::new("toggle_zoom", IconName::Maximize)
483 .icon_size(IconSize::Small)
484 .toggle_state(zoomed)
485 .selected_icon(IconName::Minimize)
486 .on_click(cx.listener(|pane, _, cx| {
487 pane.toggle_zoom(&crate::ToggleZoom, cx);
488 }))
489 .tooltip(move |cx| {
490 Tooltip::for_action(
491 if zoomed { "Zoom Out" } else { "Zoom In" },
492 &ToggleZoom,
493 cx,
494 )
495 })
496 })
497 .into_any_element()
498 .into();
499 (None, right_children)
500 }),
501 display_nav_history_buttons: Some(
502 TabBarSettings::get_global(cx).show_nav_history_buttons,
503 ),
504 _subscriptions: subscriptions,
505 double_click_dispatch_action,
506 save_modals_spawned: HashSet::default(),
507 split_item_context_menu_handle: Default::default(),
508 new_item_context_menu_handle: Default::default(),
509 pinned_tab_count: 0,
510 diagnostics: Default::default(),
511 zoom_out_on_close: true,
512 }
513 }
514
515 fn alternate_file(&mut self, cx: &mut ViewContext<Pane>) {
516 let (_, alternative) = &self.alternate_file_items;
517 if let Some(alternative) = alternative {
518 let existing = self
519 .items()
520 .find_position(|item| item.item_id() == alternative.id());
521 if let Some((ix, _)) = existing {
522 self.activate_item(ix, true, true, cx);
523 } else if let Some(upgraded) = alternative.upgrade() {
524 self.add_item(upgraded, true, true, None, cx);
525 }
526 }
527 }
528
529 pub fn track_alternate_file_items(&mut self) {
530 if let Some(item) = self.active_item().map(|item| item.downgrade_item()) {
531 let (current, _) = &self.alternate_file_items;
532 match current {
533 Some(current) => {
534 if current.id() != item.id() {
535 self.alternate_file_items =
536 (Some(item), self.alternate_file_items.0.take());
537 }
538 }
539 None => {
540 self.alternate_file_items = (Some(item), None);
541 }
542 }
543 }
544 }
545
546 pub fn has_focus(&self, cx: &WindowContext) -> bool {
547 // We not only check whether our focus handle contains focus, but also
548 // whether the active item might have focus, because we might have just activated an item
549 // that hasn't rendered yet.
550 // Before the next render, we might transfer focus
551 // to the item, and `focus_handle.contains_focus` returns false because the `active_item`
552 // is not hooked up to us in the dispatch tree.
553 self.focus_handle.contains_focused(cx)
554 || self
555 .active_item()
556 .map_or(false, |item| item.focus_handle(cx).contains_focused(cx))
557 }
558
559 fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
560 if !self.was_focused {
561 self.was_focused = true;
562 cx.emit(Event::Focus);
563 cx.notify();
564 }
565
566 self.toolbar.update(cx, |toolbar, cx| {
567 toolbar.focus_changed(true, cx);
568 });
569
570 if let Some(active_item) = self.active_item() {
571 if self.focus_handle.is_focused(cx) {
572 // Pane was focused directly. We need to either focus a view inside the active item,
573 // or focus the active item itself
574 if let Some(weak_last_focus_handle) =
575 self.last_focus_handle_by_item.get(&active_item.item_id())
576 {
577 if let Some(focus_handle) = weak_last_focus_handle.upgrade() {
578 focus_handle.focus(cx);
579 return;
580 }
581 }
582
583 active_item.focus_handle(cx).focus(cx);
584 } else if let Some(focused) = cx.focused() {
585 if !self.context_menu_focused(cx) {
586 self.last_focus_handle_by_item
587 .insert(active_item.item_id(), focused.downgrade());
588 }
589 }
590 }
591 }
592
593 pub fn context_menu_focused(&self, cx: &mut ViewContext<Self>) -> bool {
594 self.new_item_context_menu_handle.is_focused(cx)
595 || self.split_item_context_menu_handle.is_focused(cx)
596 }
597
598 fn focus_out(&mut self, _event: FocusOutEvent, cx: &mut ViewContext<Self>) {
599 self.was_focused = false;
600 self.toolbar.update(cx, |toolbar, cx| {
601 toolbar.focus_changed(false, cx);
602 });
603 cx.notify();
604 }
605
606 fn project_events(
607 this: &mut Pane,
608 _project: Model<Project>,
609 event: &project::Event,
610 cx: &mut ViewContext<Self>,
611 ) {
612 match event {
613 project::Event::DiskBasedDiagnosticsFinished { .. }
614 | project::Event::DiagnosticsUpdated { .. } => {
615 if ItemSettings::get_global(cx).show_diagnostics != ShowDiagnostics::Off {
616 this.update_diagnostics(cx);
617 cx.notify();
618 }
619 }
620 _ => {}
621 }
622 }
623
624 fn update_diagnostics(&mut self, cx: &mut ViewContext<Self>) {
625 let show_diagnostics = ItemSettings::get_global(cx).show_diagnostics;
626 self.diagnostics = if show_diagnostics != ShowDiagnostics::Off {
627 self.project
628 .read(cx)
629 .diagnostic_summaries(false, cx)
630 .filter_map(|(project_path, _, diagnostic_summary)| {
631 if diagnostic_summary.error_count > 0 {
632 Some((project_path, DiagnosticSeverity::ERROR))
633 } else if diagnostic_summary.warning_count > 0
634 && show_diagnostics != ShowDiagnostics::Errors
635 {
636 Some((project_path, DiagnosticSeverity::WARNING))
637 } else {
638 None
639 }
640 })
641 .collect::<HashMap<_, _>>()
642 } else {
643 Default::default()
644 }
645 }
646
647 fn settings_changed(&mut self, cx: &mut ViewContext<Self>) {
648 if let Some(display_nav_history_buttons) = self.display_nav_history_buttons.as_mut() {
649 *display_nav_history_buttons = TabBarSettings::get_global(cx).show_nav_history_buttons;
650 }
651 if !PreviewTabsSettings::get_global(cx).enabled {
652 self.preview_item_id = None;
653 }
654 self.update_diagnostics(cx);
655 cx.notify();
656 }
657
658 pub fn active_item_index(&self) -> usize {
659 self.active_item_index
660 }
661
662 pub fn activation_history(&self) -> &[ActivationHistoryEntry] {
663 &self.activation_history
664 }
665
666 pub fn set_should_display_tab_bar<F>(&mut self, should_display_tab_bar: F)
667 where
668 F: 'static + Fn(&ViewContext<Pane>) -> bool,
669 {
670 self.should_display_tab_bar = Rc::new(should_display_tab_bar);
671 }
672
673 pub fn set_can_split(
674 &mut self,
675 can_split_predicate: Option<
676 Arc<dyn Fn(&mut Self, &dyn Any, &mut ViewContext<Self>) -> bool + 'static>,
677 >,
678 ) {
679 self.can_split_predicate = can_split_predicate;
680 }
681
682 pub fn set_can_navigate(&mut self, can_navigate: bool, cx: &mut ViewContext<Self>) {
683 self.toolbar.update(cx, |toolbar, cx| {
684 toolbar.set_can_navigate(can_navigate, cx);
685 });
686 cx.notify();
687 }
688
689 pub fn set_render_tab_bar_buttons<F>(&mut self, cx: &mut ViewContext<Self>, render: F)
690 where
691 F: 'static
692 + Fn(&mut Pane, &mut ViewContext<Pane>) -> (Option<AnyElement>, Option<AnyElement>),
693 {
694 self.render_tab_bar_buttons = Rc::new(render);
695 cx.notify();
696 }
697
698 pub fn set_custom_drop_handle<F>(&mut self, cx: &mut ViewContext<Self>, handle: F)
699 where
700 F: 'static + Fn(&mut Pane, &dyn Any, &mut ViewContext<Pane>) -> ControlFlow<(), ()>,
701 {
702 self.custom_drop_handle = Some(Arc::new(handle));
703 cx.notify();
704 }
705
706 pub fn nav_history_for_item<T: Item>(&self, item: &View<T>) -> ItemNavHistory {
707 ItemNavHistory {
708 history: self.nav_history.clone(),
709 item: Arc::new(item.downgrade()),
710 is_preview: self.preview_item_id == Some(item.item_id()),
711 }
712 }
713
714 pub fn nav_history(&self) -> &NavHistory {
715 &self.nav_history
716 }
717
718 pub fn nav_history_mut(&mut self) -> &mut NavHistory {
719 &mut self.nav_history
720 }
721
722 pub fn disable_history(&mut self) {
723 self.nav_history.disable();
724 }
725
726 pub fn enable_history(&mut self) {
727 self.nav_history.enable();
728 }
729
730 pub fn can_navigate_backward(&self) -> bool {
731 !self.nav_history.0.lock().backward_stack.is_empty()
732 }
733
734 pub fn can_navigate_forward(&self) -> bool {
735 !self.nav_history.0.lock().forward_stack.is_empty()
736 }
737
738 fn navigate_backward(&mut self, cx: &mut ViewContext<Self>) {
739 if let Some(workspace) = self.workspace.upgrade() {
740 let pane = cx.view().downgrade();
741 cx.window_context().defer(move |cx| {
742 workspace.update(cx, |workspace, cx| {
743 workspace.go_back(pane, cx).detach_and_log_err(cx)
744 })
745 })
746 }
747 }
748
749 fn navigate_forward(&mut self, cx: &mut ViewContext<Self>) {
750 if let Some(workspace) = self.workspace.upgrade() {
751 let pane = cx.view().downgrade();
752 cx.window_context().defer(move |cx| {
753 workspace.update(cx, |workspace, cx| {
754 workspace.go_forward(pane, cx).detach_and_log_err(cx)
755 })
756 })
757 }
758 }
759
760 fn join_into_next(&mut self, cx: &mut ViewContext<Self>) {
761 cx.emit(Event::JoinIntoNext);
762 }
763
764 fn join_all(&mut self, cx: &mut ViewContext<Self>) {
765 cx.emit(Event::JoinAll);
766 }
767
768 fn history_updated(&mut self, cx: &mut ViewContext<Self>) {
769 self.toolbar.update(cx, |_, cx| cx.notify());
770 }
771
772 pub fn preview_item_id(&self) -> Option<EntityId> {
773 self.preview_item_id
774 }
775
776 pub fn preview_item(&self) -> Option<Box<dyn ItemHandle>> {
777 self.preview_item_id
778 .and_then(|id| self.items.iter().find(|item| item.item_id() == id))
779 .cloned()
780 }
781
782 fn preview_item_idx(&self) -> Option<usize> {
783 if let Some(preview_item_id) = self.preview_item_id {
784 self.items
785 .iter()
786 .position(|item| item.item_id() == preview_item_id)
787 } else {
788 None
789 }
790 }
791
792 pub fn is_active_preview_item(&self, item_id: EntityId) -> bool {
793 self.preview_item_id == Some(item_id)
794 }
795
796 /// Marks the item with the given ID as the preview item.
797 /// This will be ignored if the global setting `preview_tabs` is disabled.
798 pub fn set_preview_item_id(&mut self, item_id: Option<EntityId>, cx: &AppContext) {
799 if PreviewTabsSettings::get_global(cx).enabled {
800 self.preview_item_id = item_id;
801 }
802 }
803
804 pub(crate) fn set_pinned_count(&mut self, count: usize) {
805 self.pinned_tab_count = count;
806 }
807
808 pub(crate) fn pinned_count(&self) -> usize {
809 self.pinned_tab_count
810 }
811
812 pub fn handle_item_edit(&mut self, item_id: EntityId, cx: &AppContext) {
813 if let Some(preview_item) = self.preview_item() {
814 if preview_item.item_id() == item_id && !preview_item.preserve_preview(cx) {
815 self.set_preview_item_id(None, cx);
816 }
817 }
818 }
819
820 pub(crate) fn open_item(
821 &mut self,
822 project_entry_id: Option<ProjectEntryId>,
823 focus_item: bool,
824 allow_preview: bool,
825 suggested_position: Option<usize>,
826 cx: &mut ViewContext<Self>,
827 build_item: impl FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
828 ) -> Box<dyn ItemHandle> {
829 let mut existing_item = None;
830 if let Some(project_entry_id) = project_entry_id {
831 for (index, item) in self.items.iter().enumerate() {
832 if item.is_singleton(cx)
833 && item.project_entry_ids(cx).as_slice() == [project_entry_id]
834 {
835 let item = item.boxed_clone();
836 existing_item = Some((index, item));
837 break;
838 }
839 }
840 }
841
842 if let Some((index, existing_item)) = existing_item {
843 // If the item is already open, and the item is a preview item
844 // and we are not allowing items to open as preview, mark the item as persistent.
845 if let Some(preview_item_id) = self.preview_item_id {
846 if let Some(tab) = self.items.get(index) {
847 if tab.item_id() == preview_item_id && !allow_preview {
848 self.set_preview_item_id(None, cx);
849 }
850 }
851 }
852
853 self.activate_item(index, focus_item, focus_item, cx);
854 existing_item
855 } else {
856 // If the item is being opened as preview and we have an existing preview tab,
857 // open the new item in the position of the existing preview tab.
858 let destination_index = if allow_preview {
859 self.close_current_preview_item(cx)
860 } else {
861 suggested_position
862 };
863
864 let new_item = build_item(cx);
865
866 if allow_preview {
867 self.set_preview_item_id(Some(new_item.item_id()), cx);
868 }
869
870 self.add_item(new_item.clone(), true, focus_item, destination_index, cx);
871
872 new_item
873 }
874 }
875
876 pub fn close_current_preview_item(&mut self, cx: &mut ViewContext<Self>) -> Option<usize> {
877 let item_idx = self.preview_item_idx()?;
878 let id = self.preview_item_id()?;
879
880 let prev_active_item_index = self.active_item_index;
881 self.remove_item(id, false, false, cx);
882 self.active_item_index = prev_active_item_index;
883
884 if item_idx < self.items.len() {
885 Some(item_idx)
886 } else {
887 None
888 }
889 }
890
891 pub fn add_item(
892 &mut self,
893 item: Box<dyn ItemHandle>,
894 activate_pane: bool,
895 focus_item: bool,
896 destination_index: Option<usize>,
897 cx: &mut ViewContext<Self>,
898 ) {
899 self.close_items_over_max_tabs(cx);
900
901 if item.is_singleton(cx) {
902 if let Some(&entry_id) = item.project_entry_ids(cx).first() {
903 let project = self.project.read(cx);
904 if let Some(project_path) = project.path_for_entry(entry_id, cx) {
905 let abs_path = project.absolute_path(&project_path, cx);
906 self.nav_history
907 .0
908 .lock()
909 .paths_by_item
910 .insert(item.item_id(), (project_path, abs_path));
911 }
912 }
913 }
914 // If no destination index is specified, add or move the item after the
915 // active item (or at the start of tab bar, if the active item is pinned)
916 let mut insertion_index = {
917 cmp::min(
918 if let Some(destination_index) = destination_index {
919 destination_index
920 } else {
921 cmp::max(self.active_item_index + 1, self.pinned_count())
922 },
923 self.items.len(),
924 )
925 };
926
927 // Does the item already exist?
928 let project_entry_id = if item.is_singleton(cx) {
929 item.project_entry_ids(cx).first().copied()
930 } else {
931 None
932 };
933
934 let existing_item_index = self.items.iter().position(|existing_item| {
935 if existing_item.item_id() == item.item_id() {
936 true
937 } else if existing_item.is_singleton(cx) {
938 existing_item
939 .project_entry_ids(cx)
940 .first()
941 .map_or(false, |existing_entry_id| {
942 Some(existing_entry_id) == project_entry_id.as_ref()
943 })
944 } else {
945 false
946 }
947 });
948
949 if let Some(existing_item_index) = existing_item_index {
950 // If the item already exists, move it to the desired destination and activate it
951
952 if existing_item_index != insertion_index {
953 let existing_item_is_active = existing_item_index == self.active_item_index;
954
955 // If the caller didn't specify a destination and the added item is already
956 // the active one, don't move it
957 if existing_item_is_active && destination_index.is_none() {
958 insertion_index = existing_item_index;
959 } else {
960 self.items.remove(existing_item_index);
961 if existing_item_index < self.active_item_index {
962 self.active_item_index -= 1;
963 }
964 insertion_index = insertion_index.min(self.items.len());
965
966 self.items.insert(insertion_index, item.clone());
967
968 if existing_item_is_active {
969 self.active_item_index = insertion_index;
970 } else if insertion_index <= self.active_item_index {
971 self.active_item_index += 1;
972 }
973 }
974
975 cx.notify();
976 }
977
978 self.activate_item(insertion_index, activate_pane, focus_item, cx);
979 } else {
980 self.items.insert(insertion_index, item.clone());
981
982 if insertion_index <= self.active_item_index
983 && self.preview_item_idx() != Some(self.active_item_index)
984 {
985 self.active_item_index += 1;
986 }
987
988 self.activate_item(insertion_index, activate_pane, focus_item, cx);
989 cx.notify();
990 }
991
992 cx.emit(Event::AddItem { item });
993 }
994
995 pub fn items_len(&self) -> usize {
996 self.items.len()
997 }
998
999 pub fn items(&self) -> impl DoubleEndedIterator<Item = &Box<dyn ItemHandle>> {
1000 self.items.iter()
1001 }
1002
1003 pub fn items_of_type<T: Render>(&self) -> impl '_ + Iterator<Item = View<T>> {
1004 self.items
1005 .iter()
1006 .filter_map(|item| item.to_any().downcast().ok())
1007 }
1008
1009 pub fn active_item(&self) -> Option<Box<dyn ItemHandle>> {
1010 self.items.get(self.active_item_index).cloned()
1011 }
1012
1013 pub fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Point<Pixels>> {
1014 self.items
1015 .get(self.active_item_index)?
1016 .pixel_position_of_cursor(cx)
1017 }
1018
1019 pub fn item_for_entry(
1020 &self,
1021 entry_id: ProjectEntryId,
1022 cx: &AppContext,
1023 ) -> Option<Box<dyn ItemHandle>> {
1024 self.items.iter().find_map(|item| {
1025 if item.is_singleton(cx) && (item.project_entry_ids(cx).as_slice() == [entry_id]) {
1026 Some(item.boxed_clone())
1027 } else {
1028 None
1029 }
1030 })
1031 }
1032
1033 pub fn item_for_path(
1034 &self,
1035 project_path: ProjectPath,
1036 cx: &AppContext,
1037 ) -> Option<Box<dyn ItemHandle>> {
1038 self.items.iter().find_map(move |item| {
1039 if item.is_singleton(cx) && (item.project_path(cx).as_slice() == [project_path.clone()])
1040 {
1041 Some(item.boxed_clone())
1042 } else {
1043 None
1044 }
1045 })
1046 }
1047
1048 pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option<usize> {
1049 self.index_for_item_id(item.item_id())
1050 }
1051
1052 fn index_for_item_id(&self, item_id: EntityId) -> Option<usize> {
1053 self.items.iter().position(|i| i.item_id() == item_id)
1054 }
1055
1056 pub fn item_for_index(&self, ix: usize) -> Option<&dyn ItemHandle> {
1057 self.items.get(ix).map(|i| i.as_ref())
1058 }
1059
1060 pub fn toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext<Self>) {
1061 if self.zoomed {
1062 cx.emit(Event::ZoomOut);
1063 } else if !self.items.is_empty() {
1064 if !self.focus_handle.contains_focused(cx) {
1065 cx.focus_self();
1066 }
1067 cx.emit(Event::ZoomIn);
1068 }
1069 }
1070
1071 pub fn activate_item(
1072 &mut self,
1073 index: usize,
1074 activate_pane: bool,
1075 focus_item: bool,
1076 cx: &mut ViewContext<Self>,
1077 ) {
1078 use NavigationMode::{GoingBack, GoingForward};
1079
1080 if index < self.items.len() {
1081 let prev_active_item_ix = mem::replace(&mut self.active_item_index, index);
1082 if prev_active_item_ix != self.active_item_index
1083 || matches!(self.nav_history.mode(), GoingBack | GoingForward)
1084 {
1085 if let Some(prev_item) = self.items.get(prev_active_item_ix) {
1086 prev_item.deactivated(cx);
1087 }
1088 }
1089 cx.emit(Event::ActivateItem {
1090 local: activate_pane,
1091 });
1092
1093 if let Some(newly_active_item) = self.items.get(index) {
1094 self.activation_history
1095 .retain(|entry| entry.entity_id != newly_active_item.item_id());
1096 self.activation_history.push(ActivationHistoryEntry {
1097 entity_id: newly_active_item.item_id(),
1098 timestamp: self
1099 .next_activation_timestamp
1100 .fetch_add(1, Ordering::SeqCst),
1101 });
1102 }
1103
1104 self.update_toolbar(cx);
1105 self.update_status_bar(cx);
1106
1107 if focus_item {
1108 self.focus_active_item(cx);
1109 }
1110
1111 if !self.is_tab_pinned(index) {
1112 self.tab_bar_scroll_handle
1113 .scroll_to_item(index - self.pinned_tab_count);
1114 }
1115
1116 cx.notify();
1117 }
1118 }
1119
1120 pub fn activate_prev_item(&mut self, activate_pane: bool, cx: &mut ViewContext<Self>) {
1121 let mut index = self.active_item_index;
1122 if index > 0 {
1123 index -= 1;
1124 } else if !self.items.is_empty() {
1125 index = self.items.len() - 1;
1126 }
1127 self.activate_item(index, activate_pane, activate_pane, cx);
1128 }
1129
1130 pub fn activate_next_item(&mut self, activate_pane: bool, cx: &mut ViewContext<Self>) {
1131 let mut index = self.active_item_index;
1132 if index + 1 < self.items.len() {
1133 index += 1;
1134 } else {
1135 index = 0;
1136 }
1137 self.activate_item(index, activate_pane, activate_pane, cx);
1138 }
1139
1140 pub fn swap_item_left(&mut self, cx: &mut ViewContext<Self>) {
1141 let index = self.active_item_index;
1142 if index == 0 {
1143 return;
1144 }
1145
1146 self.items.swap(index, index - 1);
1147 self.activate_item(index - 1, true, true, cx);
1148 }
1149
1150 pub fn swap_item_right(&mut self, cx: &mut ViewContext<Self>) {
1151 let index = self.active_item_index;
1152 if index + 1 == self.items.len() {
1153 return;
1154 }
1155
1156 self.items.swap(index, index + 1);
1157 self.activate_item(index + 1, true, true, cx);
1158 }
1159
1160 pub fn close_active_item(
1161 &mut self,
1162 action: &CloseActiveItem,
1163 cx: &mut ViewContext<Self>,
1164 ) -> Option<Task<Result<()>>> {
1165 if self.items.is_empty() {
1166 // Close the window when there's no active items to close, if configured
1167 if WorkspaceSettings::get_global(cx)
1168 .when_closing_with_no_tabs
1169 .should_close()
1170 {
1171 cx.dispatch_action(Box::new(CloseWindow));
1172 }
1173
1174 return None;
1175 }
1176 let active_item_id = self.items[self.active_item_index].item_id();
1177 Some(self.close_item_by_id(
1178 active_item_id,
1179 action.save_intent.unwrap_or(SaveIntent::Close),
1180 cx,
1181 ))
1182 }
1183
1184 pub fn close_item_by_id(
1185 &mut self,
1186 item_id_to_close: EntityId,
1187 save_intent: SaveIntent,
1188 cx: &mut ViewContext<Self>,
1189 ) -> Task<Result<()>> {
1190 self.close_items(cx, save_intent, move |view_id| view_id == item_id_to_close)
1191 }
1192
1193 pub fn close_inactive_items(
1194 &mut self,
1195 action: &CloseInactiveItems,
1196 cx: &mut ViewContext<Self>,
1197 ) -> Option<Task<Result<()>>> {
1198 if self.items.is_empty() {
1199 return None;
1200 }
1201
1202 let active_item_id = self.items[self.active_item_index].item_id();
1203 let non_closeable_items = self.get_non_closeable_item_ids(action.close_pinned);
1204 Some(self.close_items(
1205 cx,
1206 action.save_intent.unwrap_or(SaveIntent::Close),
1207 move |item_id| item_id != active_item_id && !non_closeable_items.contains(&item_id),
1208 ))
1209 }
1210
1211 pub fn close_clean_items(
1212 &mut self,
1213 action: &CloseCleanItems,
1214 cx: &mut ViewContext<Self>,
1215 ) -> Option<Task<Result<()>>> {
1216 let item_ids: Vec<_> = self
1217 .items()
1218 .filter(|item| !item.is_dirty(cx))
1219 .map(|item| item.item_id())
1220 .collect();
1221 let non_closeable_items = self.get_non_closeable_item_ids(action.close_pinned);
1222 Some(self.close_items(cx, SaveIntent::Close, move |item_id| {
1223 item_ids.contains(&item_id) && !non_closeable_items.contains(&item_id)
1224 }))
1225 }
1226
1227 pub fn close_items_to_the_left(
1228 &mut self,
1229 action: &CloseItemsToTheLeft,
1230 cx: &mut ViewContext<Self>,
1231 ) -> Option<Task<Result<()>>> {
1232 if self.items.is_empty() {
1233 return None;
1234 }
1235 let active_item_id = self.items[self.active_item_index].item_id();
1236 let non_closeable_items = self.get_non_closeable_item_ids(action.close_pinned);
1237 Some(self.close_items_to_the_left_by_id(active_item_id, non_closeable_items, cx))
1238 }
1239
1240 pub fn close_items_to_the_left_by_id(
1241 &mut self,
1242 item_id: EntityId,
1243 non_closeable_items: Vec<EntityId>,
1244 cx: &mut ViewContext<Self>,
1245 ) -> Task<Result<()>> {
1246 let item_ids: Vec<_> = self
1247 .items()
1248 .take_while(|item| item.item_id() != item_id)
1249 .map(|item| item.item_id())
1250 .collect();
1251 self.close_items(cx, SaveIntent::Close, move |item_id| {
1252 item_ids.contains(&item_id) && !non_closeable_items.contains(&item_id)
1253 })
1254 }
1255
1256 pub fn close_items_to_the_right(
1257 &mut self,
1258 action: &CloseItemsToTheRight,
1259 cx: &mut ViewContext<Self>,
1260 ) -> Option<Task<Result<()>>> {
1261 if self.items.is_empty() {
1262 return None;
1263 }
1264 let active_item_id = self.items[self.active_item_index].item_id();
1265 let non_closeable_items = self.get_non_closeable_item_ids(action.close_pinned);
1266 Some(self.close_items_to_the_right_by_id(active_item_id, non_closeable_items, cx))
1267 }
1268
1269 pub fn close_items_to_the_right_by_id(
1270 &mut self,
1271 item_id: EntityId,
1272 non_closeable_items: Vec<EntityId>,
1273 cx: &mut ViewContext<Self>,
1274 ) -> Task<Result<()>> {
1275 let item_ids: Vec<_> = self
1276 .items()
1277 .rev()
1278 .take_while(|item| item.item_id() != item_id)
1279 .map(|item| item.item_id())
1280 .collect();
1281 self.close_items(cx, SaveIntent::Close, move |item_id| {
1282 item_ids.contains(&item_id) && !non_closeable_items.contains(&item_id)
1283 })
1284 }
1285
1286 pub fn close_all_items(
1287 &mut self,
1288 action: &CloseAllItems,
1289 cx: &mut ViewContext<Self>,
1290 ) -> Option<Task<Result<()>>> {
1291 if self.items.is_empty() {
1292 return None;
1293 }
1294
1295 let non_closeable_items = self.get_non_closeable_item_ids(action.close_pinned);
1296 Some(self.close_items(
1297 cx,
1298 action.save_intent.unwrap_or(SaveIntent::Close),
1299 |item_id| !non_closeable_items.contains(&item_id),
1300 ))
1301 }
1302
1303 pub fn close_items_over_max_tabs(&mut self, cx: &mut ViewContext<Self>) {
1304 let Some(max_tabs) = WorkspaceSettings::get_global(cx).max_tabs.map(|i| i.get()) else {
1305 return;
1306 };
1307
1308 // Reduce over the activation history to get every dirty items up to max_tabs
1309 // count.
1310 let mut index_list = Vec::new();
1311 let mut items_len = self.items_len();
1312 let mut indexes: HashMap<EntityId, usize> = HashMap::default();
1313 for (index, item) in self.items.iter().enumerate() {
1314 indexes.insert(item.item_id(), index);
1315 }
1316 for entry in self.activation_history.iter() {
1317 if items_len < max_tabs {
1318 break;
1319 }
1320 let Some(&index) = indexes.get(&entry.entity_id) else {
1321 continue;
1322 };
1323 if let Some(true) = self.items.get(index).map(|item| item.is_dirty(cx)) {
1324 continue;
1325 }
1326
1327 index_list.push(index);
1328 items_len -= 1;
1329 }
1330 // The sort and reverse is necessary since we remove items
1331 // using their index position, hence removing from the end
1332 // of the list first to avoid changing indexes.
1333 index_list.sort_unstable();
1334 index_list
1335 .iter()
1336 .rev()
1337 .for_each(|&index| self._remove_item(index, false, false, None, cx));
1338 }
1339
1340 pub(super) fn file_names_for_prompt(
1341 items: &mut dyn Iterator<Item = &Box<dyn ItemHandle>>,
1342 all_dirty_items: usize,
1343 cx: &AppContext,
1344 ) -> (String, String) {
1345 /// Quantity of item paths displayed in prompt prior to cutoff..
1346 const FILE_NAMES_CUTOFF_POINT: usize = 10;
1347 let mut file_names: Vec<_> = items
1348 .filter_map(|item| {
1349 item.project_path(cx).and_then(|project_path| {
1350 project_path
1351 .path
1352 .file_name()
1353 .and_then(|name| name.to_str().map(ToOwned::to_owned))
1354 })
1355 })
1356 .take(FILE_NAMES_CUTOFF_POINT)
1357 .collect();
1358 let should_display_followup_text =
1359 all_dirty_items > FILE_NAMES_CUTOFF_POINT || file_names.len() != all_dirty_items;
1360 if should_display_followup_text {
1361 let not_shown_files = all_dirty_items - file_names.len();
1362 if not_shown_files == 1 {
1363 file_names.push(".. 1 file not shown".into());
1364 } else {
1365 file_names.push(format!(".. {} files not shown", not_shown_files));
1366 }
1367 }
1368 (
1369 format!(
1370 "Do you want to save changes to the following {} files?",
1371 all_dirty_items
1372 ),
1373 file_names.join("\n"),
1374 )
1375 }
1376
1377 pub fn close_items(
1378 &mut self,
1379 cx: &mut ViewContext<Pane>,
1380 mut save_intent: SaveIntent,
1381 should_close: impl Fn(EntityId) -> bool,
1382 ) -> Task<Result<()>> {
1383 // Find the items to close.
1384 let mut items_to_close = Vec::new();
1385 let mut item_ids_to_close = HashSet::default();
1386 let mut dirty_items = Vec::new();
1387 for item in &self.items {
1388 if should_close(item.item_id()) {
1389 items_to_close.push(item.boxed_clone());
1390 item_ids_to_close.insert(item.item_id());
1391 if item.is_dirty(cx) {
1392 dirty_items.push(item.boxed_clone());
1393 }
1394 }
1395 }
1396
1397 let active_item_id = self.active_item().map(|item| item.item_id());
1398
1399 items_to_close.sort_by_key(|item| {
1400 // Put the currently active item at the end, because if the currently active item is not closed last
1401 // closing the currently active item will cause the focus to switch to another item
1402 // This will cause Zed to expand the content of the currently active item
1403 active_item_id.filter(|&id| id == item.item_id()).is_some()
1404 // If a buffer is open both in a singleton editor and in a multibuffer, make sure
1405 // to focus the singleton buffer when prompting to save that buffer, as opposed
1406 // to focusing the multibuffer, because this gives the user a more clear idea
1407 // of what content they would be saving.
1408 || !item.is_singleton(cx)
1409 });
1410
1411 let workspace = self.workspace.clone();
1412 cx.spawn(|pane, mut cx| async move {
1413 if save_intent == SaveIntent::Close && dirty_items.len() > 1 {
1414 let answer = pane.update(&mut cx, |_, cx| {
1415 let (prompt, detail) =
1416 Self::file_names_for_prompt(&mut dirty_items.iter(), dirty_items.len(), cx);
1417 cx.prompt(
1418 PromptLevel::Warning,
1419 &prompt,
1420 Some(&detail),
1421 &["Save all", "Discard all", "Cancel"],
1422 )
1423 })?;
1424 match answer.await {
1425 Ok(0) => save_intent = SaveIntent::SaveAll,
1426 Ok(1) => save_intent = SaveIntent::Skip,
1427 _ => {}
1428 }
1429 }
1430 let mut saved_project_items_ids = HashSet::default();
1431 for item_to_close in items_to_close {
1432 // Find the item's current index and its set of dirty project item models. Avoid
1433 // storing these in advance, in case they have changed since this task
1434 // was started.
1435 let mut dirty_project_item_ids = Vec::new();
1436 let Some(item_ix) = pane.update(&mut cx, |pane, cx| {
1437 item_to_close.for_each_project_item(
1438 cx,
1439 &mut |project_item_id, project_item| {
1440 if project_item.is_dirty() {
1441 dirty_project_item_ids.push(project_item_id);
1442 }
1443 },
1444 );
1445 pane.index_for_item(&*item_to_close)
1446 })?
1447 else {
1448 continue;
1449 };
1450
1451 // Check if this view has any project items that are not open anywhere else
1452 // in the workspace, AND that the user has not already been prompted to save.
1453 // If there are any such project entries, prompt the user to save this item.
1454 let project = workspace.update(&mut cx, |workspace, cx| {
1455 for open_item in workspace.items(cx) {
1456 let open_item_id = open_item.item_id();
1457 if !item_ids_to_close.contains(&open_item_id) {
1458 let other_project_item_ids = open_item.project_item_model_ids(cx);
1459 dirty_project_item_ids
1460 .retain(|id| !other_project_item_ids.contains(id));
1461 }
1462 }
1463 workspace.project().clone()
1464 })?;
1465 let should_save = dirty_project_item_ids
1466 .iter()
1467 .any(|id| saved_project_items_ids.insert(*id))
1468 // Always propose to save singleton files without any project paths: those cannot be saved via multibuffer, as require a file path selection modal.
1469 || cx
1470 .update(|cx| {
1471 item_to_close.can_save(cx) && item_to_close.is_dirty(cx)
1472 && item_to_close.is_singleton(cx)
1473 && item_to_close.project_path(cx).is_none()
1474 })
1475 .unwrap_or(false);
1476
1477 if should_save
1478 && !Self::save_item(
1479 project.clone(),
1480 &pane,
1481 item_ix,
1482 &*item_to_close,
1483 save_intent,
1484 &mut cx,
1485 )
1486 .await?
1487 {
1488 break;
1489 }
1490
1491 // Remove the item from the pane.
1492 pane.update(&mut cx, |pane, cx| {
1493 pane.remove_item(item_to_close.item_id(), false, true, cx);
1494 })
1495 .ok();
1496 }
1497
1498 pane.update(&mut cx, |_, cx| cx.notify()).ok();
1499 Ok(())
1500 })
1501 }
1502
1503 pub fn remove_item(
1504 &mut self,
1505 item_id: EntityId,
1506 activate_pane: bool,
1507 close_pane_if_empty: bool,
1508 cx: &mut ViewContext<Self>,
1509 ) {
1510 let Some(item_index) = self.index_for_item_id(item_id) else {
1511 return;
1512 };
1513 self._remove_item(item_index, activate_pane, close_pane_if_empty, None, cx)
1514 }
1515
1516 pub fn remove_item_and_focus_on_pane(
1517 &mut self,
1518 item_index: usize,
1519 activate_pane: bool,
1520 focus_on_pane_if_closed: View<Pane>,
1521 cx: &mut ViewContext<Self>,
1522 ) {
1523 self._remove_item(
1524 item_index,
1525 activate_pane,
1526 true,
1527 Some(focus_on_pane_if_closed),
1528 cx,
1529 )
1530 }
1531
1532 fn _remove_item(
1533 &mut self,
1534 item_index: usize,
1535 activate_pane: bool,
1536 close_pane_if_empty: bool,
1537 focus_on_pane_if_closed: Option<View<Pane>>,
1538 cx: &mut ViewContext<Self>,
1539 ) {
1540 let activate_on_close = &ItemSettings::get_global(cx).activate_on_close;
1541 self.activation_history
1542 .retain(|entry| entry.entity_id != self.items[item_index].item_id());
1543
1544 if self.is_tab_pinned(item_index) {
1545 self.pinned_tab_count -= 1;
1546 }
1547 if item_index == self.active_item_index {
1548 let left_neighbour_index = || item_index.min(self.items.len()).saturating_sub(1);
1549 let index_to_activate = match activate_on_close {
1550 ActivateOnClose::History => self
1551 .activation_history
1552 .pop()
1553 .and_then(|last_activated_item| {
1554 self.items.iter().enumerate().find_map(|(index, item)| {
1555 (item.item_id() == last_activated_item.entity_id).then_some(index)
1556 })
1557 })
1558 // We didn't have a valid activation history entry, so fallback
1559 // to activating the item to the left
1560 .unwrap_or_else(left_neighbour_index),
1561 ActivateOnClose::Neighbour => {
1562 self.activation_history.pop();
1563 if item_index + 1 < self.items.len() {
1564 item_index + 1
1565 } else {
1566 item_index.saturating_sub(1)
1567 }
1568 }
1569 ActivateOnClose::LeftNeighbour => {
1570 self.activation_history.pop();
1571 left_neighbour_index()
1572 }
1573 };
1574
1575 let should_activate = activate_pane || self.has_focus(cx);
1576 if self.items.len() == 1 && should_activate {
1577 self.focus_handle.focus(cx);
1578 } else {
1579 self.activate_item(index_to_activate, should_activate, should_activate, cx);
1580 }
1581 }
1582
1583 cx.emit(Event::RemoveItem { idx: item_index });
1584
1585 let item = self.items.remove(item_index);
1586
1587 cx.emit(Event::RemovedItem {
1588 item_id: item.item_id(),
1589 });
1590 if self.items.is_empty() {
1591 item.deactivated(cx);
1592 if close_pane_if_empty {
1593 self.update_toolbar(cx);
1594 cx.emit(Event::Remove {
1595 focus_on_pane: focus_on_pane_if_closed,
1596 });
1597 }
1598 }
1599
1600 if item_index < self.active_item_index {
1601 self.active_item_index -= 1;
1602 }
1603
1604 let mode = self.nav_history.mode();
1605 self.nav_history.set_mode(NavigationMode::ClosingItem);
1606 item.deactivated(cx);
1607 self.nav_history.set_mode(mode);
1608
1609 if self.is_active_preview_item(item.item_id()) {
1610 self.set_preview_item_id(None, cx);
1611 }
1612
1613 if let Some(path) = item.project_path(cx) {
1614 let abs_path = self
1615 .nav_history
1616 .0
1617 .lock()
1618 .paths_by_item
1619 .get(&item.item_id())
1620 .and_then(|(_, abs_path)| abs_path.clone());
1621
1622 self.nav_history
1623 .0
1624 .lock()
1625 .paths_by_item
1626 .insert(item.item_id(), (path, abs_path));
1627 } else {
1628 self.nav_history
1629 .0
1630 .lock()
1631 .paths_by_item
1632 .remove(&item.item_id());
1633 }
1634
1635 if self.zoom_out_on_close && self.items.is_empty() && close_pane_if_empty && self.zoomed {
1636 cx.emit(Event::ZoomOut);
1637 }
1638
1639 cx.notify();
1640 }
1641
1642 pub async fn save_item(
1643 project: Model<Project>,
1644 pane: &WeakView<Pane>,
1645 item_ix: usize,
1646 item: &dyn ItemHandle,
1647 save_intent: SaveIntent,
1648 cx: &mut AsyncWindowContext,
1649 ) -> Result<bool> {
1650 const CONFLICT_MESSAGE: &str =
1651 "This file has changed on disk since you started editing it. Do you want to overwrite it?";
1652
1653 const DELETED_MESSAGE: &str =
1654 "This file has been deleted on disk since you started editing it. Do you want to recreate it?";
1655
1656 if save_intent == SaveIntent::Skip {
1657 return Ok(true);
1658 }
1659
1660 let (mut has_conflict, mut is_dirty, mut can_save, is_singleton, has_deleted_file) = cx
1661 .update(|cx| {
1662 (
1663 item.has_conflict(cx),
1664 item.is_dirty(cx),
1665 item.can_save(cx),
1666 item.is_singleton(cx),
1667 item.has_deleted_file(cx),
1668 )
1669 })?;
1670
1671 let can_save_as = is_singleton;
1672
1673 // when saving a single buffer, we ignore whether or not it's dirty.
1674 if save_intent == SaveIntent::Save || save_intent == SaveIntent::SaveWithoutFormat {
1675 is_dirty = true;
1676 }
1677
1678 if save_intent == SaveIntent::SaveAs {
1679 is_dirty = true;
1680 has_conflict = false;
1681 can_save = false;
1682 }
1683
1684 if save_intent == SaveIntent::Overwrite {
1685 has_conflict = false;
1686 }
1687
1688 let should_format = save_intent != SaveIntent::SaveWithoutFormat;
1689
1690 if has_conflict && can_save {
1691 if has_deleted_file && is_singleton {
1692 let answer = pane.update(cx, |pane, cx| {
1693 pane.activate_item(item_ix, true, true, cx);
1694 cx.prompt(
1695 PromptLevel::Warning,
1696 DELETED_MESSAGE,
1697 None,
1698 &["Save", "Close", "Cancel"],
1699 )
1700 })?;
1701 match answer.await {
1702 Ok(0) => {
1703 pane.update(cx, |_, cx| item.save(should_format, project, cx))?
1704 .await?
1705 }
1706 Ok(1) => {
1707 pane.update(cx, |pane, cx| {
1708 pane.remove_item(item.item_id(), false, false, cx)
1709 })?;
1710 }
1711 _ => return Ok(false),
1712 }
1713 return Ok(true);
1714 } else {
1715 let answer = pane.update(cx, |pane, cx| {
1716 pane.activate_item(item_ix, true, true, cx);
1717 cx.prompt(
1718 PromptLevel::Warning,
1719 CONFLICT_MESSAGE,
1720 None,
1721 &["Overwrite", "Discard", "Cancel"],
1722 )
1723 })?;
1724 match answer.await {
1725 Ok(0) => {
1726 pane.update(cx, |_, cx| item.save(should_format, project, cx))?
1727 .await?
1728 }
1729 Ok(1) => pane.update(cx, |_, cx| item.reload(project, cx))?.await?,
1730 _ => return Ok(false),
1731 }
1732 }
1733 } else if is_dirty && (can_save || can_save_as) {
1734 if save_intent == SaveIntent::Close {
1735 let will_autosave = cx.update(|cx| {
1736 matches!(
1737 item.workspace_settings(cx).autosave,
1738 AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange
1739 ) && Self::can_autosave_item(item, cx)
1740 })?;
1741 if !will_autosave {
1742 let item_id = item.item_id();
1743 let answer_task = pane.update(cx, |pane, cx| {
1744 if pane.save_modals_spawned.insert(item_id) {
1745 pane.activate_item(item_ix, true, true, cx);
1746 let prompt = dirty_message_for(item.project_path(cx));
1747 Some(cx.prompt(
1748 PromptLevel::Warning,
1749 &prompt,
1750 None,
1751 &["Save", "Don't Save", "Cancel"],
1752 ))
1753 } else {
1754 None
1755 }
1756 })?;
1757 if let Some(answer_task) = answer_task {
1758 let answer = answer_task.await;
1759 pane.update(cx, |pane, _| {
1760 if !pane.save_modals_spawned.remove(&item_id) {
1761 debug_panic!(
1762 "save modal was not present in spawned modals after awaiting for its answer"
1763 )
1764 }
1765 })?;
1766 match answer {
1767 Ok(0) => {}
1768 Ok(1) => {
1769 // Don't save this file
1770 pane.update(cx, |pane, cx| {
1771 if pane.is_tab_pinned(item_ix) && !item.can_save(cx) {
1772 pane.pinned_tab_count -= 1;
1773 }
1774 item.discarded(project, cx)
1775 })
1776 .log_err();
1777 return Ok(true);
1778 }
1779 _ => return Ok(false), // Cancel
1780 }
1781 } else {
1782 return Ok(false);
1783 }
1784 }
1785 }
1786
1787 if can_save {
1788 pane.update(cx, |pane, cx| {
1789 if pane.is_active_preview_item(item.item_id()) {
1790 pane.set_preview_item_id(None, cx);
1791 }
1792 item.save(should_format, project, cx)
1793 })?
1794 .await?;
1795 } else if can_save_as {
1796 let abs_path = pane.update(cx, |pane, cx| {
1797 pane.workspace
1798 .update(cx, |workspace, cx| workspace.prompt_for_new_path(cx))
1799 })??;
1800 if let Some(abs_path) = abs_path.await.ok().flatten() {
1801 pane.update(cx, |pane, cx| {
1802 if let Some(item) = pane.item_for_path(abs_path.clone(), cx) {
1803 pane.remove_item(item.item_id(), false, false, cx);
1804 }
1805
1806 item.save_as(project, abs_path, cx)
1807 })?
1808 .await?;
1809 } else {
1810 return Ok(false);
1811 }
1812 }
1813 }
1814
1815 pane.update(cx, |_, cx| {
1816 cx.emit(Event::UserSavedItem {
1817 item: item.downgrade_item(),
1818 save_intent,
1819 });
1820 true
1821 })
1822 }
1823
1824 fn can_autosave_item(item: &dyn ItemHandle, cx: &AppContext) -> bool {
1825 let is_deleted = item.project_entry_ids(cx).is_empty();
1826 item.is_dirty(cx) && !item.has_conflict(cx) && item.can_save(cx) && !is_deleted
1827 }
1828
1829 pub fn autosave_item(
1830 item: &dyn ItemHandle,
1831 project: Model<Project>,
1832 cx: &mut WindowContext,
1833 ) -> Task<Result<()>> {
1834 let format = !matches!(
1835 item.workspace_settings(cx).autosave,
1836 AutosaveSetting::AfterDelay { .. }
1837 );
1838 if Self::can_autosave_item(item, cx) {
1839 item.save(format, project, cx)
1840 } else {
1841 Task::ready(Ok(()))
1842 }
1843 }
1844
1845 pub fn focus(&mut self, cx: &mut ViewContext<Pane>) {
1846 cx.focus(&self.focus_handle);
1847 }
1848
1849 pub fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
1850 if let Some(active_item) = self.active_item() {
1851 let focus_handle = active_item.focus_handle(cx);
1852 cx.focus(&focus_handle);
1853 }
1854 }
1855
1856 pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext<Self>) {
1857 cx.emit(Event::Split(direction));
1858 }
1859
1860 pub fn toolbar(&self) -> &View<Toolbar> {
1861 &self.toolbar
1862 }
1863
1864 pub fn handle_deleted_project_item(
1865 &mut self,
1866 entry_id: ProjectEntryId,
1867 cx: &mut ViewContext<Pane>,
1868 ) -> Option<()> {
1869 let item_id = self.items().find_map(|item| {
1870 if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
1871 Some(item.item_id())
1872 } else {
1873 None
1874 }
1875 })?;
1876
1877 self.remove_item(item_id, false, true, cx);
1878 self.nav_history.remove_item(item_id);
1879
1880 Some(())
1881 }
1882
1883 fn update_toolbar(&mut self, cx: &mut ViewContext<Self>) {
1884 let active_item = self
1885 .items
1886 .get(self.active_item_index)
1887 .map(|item| item.as_ref());
1888 self.toolbar.update(cx, |toolbar, cx| {
1889 toolbar.set_active_item(active_item, cx);
1890 });
1891 }
1892
1893 fn update_status_bar(&mut self, cx: &mut ViewContext<Self>) {
1894 let workspace = self.workspace.clone();
1895 let pane = cx.view().clone();
1896
1897 cx.window_context().defer(move |cx| {
1898 let Ok(status_bar) = workspace.update(cx, |workspace, _| workspace.status_bar.clone())
1899 else {
1900 return;
1901 };
1902
1903 status_bar.update(cx, move |status_bar, cx| {
1904 status_bar.set_active_pane(&pane, cx);
1905 });
1906 });
1907 }
1908
1909 fn entry_abs_path(&self, entry: ProjectEntryId, cx: &WindowContext) -> Option<PathBuf> {
1910 let worktree = self
1911 .workspace
1912 .upgrade()?
1913 .read(cx)
1914 .project()
1915 .read(cx)
1916 .worktree_for_entry(entry, cx)?
1917 .read(cx);
1918 let entry = worktree.entry_for_id(entry)?;
1919 match &entry.canonical_path {
1920 Some(canonical_path) => Some(canonical_path.to_path_buf()),
1921 None => worktree.absolutize(&entry.path).ok(),
1922 }
1923 }
1924
1925 pub fn icon_color(selected: bool) -> Color {
1926 if selected {
1927 Color::Default
1928 } else {
1929 Color::Muted
1930 }
1931 }
1932
1933 fn toggle_pin_tab(&mut self, _: &TogglePinTab, cx: &mut ViewContext<'_, Self>) {
1934 if self.items.is_empty() {
1935 return;
1936 }
1937 let active_tab_ix = self.active_item_index();
1938 if self.is_tab_pinned(active_tab_ix) {
1939 self.unpin_tab_at(active_tab_ix, cx);
1940 } else {
1941 self.pin_tab_at(active_tab_ix, cx);
1942 }
1943 }
1944
1945 fn pin_tab_at(&mut self, ix: usize, cx: &mut ViewContext<'_, Self>) {
1946 maybe!({
1947 let pane = cx.view().clone();
1948 let destination_index = self.pinned_tab_count.min(ix);
1949 self.pinned_tab_count += 1;
1950 let id = self.item_for_index(ix)?.item_id();
1951
1952 self.workspace
1953 .update(cx, |_, cx| {
1954 cx.defer(move |_, cx| move_item(&pane, &pane, id, destination_index, cx));
1955 })
1956 .ok()?;
1957
1958 Some(())
1959 });
1960 }
1961
1962 fn unpin_tab_at(&mut self, ix: usize, cx: &mut ViewContext<'_, Self>) {
1963 maybe!({
1964 let pane = cx.view().clone();
1965 self.pinned_tab_count = self.pinned_tab_count.checked_sub(1)?;
1966 let destination_index = self.pinned_tab_count;
1967
1968 let id = self.item_for_index(ix)?.item_id();
1969
1970 self.workspace
1971 .update(cx, |_, cx| {
1972 cx.defer(move |_, cx| move_item(&pane, &pane, id, destination_index, cx));
1973 })
1974 .ok()?;
1975
1976 Some(())
1977 });
1978 }
1979
1980 fn is_tab_pinned(&self, ix: usize) -> bool {
1981 self.pinned_tab_count > ix
1982 }
1983
1984 fn has_pinned_tabs(&self) -> bool {
1985 self.pinned_tab_count != 0
1986 }
1987
1988 fn render_tab(
1989 &self,
1990 ix: usize,
1991 item: &dyn ItemHandle,
1992 detail: usize,
1993 focus_handle: &FocusHandle,
1994 cx: &mut ViewContext<'_, Pane>,
1995 ) -> impl IntoElement {
1996 let is_active = ix == self.active_item_index;
1997 let is_preview = self
1998 .preview_item_id
1999 .map(|id| id == item.item_id())
2000 .unwrap_or(false);
2001
2002 let label = item.tab_content(
2003 TabContentParams {
2004 detail: Some(detail),
2005 selected: is_active,
2006 preview: is_preview,
2007 },
2008 cx,
2009 );
2010
2011 let item_diagnostic = item
2012 .project_path(cx)
2013 .map_or(None, |project_path| self.diagnostics.get(&project_path));
2014
2015 let decorated_icon = item_diagnostic.map_or(None, |diagnostic| {
2016 let icon = match item.tab_icon(cx) {
2017 Some(icon) => icon,
2018 None => return None,
2019 };
2020
2021 let knockout_item_color = if is_active {
2022 cx.theme().colors().tab_active_background
2023 } else {
2024 cx.theme().colors().tab_bar_background
2025 };
2026
2027 let (icon_decoration, icon_color) = if matches!(diagnostic, &DiagnosticSeverity::ERROR)
2028 {
2029 (IconDecorationKind::X, Color::Error)
2030 } else {
2031 (IconDecorationKind::Triangle, Color::Warning)
2032 };
2033
2034 Some(DecoratedIcon::new(
2035 icon.size(IconSize::Small).color(Color::Muted),
2036 Some(
2037 IconDecoration::new(icon_decoration, knockout_item_color, cx)
2038 .color(icon_color.color(cx))
2039 .position(Point {
2040 x: px(-2.),
2041 y: px(-2.),
2042 }),
2043 ),
2044 ))
2045 });
2046
2047 let icon = if decorated_icon.is_none() {
2048 match item_diagnostic {
2049 Some(&DiagnosticSeverity::ERROR) => None,
2050 Some(&DiagnosticSeverity::WARNING) => None,
2051 _ => item.tab_icon(cx).map(|icon| icon.color(Color::Muted)),
2052 }
2053 .map(|icon| icon.size(IconSize::Small))
2054 } else {
2055 None
2056 };
2057
2058 let settings = ItemSettings::get_global(cx);
2059 let close_side = &settings.close_position;
2060 let always_show_close_button = settings.always_show_close_button;
2061 let indicator = render_item_indicator(item.boxed_clone(), cx);
2062 let item_id = item.item_id();
2063 let is_first_item = ix == 0;
2064 let is_last_item = ix == self.items.len() - 1;
2065 let is_pinned = self.is_tab_pinned(ix);
2066 let position_relative_to_active_item = ix.cmp(&self.active_item_index);
2067
2068 let tab = Tab::new(ix)
2069 .position(if is_first_item {
2070 TabPosition::First
2071 } else if is_last_item {
2072 TabPosition::Last
2073 } else {
2074 TabPosition::Middle(position_relative_to_active_item)
2075 })
2076 .close_side(match close_side {
2077 ClosePosition::Left => ui::TabCloseSide::Start,
2078 ClosePosition::Right => ui::TabCloseSide::End,
2079 })
2080 .toggle_state(is_active)
2081 .on_click(
2082 cx.listener(move |pane: &mut Self, _, cx| pane.activate_item(ix, true, true, cx)),
2083 )
2084 // TODO: This should be a click listener with the middle mouse button instead of a mouse down listener.
2085 .on_mouse_down(
2086 MouseButton::Middle,
2087 cx.listener(move |pane, _event, cx| {
2088 pane.close_item_by_id(item_id, SaveIntent::Close, cx)
2089 .detach_and_log_err(cx);
2090 }),
2091 )
2092 .on_mouse_down(
2093 MouseButton::Left,
2094 cx.listener(move |pane, event: &MouseDownEvent, cx| {
2095 if let Some(id) = pane.preview_item_id {
2096 if id == item_id && event.click_count > 1 {
2097 pane.set_preview_item_id(None, cx);
2098 }
2099 }
2100 }),
2101 )
2102 .on_drag(
2103 DraggedTab {
2104 item: item.boxed_clone(),
2105 pane: cx.view().clone(),
2106 detail,
2107 is_active,
2108 ix,
2109 },
2110 |tab, _, cx| cx.new_view(|_| tab.clone()),
2111 )
2112 .drag_over::<DraggedTab>(|tab, _, cx| {
2113 tab.bg(cx.theme().colors().drop_target_background)
2114 })
2115 .drag_over::<DraggedSelection>(|tab, _, cx| {
2116 tab.bg(cx.theme().colors().drop_target_background)
2117 })
2118 .when_some(self.can_drop_predicate.clone(), |this, p| {
2119 this.can_drop(move |a, cx| p(a, cx))
2120 })
2121 .on_drop(cx.listener(move |this, dragged_tab: &DraggedTab, cx| {
2122 this.drag_split_direction = None;
2123 this.handle_tab_drop(dragged_tab, ix, cx)
2124 }))
2125 .on_drop(cx.listener(move |this, selection: &DraggedSelection, cx| {
2126 this.drag_split_direction = None;
2127 this.handle_dragged_selection_drop(selection, Some(ix), cx)
2128 }))
2129 .on_drop(cx.listener(move |this, paths, cx| {
2130 this.drag_split_direction = None;
2131 this.handle_external_paths_drop(paths, cx)
2132 }))
2133 .when_some(item.tab_tooltip_text(cx), |tab, text| {
2134 tab.tooltip(move |cx| Tooltip::text(text.clone(), cx))
2135 })
2136 .start_slot::<Indicator>(indicator)
2137 .map(|this| {
2138 let end_slot_action: &'static dyn Action;
2139 let end_slot_tooltip_text: &'static str;
2140 let end_slot = if is_pinned {
2141 end_slot_action = &TogglePinTab;
2142 end_slot_tooltip_text = "Unpin Tab";
2143 IconButton::new("unpin tab", IconName::Pin)
2144 .shape(IconButtonShape::Square)
2145 .icon_color(Color::Muted)
2146 .size(ButtonSize::None)
2147 .icon_size(IconSize::XSmall)
2148 .on_click(cx.listener(move |pane, _, cx| {
2149 pane.unpin_tab_at(ix, cx);
2150 }))
2151 } else {
2152 end_slot_action = &CloseActiveItem { save_intent: None };
2153 end_slot_tooltip_text = "Close Tab";
2154 IconButton::new("close tab", IconName::Close)
2155 .when(!always_show_close_button, |button| {
2156 button.visible_on_hover("")
2157 })
2158 .shape(IconButtonShape::Square)
2159 .icon_color(Color::Muted)
2160 .size(ButtonSize::None)
2161 .icon_size(IconSize::XSmall)
2162 .on_click(cx.listener(move |pane, _, cx| {
2163 pane.close_item_by_id(item_id, SaveIntent::Close, cx)
2164 .detach_and_log_err(cx);
2165 }))
2166 }
2167 .map(|this| {
2168 if is_active {
2169 let focus_handle = focus_handle.clone();
2170 this.tooltip(move |cx| {
2171 Tooltip::for_action_in(
2172 end_slot_tooltip_text,
2173 end_slot_action,
2174 &focus_handle,
2175 cx,
2176 )
2177 })
2178 } else {
2179 this.tooltip(move |cx| Tooltip::text(end_slot_tooltip_text, cx))
2180 }
2181 });
2182 this.end_slot(end_slot)
2183 })
2184 .child(
2185 h_flex()
2186 .gap_1()
2187 .items_center()
2188 .children(
2189 std::iter::once(if let Some(decorated_icon) = decorated_icon {
2190 Some(div().child(decorated_icon.into_any_element()))
2191 } else if let Some(icon) = icon {
2192 Some(div().child(icon.into_any_element()))
2193 } else {
2194 None
2195 })
2196 .flatten(),
2197 )
2198 .child(label),
2199 );
2200
2201 let single_entry_to_resolve = {
2202 let item_entries = self.items[ix].project_entry_ids(cx);
2203 if item_entries.len() == 1 {
2204 Some(item_entries[0])
2205 } else {
2206 None
2207 }
2208 };
2209
2210 let is_pinned = self.is_tab_pinned(ix);
2211 let pane = cx.view().downgrade();
2212 let menu_context = item.focus_handle(cx);
2213 right_click_menu(ix).trigger(tab).menu(move |cx| {
2214 let pane = pane.clone();
2215 let menu_context = menu_context.clone();
2216 ContextMenu::build(cx, move |mut menu, cx| {
2217 if let Some(pane) = pane.upgrade() {
2218 menu = menu
2219 .entry(
2220 "Close",
2221 Some(Box::new(CloseActiveItem { save_intent: None })),
2222 cx.handler_for(&pane, move |pane, cx| {
2223 pane.close_item_by_id(item_id, SaveIntent::Close, cx)
2224 .detach_and_log_err(cx);
2225 }),
2226 )
2227 .entry(
2228 "Close Others",
2229 Some(Box::new(CloseInactiveItems {
2230 save_intent: None,
2231 close_pinned: false,
2232 })),
2233 cx.handler_for(&pane, move |pane, cx| {
2234 pane.close_items(cx, SaveIntent::Close, |id| id != item_id)
2235 .detach_and_log_err(cx);
2236 }),
2237 )
2238 .separator()
2239 .entry(
2240 "Close Left",
2241 Some(Box::new(CloseItemsToTheLeft {
2242 close_pinned: false,
2243 })),
2244 cx.handler_for(&pane, move |pane, cx| {
2245 pane.close_items_to_the_left_by_id(
2246 item_id,
2247 pane.get_non_closeable_item_ids(false),
2248 cx,
2249 )
2250 .detach_and_log_err(cx);
2251 }),
2252 )
2253 .entry(
2254 "Close Right",
2255 Some(Box::new(CloseItemsToTheRight {
2256 close_pinned: false,
2257 })),
2258 cx.handler_for(&pane, move |pane, cx| {
2259 pane.close_items_to_the_right_by_id(
2260 item_id,
2261 pane.get_non_closeable_item_ids(false),
2262 cx,
2263 )
2264 .detach_and_log_err(cx);
2265 }),
2266 )
2267 .separator()
2268 .entry(
2269 "Close Clean",
2270 Some(Box::new(CloseCleanItems {
2271 close_pinned: false,
2272 })),
2273 cx.handler_for(&pane, move |pane, cx| {
2274 if let Some(task) = pane.close_clean_items(
2275 &CloseCleanItems {
2276 close_pinned: false,
2277 },
2278 cx,
2279 ) {
2280 task.detach_and_log_err(cx)
2281 }
2282 }),
2283 )
2284 .entry(
2285 "Close All",
2286 Some(Box::new(CloseAllItems {
2287 save_intent: None,
2288 close_pinned: false,
2289 })),
2290 cx.handler_for(&pane, |pane, cx| {
2291 if let Some(task) = pane.close_all_items(
2292 &CloseAllItems {
2293 save_intent: None,
2294 close_pinned: false,
2295 },
2296 cx,
2297 ) {
2298 task.detach_and_log_err(cx)
2299 }
2300 }),
2301 );
2302
2303 let pin_tab_entries = |menu: ContextMenu| {
2304 menu.separator().map(|this| {
2305 if is_pinned {
2306 this.entry(
2307 "Unpin Tab",
2308 Some(TogglePinTab.boxed_clone()),
2309 cx.handler_for(&pane, move |pane, cx| {
2310 pane.unpin_tab_at(ix, cx);
2311 }),
2312 )
2313 } else {
2314 this.entry(
2315 "Pin Tab",
2316 Some(TogglePinTab.boxed_clone()),
2317 cx.handler_for(&pane, move |pane, cx| {
2318 pane.pin_tab_at(ix, cx);
2319 }),
2320 )
2321 }
2322 })
2323 };
2324 if let Some(entry) = single_entry_to_resolve {
2325 let entry_abs_path = pane.read(cx).entry_abs_path(entry, cx);
2326 let parent_abs_path = entry_abs_path
2327 .as_deref()
2328 .and_then(|abs_path| Some(abs_path.parent()?.to_path_buf()));
2329 let relative_path = pane
2330 .read(cx)
2331 .item_for_entry(entry, cx)
2332 .and_then(|item| item.project_path(cx))
2333 .map(|project_path| project_path.path);
2334
2335 let entry_id = entry.to_proto();
2336 menu = menu
2337 .separator()
2338 .when_some(entry_abs_path, |menu, abs_path| {
2339 menu.entry(
2340 "Copy Path",
2341 Some(Box::new(CopyPath)),
2342 cx.handler_for(&pane, move |_, cx| {
2343 cx.write_to_clipboard(ClipboardItem::new_string(
2344 abs_path.to_string_lossy().to_string(),
2345 ));
2346 }),
2347 )
2348 })
2349 .when_some(relative_path, |menu, relative_path| {
2350 menu.entry(
2351 "Copy Relative Path",
2352 Some(Box::new(CopyRelativePath)),
2353 cx.handler_for(&pane, move |_, cx| {
2354 cx.write_to_clipboard(ClipboardItem::new_string(
2355 relative_path.to_string_lossy().to_string(),
2356 ));
2357 }),
2358 )
2359 })
2360 .map(pin_tab_entries)
2361 .separator()
2362 .entry(
2363 "Reveal In Project Panel",
2364 Some(Box::new(RevealInProjectPanel {
2365 entry_id: Some(entry_id),
2366 })),
2367 cx.handler_for(&pane, move |pane, cx| {
2368 pane.project.update(cx, |_, cx| {
2369 cx.emit(project::Event::RevealInProjectPanel(
2370 ProjectEntryId::from_proto(entry_id),
2371 ))
2372 });
2373 }),
2374 )
2375 .when_some(parent_abs_path, |menu, parent_abs_path| {
2376 menu.entry(
2377 "Open in Terminal",
2378 Some(Box::new(OpenInTerminal)),
2379 cx.handler_for(&pane, move |_, cx| {
2380 cx.dispatch_action(
2381 OpenTerminal {
2382 working_directory: parent_abs_path.clone(),
2383 }
2384 .boxed_clone(),
2385 );
2386 }),
2387 )
2388 });
2389 } else {
2390 menu = menu.map(pin_tab_entries);
2391 }
2392 }
2393
2394 menu.context(menu_context)
2395 })
2396 })
2397 }
2398
2399 fn render_tab_bar(&mut self, cx: &mut ViewContext<'_, Pane>) -> impl IntoElement {
2400 let focus_handle = self.focus_handle.clone();
2401 let navigate_backward = IconButton::new("navigate_backward", IconName::ArrowLeft)
2402 .icon_size(IconSize::Small)
2403 .on_click({
2404 let view = cx.view().clone();
2405 move |_, cx| view.update(cx, Self::navigate_backward)
2406 })
2407 .disabled(!self.can_navigate_backward())
2408 .tooltip({
2409 let focus_handle = focus_handle.clone();
2410 move |cx| Tooltip::for_action_in("Go Back", &GoBack, &focus_handle, cx)
2411 });
2412
2413 let navigate_forward = IconButton::new("navigate_forward", IconName::ArrowRight)
2414 .icon_size(IconSize::Small)
2415 .on_click({
2416 let view = cx.view().clone();
2417 move |_, cx| view.update(cx, Self::navigate_forward)
2418 })
2419 .disabled(!self.can_navigate_forward())
2420 .tooltip({
2421 let focus_handle = focus_handle.clone();
2422 move |cx| Tooltip::for_action_in("Go Forward", &GoForward, &focus_handle, cx)
2423 });
2424
2425 let mut tab_items = self
2426 .items
2427 .iter()
2428 .enumerate()
2429 .zip(tab_details(&self.items, cx))
2430 .map(|((ix, item), detail)| self.render_tab(ix, &**item, detail, &focus_handle, cx))
2431 .collect::<Vec<_>>();
2432 let tab_count = tab_items.len();
2433 let unpinned_tabs = tab_items.split_off(self.pinned_tab_count);
2434 let pinned_tabs = tab_items;
2435 TabBar::new("tab_bar")
2436 .when(
2437 self.display_nav_history_buttons.unwrap_or_default(),
2438 |tab_bar| {
2439 tab_bar
2440 .start_child(navigate_backward)
2441 .start_child(navigate_forward)
2442 },
2443 )
2444 .map(|tab_bar| {
2445 let render_tab_buttons = self.render_tab_bar_buttons.clone();
2446 let (left_children, right_children) = render_tab_buttons(self, cx);
2447
2448 tab_bar
2449 .start_children(left_children)
2450 .end_children(right_children)
2451 })
2452 .children(pinned_tabs.len().ne(&0).then(|| {
2453 h_flex()
2454 .children(pinned_tabs)
2455 .border_r_2()
2456 .border_color(cx.theme().colors().border)
2457 }))
2458 .child(
2459 h_flex()
2460 .id("unpinned tabs")
2461 .overflow_x_scroll()
2462 .w_full()
2463 .track_scroll(&self.tab_bar_scroll_handle)
2464 .children(unpinned_tabs)
2465 .child(
2466 div()
2467 .id("tab_bar_drop_target")
2468 .min_w_6()
2469 // HACK: This empty child is currently necessary to force the drop target to appear
2470 // despite us setting a min width above.
2471 .child("")
2472 .h_full()
2473 .flex_grow()
2474 .drag_over::<DraggedTab>(|bar, _, cx| {
2475 bar.bg(cx.theme().colors().drop_target_background)
2476 })
2477 .drag_over::<DraggedSelection>(|bar, _, cx| {
2478 bar.bg(cx.theme().colors().drop_target_background)
2479 })
2480 .on_drop(cx.listener(move |this, dragged_tab: &DraggedTab, cx| {
2481 this.drag_split_direction = None;
2482 this.handle_tab_drop(dragged_tab, this.items.len(), cx)
2483 }))
2484 .on_drop(cx.listener(move |this, selection: &DraggedSelection, cx| {
2485 this.drag_split_direction = None;
2486 this.handle_project_entry_drop(
2487 &selection.active_selection.entry_id,
2488 Some(tab_count),
2489 cx,
2490 )
2491 }))
2492 .on_drop(cx.listener(move |this, paths, cx| {
2493 this.drag_split_direction = None;
2494 this.handle_external_paths_drop(paths, cx)
2495 }))
2496 .on_click(cx.listener(move |this, event: &ClickEvent, cx| {
2497 if event.up.click_count == 2 {
2498 cx.dispatch_action(
2499 this.double_click_dispatch_action.boxed_clone(),
2500 )
2501 }
2502 })),
2503 ),
2504 )
2505 }
2506
2507 pub fn render_menu_overlay(menu: &View<ContextMenu>) -> Div {
2508 div().absolute().bottom_0().right_0().size_0().child(
2509 deferred(anchored().anchor(Corner::TopRight).child(menu.clone())).with_priority(1),
2510 )
2511 }
2512
2513 pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
2514 self.zoomed = zoomed;
2515 cx.notify();
2516 }
2517
2518 pub fn is_zoomed(&self) -> bool {
2519 self.zoomed
2520 }
2521
2522 fn handle_drag_move<T: 'static>(
2523 &mut self,
2524 event: &DragMoveEvent<T>,
2525 cx: &mut ViewContext<Self>,
2526 ) {
2527 let can_split_predicate = self.can_split_predicate.take();
2528 let can_split = match &can_split_predicate {
2529 Some(can_split_predicate) => can_split_predicate(self, event.dragged_item(), cx),
2530 None => false,
2531 };
2532 self.can_split_predicate = can_split_predicate;
2533 if !can_split {
2534 return;
2535 }
2536
2537 let rect = event.bounds.size;
2538
2539 let size = event.bounds.size.width.min(event.bounds.size.height)
2540 * WorkspaceSettings::get_global(cx).drop_target_size;
2541
2542 let relative_cursor = Point::new(
2543 event.event.position.x - event.bounds.left(),
2544 event.event.position.y - event.bounds.top(),
2545 );
2546
2547 let direction = if relative_cursor.x < size
2548 || relative_cursor.x > rect.width - size
2549 || relative_cursor.y < size
2550 || relative_cursor.y > rect.height - size
2551 {
2552 [
2553 SplitDirection::Up,
2554 SplitDirection::Right,
2555 SplitDirection::Down,
2556 SplitDirection::Left,
2557 ]
2558 .iter()
2559 .min_by_key(|side| match side {
2560 SplitDirection::Up => relative_cursor.y,
2561 SplitDirection::Right => rect.width - relative_cursor.x,
2562 SplitDirection::Down => rect.height - relative_cursor.y,
2563 SplitDirection::Left => relative_cursor.x,
2564 })
2565 .cloned()
2566 } else {
2567 None
2568 };
2569
2570 if direction != self.drag_split_direction {
2571 self.drag_split_direction = direction;
2572 }
2573 }
2574
2575 fn handle_tab_drop(
2576 &mut self,
2577 dragged_tab: &DraggedTab,
2578 ix: usize,
2579 cx: &mut ViewContext<'_, Self>,
2580 ) {
2581 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2582 if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_tab, cx) {
2583 return;
2584 }
2585 }
2586 let mut to_pane = cx.view().clone();
2587 let split_direction = self.drag_split_direction;
2588 let item_id = dragged_tab.item.item_id();
2589 if let Some(preview_item_id) = self.preview_item_id {
2590 if item_id == preview_item_id {
2591 self.set_preview_item_id(None, cx);
2592 }
2593 }
2594
2595 let from_pane = dragged_tab.pane.clone();
2596 self.workspace
2597 .update(cx, |_, cx| {
2598 cx.defer(move |workspace, cx| {
2599 if let Some(split_direction) = split_direction {
2600 to_pane = workspace.split_pane(to_pane, split_direction, cx);
2601 }
2602 let old_ix = from_pane.read(cx).index_for_item_id(item_id);
2603 let old_len = to_pane.read(cx).items.len();
2604 move_item(&from_pane, &to_pane, item_id, ix, cx);
2605 if to_pane == from_pane {
2606 if let Some(old_index) = old_ix {
2607 to_pane.update(cx, |this, _| {
2608 if old_index < this.pinned_tab_count
2609 && (ix == this.items.len() || ix > this.pinned_tab_count)
2610 {
2611 this.pinned_tab_count -= 1;
2612 } else if this.has_pinned_tabs()
2613 && old_index >= this.pinned_tab_count
2614 && ix < this.pinned_tab_count
2615 {
2616 this.pinned_tab_count += 1;
2617 }
2618 });
2619 }
2620 } else {
2621 to_pane.update(cx, |this, _| {
2622 if this.items.len() > old_len // Did we not deduplicate on drag?
2623 && this.has_pinned_tabs()
2624 && ix < this.pinned_tab_count
2625 {
2626 this.pinned_tab_count += 1;
2627 }
2628 });
2629 from_pane.update(cx, |this, _| {
2630 if let Some(index) = old_ix {
2631 if this.pinned_tab_count > index {
2632 this.pinned_tab_count -= 1;
2633 }
2634 }
2635 })
2636 }
2637 });
2638 })
2639 .log_err();
2640 }
2641
2642 fn handle_dragged_selection_drop(
2643 &mut self,
2644 dragged_selection: &DraggedSelection,
2645 dragged_onto: Option<usize>,
2646 cx: &mut ViewContext<'_, Self>,
2647 ) {
2648 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2649 if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_selection, cx) {
2650 return;
2651 }
2652 }
2653 self.handle_project_entry_drop(
2654 &dragged_selection.active_selection.entry_id,
2655 dragged_onto,
2656 cx,
2657 );
2658 }
2659
2660 fn handle_project_entry_drop(
2661 &mut self,
2662 project_entry_id: &ProjectEntryId,
2663 target: Option<usize>,
2664 cx: &mut ViewContext<'_, Self>,
2665 ) {
2666 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2667 if let ControlFlow::Break(()) = custom_drop_handle(self, project_entry_id, cx) {
2668 return;
2669 }
2670 }
2671 let mut to_pane = cx.view().clone();
2672 let split_direction = self.drag_split_direction;
2673 let project_entry_id = *project_entry_id;
2674 self.workspace
2675 .update(cx, |_, cx| {
2676 cx.defer(move |workspace, cx| {
2677 if let Some(path) = workspace
2678 .project()
2679 .read(cx)
2680 .path_for_entry(project_entry_id, cx)
2681 {
2682 let load_path_task = workspace.load_path(path, cx);
2683 cx.spawn(|workspace, mut cx| async move {
2684 if let Some((project_entry_id, build_item)) =
2685 load_path_task.await.notify_async_err(&mut cx)
2686 {
2687 let (to_pane, new_item_handle) = workspace
2688 .update(&mut cx, |workspace, cx| {
2689 if let Some(split_direction) = split_direction {
2690 to_pane =
2691 workspace.split_pane(to_pane, split_direction, cx);
2692 }
2693 let new_item_handle = to_pane.update(cx, |pane, cx| {
2694 pane.open_item(
2695 project_entry_id,
2696 true,
2697 false,
2698 target,
2699 cx,
2700 build_item,
2701 )
2702 });
2703 (to_pane, new_item_handle)
2704 })
2705 .log_err()?;
2706 to_pane
2707 .update(&mut cx, |this, cx| {
2708 let Some(index) = this.index_for_item(&*new_item_handle)
2709 else {
2710 return;
2711 };
2712
2713 if target.map_or(false, |target| this.is_tab_pinned(target))
2714 {
2715 this.pin_tab_at(index, cx);
2716 }
2717 })
2718 .ok()?
2719 }
2720 Some(())
2721 })
2722 .detach();
2723 };
2724 });
2725 })
2726 .log_err();
2727 }
2728
2729 fn handle_external_paths_drop(
2730 &mut self,
2731 paths: &ExternalPaths,
2732 cx: &mut ViewContext<'_, Self>,
2733 ) {
2734 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2735 if let ControlFlow::Break(()) = custom_drop_handle(self, paths, cx) {
2736 return;
2737 }
2738 }
2739 let mut to_pane = cx.view().clone();
2740 let mut split_direction = self.drag_split_direction;
2741 let paths = paths.paths().to_vec();
2742 let is_remote = self
2743 .workspace
2744 .update(cx, |workspace, cx| {
2745 if workspace.project().read(cx).is_via_collab() {
2746 workspace.show_error(
2747 &anyhow::anyhow!("Cannot drop files on a remote project"),
2748 cx,
2749 );
2750 true
2751 } else {
2752 false
2753 }
2754 })
2755 .unwrap_or(true);
2756 if is_remote {
2757 return;
2758 }
2759
2760 self.workspace
2761 .update(cx, |workspace, cx| {
2762 let fs = Arc::clone(workspace.project().read(cx).fs());
2763 cx.spawn(|workspace, mut cx| async move {
2764 let mut is_file_checks = FuturesUnordered::new();
2765 for path in &paths {
2766 is_file_checks.push(fs.is_file(path))
2767 }
2768 let mut has_files_to_open = false;
2769 while let Some(is_file) = is_file_checks.next().await {
2770 if is_file {
2771 has_files_to_open = true;
2772 break;
2773 }
2774 }
2775 drop(is_file_checks);
2776 if !has_files_to_open {
2777 split_direction = None;
2778 }
2779
2780 if let Ok(open_task) = workspace.update(&mut cx, |workspace, cx| {
2781 if let Some(split_direction) = split_direction {
2782 to_pane = workspace.split_pane(to_pane, split_direction, cx);
2783 }
2784 workspace.open_paths(
2785 paths,
2786 OpenVisible::OnlyDirectories,
2787 Some(to_pane.downgrade()),
2788 cx,
2789 )
2790 }) {
2791 let opened_items: Vec<_> = open_task.await;
2792 _ = workspace.update(&mut cx, |workspace, cx| {
2793 for item in opened_items.into_iter().flatten() {
2794 if let Err(e) = item {
2795 workspace.show_error(&e, cx);
2796 }
2797 }
2798 });
2799 }
2800 })
2801 .detach();
2802 })
2803 .log_err();
2804 }
2805
2806 pub fn display_nav_history_buttons(&mut self, display: Option<bool>) {
2807 self.display_nav_history_buttons = display;
2808 }
2809
2810 fn get_non_closeable_item_ids(&self, close_pinned: bool) -> Vec<EntityId> {
2811 if close_pinned {
2812 return vec![];
2813 }
2814
2815 self.items
2816 .iter()
2817 .map(|item| item.item_id())
2818 .filter(|item_id| {
2819 if let Some(ix) = self.index_for_item_id(*item_id) {
2820 self.is_tab_pinned(ix)
2821 } else {
2822 true
2823 }
2824 })
2825 .collect()
2826 }
2827
2828 pub fn drag_split_direction(&self) -> Option<SplitDirection> {
2829 self.drag_split_direction
2830 }
2831
2832 pub fn set_zoom_out_on_close(&mut self, zoom_out_on_close: bool) {
2833 self.zoom_out_on_close = zoom_out_on_close;
2834 }
2835}
2836
2837impl FocusableView for Pane {
2838 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
2839 self.focus_handle.clone()
2840 }
2841}
2842
2843impl Render for Pane {
2844 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2845 let mut key_context = KeyContext::new_with_defaults();
2846 key_context.add("Pane");
2847 if self.active_item().is_none() {
2848 key_context.add("EmptyPane");
2849 }
2850
2851 let should_display_tab_bar = self.should_display_tab_bar.clone();
2852 let display_tab_bar = should_display_tab_bar(cx);
2853 let is_local = self.project.read(cx).is_local();
2854
2855 v_flex()
2856 .key_context(key_context)
2857 .track_focus(&self.focus_handle(cx))
2858 .size_full()
2859 .flex_none()
2860 .overflow_hidden()
2861 .on_action(cx.listener(|pane, _: &AlternateFile, cx| {
2862 pane.alternate_file(cx);
2863 }))
2864 .on_action(cx.listener(|pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx)))
2865 .on_action(cx.listener(|pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx)))
2866 .on_action(cx.listener(|pane, _: &SplitHorizontal, cx| {
2867 pane.split(SplitDirection::horizontal(cx), cx)
2868 }))
2869 .on_action(cx.listener(|pane, _: &SplitVertical, cx| {
2870 pane.split(SplitDirection::vertical(cx), cx)
2871 }))
2872 .on_action(
2873 cx.listener(|pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx)),
2874 )
2875 .on_action(cx.listener(|pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx)))
2876 .on_action(cx.listener(|pane, _: &GoBack, cx| pane.navigate_backward(cx)))
2877 .on_action(cx.listener(|pane, _: &GoForward, cx| pane.navigate_forward(cx)))
2878 .on_action(cx.listener(|pane, _: &JoinIntoNext, cx| pane.join_into_next(cx)))
2879 .on_action(cx.listener(|pane, _: &JoinAll, cx| pane.join_all(cx)))
2880 .on_action(cx.listener(Pane::toggle_zoom))
2881 .on_action(cx.listener(|pane: &mut Pane, action: &ActivateItem, cx| {
2882 pane.activate_item(action.0, true, true, cx);
2883 }))
2884 .on_action(cx.listener(|pane: &mut Pane, _: &ActivateLastItem, cx| {
2885 pane.activate_item(pane.items.len() - 1, true, true, cx);
2886 }))
2887 .on_action(cx.listener(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
2888 pane.activate_prev_item(true, cx);
2889 }))
2890 .on_action(cx.listener(|pane: &mut Pane, _: &ActivateNextItem, cx| {
2891 pane.activate_next_item(true, cx);
2892 }))
2893 .on_action(cx.listener(|pane, _: &SwapItemLeft, cx| pane.swap_item_left(cx)))
2894 .on_action(cx.listener(|pane, _: &SwapItemRight, cx| pane.swap_item_right(cx)))
2895 .on_action(cx.listener(|pane, action, cx| {
2896 pane.toggle_pin_tab(action, cx);
2897 }))
2898 .when(PreviewTabsSettings::get_global(cx).enabled, |this| {
2899 this.on_action(cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, cx| {
2900 if let Some(active_item_id) = pane.active_item().map(|i| i.item_id()) {
2901 if pane.is_active_preview_item(active_item_id) {
2902 pane.set_preview_item_id(None, cx);
2903 } else {
2904 pane.set_preview_item_id(Some(active_item_id), cx);
2905 }
2906 }
2907 }))
2908 })
2909 .on_action(
2910 cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
2911 if let Some(task) = pane.close_active_item(action, cx) {
2912 task.detach_and_log_err(cx)
2913 }
2914 }),
2915 )
2916 .on_action(
2917 cx.listener(|pane: &mut Self, action: &CloseInactiveItems, cx| {
2918 if let Some(task) = pane.close_inactive_items(action, cx) {
2919 task.detach_and_log_err(cx)
2920 }
2921 }),
2922 )
2923 .on_action(
2924 cx.listener(|pane: &mut Self, action: &CloseCleanItems, cx| {
2925 if let Some(task) = pane.close_clean_items(action, cx) {
2926 task.detach_and_log_err(cx)
2927 }
2928 }),
2929 )
2930 .on_action(
2931 cx.listener(|pane: &mut Self, action: &CloseItemsToTheLeft, cx| {
2932 if let Some(task) = pane.close_items_to_the_left(action, cx) {
2933 task.detach_and_log_err(cx)
2934 }
2935 }),
2936 )
2937 .on_action(
2938 cx.listener(|pane: &mut Self, action: &CloseItemsToTheRight, cx| {
2939 if let Some(task) = pane.close_items_to_the_right(action, cx) {
2940 task.detach_and_log_err(cx)
2941 }
2942 }),
2943 )
2944 .on_action(cx.listener(|pane: &mut Self, action: &CloseAllItems, cx| {
2945 if let Some(task) = pane.close_all_items(action, cx) {
2946 task.detach_and_log_err(cx)
2947 }
2948 }))
2949 .on_action(
2950 cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
2951 if let Some(task) = pane.close_active_item(action, cx) {
2952 task.detach_and_log_err(cx)
2953 }
2954 }),
2955 )
2956 .on_action(
2957 cx.listener(|pane: &mut Self, action: &RevealInProjectPanel, cx| {
2958 let entry_id = action
2959 .entry_id
2960 .map(ProjectEntryId::from_proto)
2961 .or_else(|| pane.active_item()?.project_entry_ids(cx).first().copied());
2962 if let Some(entry_id) = entry_id {
2963 pane.project.update(cx, |_, cx| {
2964 cx.emit(project::Event::RevealInProjectPanel(entry_id))
2965 });
2966 }
2967 }),
2968 )
2969 .when(self.active_item().is_some() && display_tab_bar, |pane| {
2970 pane.child(self.render_tab_bar(cx))
2971 })
2972 .child({
2973 let has_worktrees = self.project.read(cx).worktrees(cx).next().is_some();
2974 // main content
2975 div()
2976 .flex_1()
2977 .relative()
2978 .group("")
2979 .overflow_hidden()
2980 .on_drag_move::<DraggedTab>(cx.listener(Self::handle_drag_move))
2981 .on_drag_move::<DraggedSelection>(cx.listener(Self::handle_drag_move))
2982 .when(is_local, |div| {
2983 div.on_drag_move::<ExternalPaths>(cx.listener(Self::handle_drag_move))
2984 })
2985 .map(|div| {
2986 if let Some(item) = self.active_item() {
2987 div.v_flex()
2988 .size_full()
2989 .overflow_hidden()
2990 .child(self.toolbar.clone())
2991 .child(item.to_any())
2992 } else {
2993 let placeholder = div.h_flex().size_full().justify_center();
2994 if has_worktrees {
2995 placeholder
2996 } else {
2997 placeholder.child(
2998 Label::new("Open a file or project to get started.")
2999 .color(Color::Muted),
3000 )
3001 }
3002 }
3003 })
3004 .child(
3005 // drag target
3006 div()
3007 .invisible()
3008 .absolute()
3009 .bg(cx.theme().colors().drop_target_background)
3010 .group_drag_over::<DraggedTab>("", |style| style.visible())
3011 .group_drag_over::<DraggedSelection>("", |style| style.visible())
3012 .when(is_local, |div| {
3013 div.group_drag_over::<ExternalPaths>("", |style| style.visible())
3014 })
3015 .when_some(self.can_drop_predicate.clone(), |this, p| {
3016 this.can_drop(move |a, cx| p(a, cx))
3017 })
3018 .on_drop(cx.listener(move |this, dragged_tab, cx| {
3019 this.handle_tab_drop(dragged_tab, this.active_item_index(), cx)
3020 }))
3021 .on_drop(cx.listener(move |this, selection: &DraggedSelection, cx| {
3022 this.handle_dragged_selection_drop(selection, None, cx)
3023 }))
3024 .on_drop(cx.listener(move |this, paths, cx| {
3025 this.handle_external_paths_drop(paths, cx)
3026 }))
3027 .map(|div| {
3028 let size = DefiniteLength::Fraction(0.5);
3029 match self.drag_split_direction {
3030 None => div.top_0().right_0().bottom_0().left_0(),
3031 Some(SplitDirection::Up) => {
3032 div.top_0().left_0().right_0().h(size)
3033 }
3034 Some(SplitDirection::Down) => {
3035 div.left_0().bottom_0().right_0().h(size)
3036 }
3037 Some(SplitDirection::Left) => {
3038 div.top_0().left_0().bottom_0().w(size)
3039 }
3040 Some(SplitDirection::Right) => {
3041 div.top_0().bottom_0().right_0().w(size)
3042 }
3043 }
3044 }),
3045 )
3046 })
3047 .on_mouse_down(
3048 MouseButton::Navigate(NavigationDirection::Back),
3049 cx.listener(|pane, _, cx| {
3050 if let Some(workspace) = pane.workspace.upgrade() {
3051 let pane = cx.view().downgrade();
3052 cx.window_context().defer(move |cx| {
3053 workspace.update(cx, |workspace, cx| {
3054 workspace.go_back(pane, cx).detach_and_log_err(cx)
3055 })
3056 })
3057 }
3058 }),
3059 )
3060 .on_mouse_down(
3061 MouseButton::Navigate(NavigationDirection::Forward),
3062 cx.listener(|pane, _, cx| {
3063 if let Some(workspace) = pane.workspace.upgrade() {
3064 let pane = cx.view().downgrade();
3065 cx.window_context().defer(move |cx| {
3066 workspace.update(cx, |workspace, cx| {
3067 workspace.go_forward(pane, cx).detach_and_log_err(cx)
3068 })
3069 })
3070 }
3071 }),
3072 )
3073 }
3074}
3075
3076impl ItemNavHistory {
3077 pub fn push<D: 'static + Send + Any>(&mut self, data: Option<D>, cx: &mut WindowContext) {
3078 self.history
3079 .push(data, self.item.clone(), self.is_preview, cx);
3080 }
3081
3082 pub fn pop_backward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
3083 self.history.pop(NavigationMode::GoingBack, cx)
3084 }
3085
3086 pub fn pop_forward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
3087 self.history.pop(NavigationMode::GoingForward, cx)
3088 }
3089}
3090
3091impl NavHistory {
3092 pub fn for_each_entry(
3093 &self,
3094 cx: &AppContext,
3095 mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
3096 ) {
3097 let borrowed_history = self.0.lock();
3098 borrowed_history
3099 .forward_stack
3100 .iter()
3101 .chain(borrowed_history.backward_stack.iter())
3102 .chain(borrowed_history.closed_stack.iter())
3103 .for_each(|entry| {
3104 if let Some(project_and_abs_path) =
3105 borrowed_history.paths_by_item.get(&entry.item.id())
3106 {
3107 f(entry, project_and_abs_path.clone());
3108 } else if let Some(item) = entry.item.upgrade() {
3109 if let Some(path) = item.project_path(cx) {
3110 f(entry, (path, None));
3111 }
3112 }
3113 })
3114 }
3115
3116 pub fn set_mode(&mut self, mode: NavigationMode) {
3117 self.0.lock().mode = mode;
3118 }
3119
3120 pub fn mode(&self) -> NavigationMode {
3121 self.0.lock().mode
3122 }
3123
3124 pub fn disable(&mut self) {
3125 self.0.lock().mode = NavigationMode::Disabled;
3126 }
3127
3128 pub fn enable(&mut self) {
3129 self.0.lock().mode = NavigationMode::Normal;
3130 }
3131
3132 pub fn pop(&mut self, mode: NavigationMode, cx: &mut WindowContext) -> Option<NavigationEntry> {
3133 let mut state = self.0.lock();
3134 let entry = match mode {
3135 NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
3136 return None
3137 }
3138 NavigationMode::GoingBack => &mut state.backward_stack,
3139 NavigationMode::GoingForward => &mut state.forward_stack,
3140 NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
3141 }
3142 .pop_back();
3143 if entry.is_some() {
3144 state.did_update(cx);
3145 }
3146 entry
3147 }
3148
3149 pub fn push<D: 'static + Send + Any>(
3150 &mut self,
3151 data: Option<D>,
3152 item: Arc<dyn WeakItemHandle>,
3153 is_preview: bool,
3154 cx: &mut WindowContext,
3155 ) {
3156 let state = &mut *self.0.lock();
3157 match state.mode {
3158 NavigationMode::Disabled => {}
3159 NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
3160 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3161 state.backward_stack.pop_front();
3162 }
3163 state.backward_stack.push_back(NavigationEntry {
3164 item,
3165 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3166 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3167 is_preview,
3168 });
3169 state.forward_stack.clear();
3170 }
3171 NavigationMode::GoingBack => {
3172 if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3173 state.forward_stack.pop_front();
3174 }
3175 state.forward_stack.push_back(NavigationEntry {
3176 item,
3177 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3178 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3179 is_preview,
3180 });
3181 }
3182 NavigationMode::GoingForward => {
3183 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3184 state.backward_stack.pop_front();
3185 }
3186 state.backward_stack.push_back(NavigationEntry {
3187 item,
3188 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3189 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3190 is_preview,
3191 });
3192 }
3193 NavigationMode::ClosingItem => {
3194 if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3195 state.closed_stack.pop_front();
3196 }
3197 state.closed_stack.push_back(NavigationEntry {
3198 item,
3199 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3200 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3201 is_preview,
3202 });
3203 }
3204 }
3205 state.did_update(cx);
3206 }
3207
3208 pub fn remove_item(&mut self, item_id: EntityId) {
3209 let mut state = self.0.lock();
3210 state.paths_by_item.remove(&item_id);
3211 state
3212 .backward_stack
3213 .retain(|entry| entry.item.id() != item_id);
3214 state
3215 .forward_stack
3216 .retain(|entry| entry.item.id() != item_id);
3217 state
3218 .closed_stack
3219 .retain(|entry| entry.item.id() != item_id);
3220 }
3221
3222 pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option<PathBuf>)> {
3223 self.0.lock().paths_by_item.get(&item_id).cloned()
3224 }
3225}
3226
3227impl NavHistoryState {
3228 pub fn did_update(&self, cx: &mut WindowContext) {
3229 if let Some(pane) = self.pane.upgrade() {
3230 cx.defer(move |cx| {
3231 pane.update(cx, |pane, cx| pane.history_updated(cx));
3232 });
3233 }
3234 }
3235}
3236
3237fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
3238 let path = buffer_path
3239 .as_ref()
3240 .and_then(|p| {
3241 p.path
3242 .to_str()
3243 .and_then(|s| if s.is_empty() { None } else { Some(s) })
3244 })
3245 .unwrap_or("This buffer");
3246 let path = truncate_and_remove_front(path, 80);
3247 format!("{path} contains unsaved edits. Do you want to save it?")
3248}
3249
3250pub fn tab_details(items: &[Box<dyn ItemHandle>], cx: &AppContext) -> Vec<usize> {
3251 let mut tab_details = items.iter().map(|_| 0).collect::<Vec<_>>();
3252 let mut tab_descriptions = HashMap::default();
3253 let mut done = false;
3254 while !done {
3255 done = true;
3256
3257 // Store item indices by their tab description.
3258 for (ix, (item, detail)) in items.iter().zip(&tab_details).enumerate() {
3259 if let Some(description) = item.tab_description(*detail, cx) {
3260 if *detail == 0
3261 || Some(&description) != item.tab_description(detail - 1, cx).as_ref()
3262 {
3263 tab_descriptions
3264 .entry(description)
3265 .or_insert(Vec::new())
3266 .push(ix);
3267 }
3268 }
3269 }
3270
3271 // If two or more items have the same tab description, increase their level
3272 // of detail and try again.
3273 for (_, item_ixs) in tab_descriptions.drain() {
3274 if item_ixs.len() > 1 {
3275 done = false;
3276 for ix in item_ixs {
3277 tab_details[ix] += 1;
3278 }
3279 }
3280 }
3281 }
3282
3283 tab_details
3284}
3285
3286pub fn render_item_indicator(item: Box<dyn ItemHandle>, cx: &WindowContext) -> Option<Indicator> {
3287 maybe!({
3288 let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
3289 (true, _) => Color::Warning,
3290 (_, true) => Color::Accent,
3291 (false, false) => return None,
3292 };
3293
3294 Some(Indicator::dot().color(indicator_color))
3295 })
3296}
3297
3298impl Render for DraggedTab {
3299 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3300 let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
3301 let label = self.item.tab_content(
3302 TabContentParams {
3303 detail: Some(self.detail),
3304 selected: false,
3305 preview: false,
3306 },
3307 cx,
3308 );
3309 Tab::new("")
3310 .toggle_state(self.is_active)
3311 .child(label)
3312 .render(cx)
3313 .font(ui_font)
3314 }
3315}
3316
3317#[cfg(test)]
3318mod tests {
3319 use std::num::NonZero;
3320
3321 use super::*;
3322 use crate::item::test::{TestItem, TestProjectItem};
3323 use gpui::{TestAppContext, VisualTestContext};
3324 use project::FakeFs;
3325 use settings::SettingsStore;
3326 use theme::LoadThemes;
3327
3328 #[gpui::test]
3329 async fn test_remove_active_empty(cx: &mut TestAppContext) {
3330 init_test(cx);
3331 let fs = FakeFs::new(cx.executor());
3332
3333 let project = Project::test(fs, None, cx).await;
3334 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3335 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3336
3337 pane.update(cx, |pane, cx| {
3338 assert!(pane
3339 .close_active_item(&CloseActiveItem { save_intent: None }, cx)
3340 .is_none())
3341 });
3342 }
3343
3344 #[gpui::test]
3345 async fn test_add_item_capped_to_max_tabs(cx: &mut TestAppContext) {
3346 init_test(cx);
3347 let fs = FakeFs::new(cx.executor());
3348
3349 let project = Project::test(fs, None, cx).await;
3350 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3351 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3352
3353 for i in 0..7 {
3354 add_labeled_item(&pane, format!("{}", i).as_str(), false, cx);
3355 }
3356 set_max_tabs(cx, Some(5));
3357 add_labeled_item(&pane, "7", false, cx);
3358 // Remove items to respect the max tab cap.
3359 assert_item_labels(&pane, ["3", "4", "5", "6", "7*"], cx);
3360 pane.update(cx, |pane, cx| {
3361 pane.activate_item(0, false, false, cx);
3362 });
3363 add_labeled_item(&pane, "X", false, cx);
3364 // Respect activation order.
3365 assert_item_labels(&pane, ["3", "X*", "5", "6", "7"], cx);
3366
3367 for i in 0..7 {
3368 add_labeled_item(&pane, format!("D{}", i).as_str(), true, cx);
3369 }
3370 // Keeps dirty items, even over max tab cap.
3371 assert_item_labels(
3372 &pane,
3373 ["D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6*^"],
3374 cx,
3375 );
3376
3377 set_max_tabs(cx, None);
3378 for i in 0..7 {
3379 add_labeled_item(&pane, format!("N{}", i).as_str(), false, cx);
3380 }
3381 // No cap when max tabs is None.
3382 assert_item_labels(
3383 &pane,
3384 [
3385 "D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6^", "N0", "N1", "N2", "N3", "N4",
3386 "N5", "N6*",
3387 ],
3388 cx,
3389 );
3390 }
3391
3392 #[gpui::test]
3393 async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
3394 init_test(cx);
3395 let fs = FakeFs::new(cx.executor());
3396
3397 let project = Project::test(fs, None, cx).await;
3398 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3399 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3400
3401 // 1. Add with a destination index
3402 // a. Add before the active item
3403 set_labeled_items(&pane, ["A", "B*", "C"], cx);
3404 pane.update(cx, |pane, cx| {
3405 pane.add_item(
3406 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3407 false,
3408 false,
3409 Some(0),
3410 cx,
3411 );
3412 });
3413 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
3414
3415 // b. Add after the active item
3416 set_labeled_items(&pane, ["A", "B*", "C"], cx);
3417 pane.update(cx, |pane, cx| {
3418 pane.add_item(
3419 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3420 false,
3421 false,
3422 Some(2),
3423 cx,
3424 );
3425 });
3426 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3427
3428 // c. Add at the end of the item list (including off the length)
3429 set_labeled_items(&pane, ["A", "B*", "C"], cx);
3430 pane.update(cx, |pane, cx| {
3431 pane.add_item(
3432 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3433 false,
3434 false,
3435 Some(5),
3436 cx,
3437 );
3438 });
3439 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3440
3441 // 2. Add without a destination index
3442 // a. Add with active item at the start of the item list
3443 set_labeled_items(&pane, ["A*", "B", "C"], cx);
3444 pane.update(cx, |pane, cx| {
3445 pane.add_item(
3446 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3447 false,
3448 false,
3449 None,
3450 cx,
3451 );
3452 });
3453 set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
3454
3455 // b. Add with active item at the end of the item list
3456 set_labeled_items(&pane, ["A", "B", "C*"], cx);
3457 pane.update(cx, |pane, cx| {
3458 pane.add_item(
3459 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3460 false,
3461 false,
3462 None,
3463 cx,
3464 );
3465 });
3466 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3467 }
3468
3469 #[gpui::test]
3470 async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
3471 init_test(cx);
3472 let fs = FakeFs::new(cx.executor());
3473
3474 let project = Project::test(fs, None, cx).await;
3475 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3476 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3477
3478 // 1. Add with a destination index
3479 // 1a. Add before the active item
3480 let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3481 pane.update(cx, |pane, cx| {
3482 pane.add_item(d, false, false, Some(0), cx);
3483 });
3484 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
3485
3486 // 1b. Add after the active item
3487 let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3488 pane.update(cx, |pane, cx| {
3489 pane.add_item(d, false, false, Some(2), cx);
3490 });
3491 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3492
3493 // 1c. Add at the end of the item list (including off the length)
3494 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3495 pane.update(cx, |pane, cx| {
3496 pane.add_item(a, false, false, Some(5), cx);
3497 });
3498 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3499
3500 // 1d. Add same item to active index
3501 let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3502 pane.update(cx, |pane, cx| {
3503 pane.add_item(b, false, false, Some(1), cx);
3504 });
3505 assert_item_labels(&pane, ["A", "B*", "C"], cx);
3506
3507 // 1e. Add item to index after same item in last position
3508 let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3509 pane.update(cx, |pane, cx| {
3510 pane.add_item(c, false, false, Some(2), cx);
3511 });
3512 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3513
3514 // 2. Add without a destination index
3515 // 2a. Add with active item at the start of the item list
3516 let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
3517 pane.update(cx, |pane, cx| {
3518 pane.add_item(d, false, false, None, cx);
3519 });
3520 assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
3521
3522 // 2b. Add with active item at the end of the item list
3523 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
3524 pane.update(cx, |pane, cx| {
3525 pane.add_item(a, false, false, None, cx);
3526 });
3527 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3528
3529 // 2c. Add active item to active item at end of list
3530 let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
3531 pane.update(cx, |pane, cx| {
3532 pane.add_item(c, false, false, None, cx);
3533 });
3534 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3535
3536 // 2d. Add active item to active item at start of list
3537 let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
3538 pane.update(cx, |pane, cx| {
3539 pane.add_item(a, false, false, None, cx);
3540 });
3541 assert_item_labels(&pane, ["A*", "B", "C"], cx);
3542 }
3543
3544 #[gpui::test]
3545 async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
3546 init_test(cx);
3547 let fs = FakeFs::new(cx.executor());
3548
3549 let project = Project::test(fs, None, cx).await;
3550 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3551 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3552
3553 // singleton view
3554 pane.update(cx, |pane, cx| {
3555 pane.add_item(
3556 Box::new(cx.new_view(|cx| {
3557 TestItem::new(cx)
3558 .with_singleton(true)
3559 .with_label("buffer 1")
3560 .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
3561 })),
3562 false,
3563 false,
3564 None,
3565 cx,
3566 );
3567 });
3568 assert_item_labels(&pane, ["buffer 1*"], cx);
3569
3570 // new singleton view with the same project entry
3571 pane.update(cx, |pane, cx| {
3572 pane.add_item(
3573 Box::new(cx.new_view(|cx| {
3574 TestItem::new(cx)
3575 .with_singleton(true)
3576 .with_label("buffer 1")
3577 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3578 })),
3579 false,
3580 false,
3581 None,
3582 cx,
3583 );
3584 });
3585 assert_item_labels(&pane, ["buffer 1*"], cx);
3586
3587 // new singleton view with different project entry
3588 pane.update(cx, |pane, cx| {
3589 pane.add_item(
3590 Box::new(cx.new_view(|cx| {
3591 TestItem::new(cx)
3592 .with_singleton(true)
3593 .with_label("buffer 2")
3594 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
3595 })),
3596 false,
3597 false,
3598 None,
3599 cx,
3600 );
3601 });
3602 assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
3603
3604 // new multibuffer view with the same project entry
3605 pane.update(cx, |pane, cx| {
3606 pane.add_item(
3607 Box::new(cx.new_view(|cx| {
3608 TestItem::new(cx)
3609 .with_singleton(false)
3610 .with_label("multibuffer 1")
3611 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3612 })),
3613 false,
3614 false,
3615 None,
3616 cx,
3617 );
3618 });
3619 assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
3620
3621 // another multibuffer view with the same project entry
3622 pane.update(cx, |pane, cx| {
3623 pane.add_item(
3624 Box::new(cx.new_view(|cx| {
3625 TestItem::new(cx)
3626 .with_singleton(false)
3627 .with_label("multibuffer 1b")
3628 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3629 })),
3630 false,
3631 false,
3632 None,
3633 cx,
3634 );
3635 });
3636 assert_item_labels(
3637 &pane,
3638 ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
3639 cx,
3640 );
3641 }
3642
3643 #[gpui::test]
3644 async fn test_remove_item_ordering_history(cx: &mut TestAppContext) {
3645 init_test(cx);
3646 let fs = FakeFs::new(cx.executor());
3647
3648 let project = Project::test(fs, None, cx).await;
3649 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3650 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3651
3652 add_labeled_item(&pane, "A", false, cx);
3653 add_labeled_item(&pane, "B", false, cx);
3654 add_labeled_item(&pane, "C", false, cx);
3655 add_labeled_item(&pane, "D", false, cx);
3656 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3657
3658 pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
3659 add_labeled_item(&pane, "1", false, cx);
3660 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
3661
3662 pane.update(cx, |pane, cx| {
3663 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3664 })
3665 .unwrap()
3666 .await
3667 .unwrap();
3668 assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
3669
3670 pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
3671 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3672
3673 pane.update(cx, |pane, cx| {
3674 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3675 })
3676 .unwrap()
3677 .await
3678 .unwrap();
3679 assert_item_labels(&pane, ["A", "B*", "C"], cx);
3680
3681 pane.update(cx, |pane, cx| {
3682 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3683 })
3684 .unwrap()
3685 .await
3686 .unwrap();
3687 assert_item_labels(&pane, ["A", "C*"], cx);
3688
3689 pane.update(cx, |pane, cx| {
3690 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3691 })
3692 .unwrap()
3693 .await
3694 .unwrap();
3695 assert_item_labels(&pane, ["A*"], cx);
3696 }
3697
3698 #[gpui::test]
3699 async fn test_remove_item_ordering_neighbour(cx: &mut TestAppContext) {
3700 init_test(cx);
3701 cx.update_global::<SettingsStore, ()>(|s, cx| {
3702 s.update_user_settings::<ItemSettings>(cx, |s| {
3703 s.activate_on_close = Some(ActivateOnClose::Neighbour);
3704 });
3705 });
3706 let fs = FakeFs::new(cx.executor());
3707
3708 let project = Project::test(fs, None, cx).await;
3709 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3710 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3711
3712 add_labeled_item(&pane, "A", false, cx);
3713 add_labeled_item(&pane, "B", false, cx);
3714 add_labeled_item(&pane, "C", false, cx);
3715 add_labeled_item(&pane, "D", false, cx);
3716 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3717
3718 pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
3719 add_labeled_item(&pane, "1", false, cx);
3720 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
3721
3722 pane.update(cx, |pane, cx| {
3723 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3724 })
3725 .unwrap()
3726 .await
3727 .unwrap();
3728 assert_item_labels(&pane, ["A", "B", "C*", "D"], cx);
3729
3730 pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
3731 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3732
3733 pane.update(cx, |pane, cx| {
3734 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3735 })
3736 .unwrap()
3737 .await
3738 .unwrap();
3739 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3740
3741 pane.update(cx, |pane, cx| {
3742 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3743 })
3744 .unwrap()
3745 .await
3746 .unwrap();
3747 assert_item_labels(&pane, ["A", "B*"], cx);
3748
3749 pane.update(cx, |pane, cx| {
3750 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3751 })
3752 .unwrap()
3753 .await
3754 .unwrap();
3755 assert_item_labels(&pane, ["A*"], cx);
3756 }
3757
3758 #[gpui::test]
3759 async fn test_remove_item_ordering_left_neighbour(cx: &mut TestAppContext) {
3760 init_test(cx);
3761 cx.update_global::<SettingsStore, ()>(|s, cx| {
3762 s.update_user_settings::<ItemSettings>(cx, |s| {
3763 s.activate_on_close = Some(ActivateOnClose::LeftNeighbour);
3764 });
3765 });
3766 let fs = FakeFs::new(cx.executor());
3767
3768 let project = Project::test(fs, None, cx).await;
3769 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3770 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3771
3772 add_labeled_item(&pane, "A", false, cx);
3773 add_labeled_item(&pane, "B", false, cx);
3774 add_labeled_item(&pane, "C", false, cx);
3775 add_labeled_item(&pane, "D", false, cx);
3776 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3777
3778 pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
3779 add_labeled_item(&pane, "1", false, cx);
3780 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
3781
3782 pane.update(cx, |pane, cx| {
3783 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3784 })
3785 .unwrap()
3786 .await
3787 .unwrap();
3788 assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
3789
3790 pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
3791 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3792
3793 pane.update(cx, |pane, cx| {
3794 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3795 })
3796 .unwrap()
3797 .await
3798 .unwrap();
3799 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3800
3801 pane.update(cx, |pane, cx| pane.activate_item(0, false, false, cx));
3802 assert_item_labels(&pane, ["A*", "B", "C"], cx);
3803
3804 pane.update(cx, |pane, cx| {
3805 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3806 })
3807 .unwrap()
3808 .await
3809 .unwrap();
3810 assert_item_labels(&pane, ["B*", "C"], cx);
3811
3812 pane.update(cx, |pane, cx| {
3813 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3814 })
3815 .unwrap()
3816 .await
3817 .unwrap();
3818 assert_item_labels(&pane, ["C*"], cx);
3819 }
3820
3821 #[gpui::test]
3822 async fn test_close_inactive_items(cx: &mut TestAppContext) {
3823 init_test(cx);
3824 let fs = FakeFs::new(cx.executor());
3825
3826 let project = Project::test(fs, None, cx).await;
3827 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3828 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3829
3830 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
3831
3832 pane.update(cx, |pane, cx| {
3833 pane.close_inactive_items(
3834 &CloseInactiveItems {
3835 save_intent: None,
3836 close_pinned: false,
3837 },
3838 cx,
3839 )
3840 })
3841 .unwrap()
3842 .await
3843 .unwrap();
3844 assert_item_labels(&pane, ["C*"], cx);
3845 }
3846
3847 #[gpui::test]
3848 async fn test_close_clean_items(cx: &mut TestAppContext) {
3849 init_test(cx);
3850 let fs = FakeFs::new(cx.executor());
3851
3852 let project = Project::test(fs, None, cx).await;
3853 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3854 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3855
3856 add_labeled_item(&pane, "A", true, cx);
3857 add_labeled_item(&pane, "B", false, cx);
3858 add_labeled_item(&pane, "C", true, cx);
3859 add_labeled_item(&pane, "D", false, cx);
3860 add_labeled_item(&pane, "E", false, cx);
3861 assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
3862
3863 pane.update(cx, |pane, cx| {
3864 pane.close_clean_items(
3865 &CloseCleanItems {
3866 close_pinned: false,
3867 },
3868 cx,
3869 )
3870 })
3871 .unwrap()
3872 .await
3873 .unwrap();
3874 assert_item_labels(&pane, ["A^", "C*^"], cx);
3875 }
3876
3877 #[gpui::test]
3878 async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
3879 init_test(cx);
3880 let fs = FakeFs::new(cx.executor());
3881
3882 let project = Project::test(fs, None, cx).await;
3883 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3884 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3885
3886 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
3887
3888 pane.update(cx, |pane, cx| {
3889 pane.close_items_to_the_left(
3890 &CloseItemsToTheLeft {
3891 close_pinned: false,
3892 },
3893 cx,
3894 )
3895 })
3896 .unwrap()
3897 .await
3898 .unwrap();
3899 assert_item_labels(&pane, ["C*", "D", "E"], cx);
3900 }
3901
3902 #[gpui::test]
3903 async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
3904 init_test(cx);
3905 let fs = FakeFs::new(cx.executor());
3906
3907 let project = Project::test(fs, None, cx).await;
3908 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3909 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3910
3911 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
3912
3913 pane.update(cx, |pane, cx| {
3914 pane.close_items_to_the_right(
3915 &CloseItemsToTheRight {
3916 close_pinned: false,
3917 },
3918 cx,
3919 )
3920 })
3921 .unwrap()
3922 .await
3923 .unwrap();
3924 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3925 }
3926
3927 #[gpui::test]
3928 async fn test_close_all_items(cx: &mut TestAppContext) {
3929 init_test(cx);
3930 let fs = FakeFs::new(cx.executor());
3931
3932 let project = Project::test(fs, None, cx).await;
3933 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3934 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3935
3936 let item_a = add_labeled_item(&pane, "A", false, cx);
3937 add_labeled_item(&pane, "B", false, cx);
3938 add_labeled_item(&pane, "C", false, cx);
3939 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3940
3941 pane.update(cx, |pane, cx| {
3942 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
3943 pane.pin_tab_at(ix, cx);
3944 pane.close_all_items(
3945 &CloseAllItems {
3946 save_intent: None,
3947 close_pinned: false,
3948 },
3949 cx,
3950 )
3951 })
3952 .unwrap()
3953 .await
3954 .unwrap();
3955 assert_item_labels(&pane, ["A*"], cx);
3956
3957 pane.update(cx, |pane, cx| {
3958 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
3959 pane.unpin_tab_at(ix, cx);
3960 pane.close_all_items(
3961 &CloseAllItems {
3962 save_intent: None,
3963 close_pinned: false,
3964 },
3965 cx,
3966 )
3967 })
3968 .unwrap()
3969 .await
3970 .unwrap();
3971
3972 assert_item_labels(&pane, [], cx);
3973
3974 add_labeled_item(&pane, "A", true, cx).update(cx, |item, cx| {
3975 item.project_items
3976 .push(TestProjectItem::new(1, "A.txt", cx))
3977 });
3978 add_labeled_item(&pane, "B", true, cx).update(cx, |item, cx| {
3979 item.project_items
3980 .push(TestProjectItem::new(2, "B.txt", cx))
3981 });
3982 add_labeled_item(&pane, "C", true, cx).update(cx, |item, cx| {
3983 item.project_items
3984 .push(TestProjectItem::new(3, "C.txt", cx))
3985 });
3986 assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
3987
3988 let save = pane
3989 .update(cx, |pane, cx| {
3990 pane.close_all_items(
3991 &CloseAllItems {
3992 save_intent: None,
3993 close_pinned: false,
3994 },
3995 cx,
3996 )
3997 })
3998 .unwrap();
3999
4000 cx.executor().run_until_parked();
4001 cx.simulate_prompt_answer(2);
4002 save.await.unwrap();
4003 assert_item_labels(&pane, [], cx);
4004
4005 add_labeled_item(&pane, "A", true, cx);
4006 add_labeled_item(&pane, "B", true, cx);
4007 add_labeled_item(&pane, "C", true, cx);
4008 assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
4009 let save = pane
4010 .update(cx, |pane, cx| {
4011 pane.close_all_items(
4012 &CloseAllItems {
4013 save_intent: None,
4014 close_pinned: false,
4015 },
4016 cx,
4017 )
4018 })
4019 .unwrap();
4020
4021 cx.executor().run_until_parked();
4022 cx.simulate_prompt_answer(2);
4023 save.await.unwrap();
4024 assert_item_labels(&pane, [], cx);
4025 }
4026
4027 #[gpui::test]
4028 async fn test_close_all_items_including_pinned(cx: &mut TestAppContext) {
4029 init_test(cx);
4030 let fs = FakeFs::new(cx.executor());
4031
4032 let project = Project::test(fs, None, cx).await;
4033 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
4034 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4035
4036 let item_a = add_labeled_item(&pane, "A", false, cx);
4037 add_labeled_item(&pane, "B", false, cx);
4038 add_labeled_item(&pane, "C", false, cx);
4039 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4040
4041 pane.update(cx, |pane, cx| {
4042 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4043 pane.pin_tab_at(ix, cx);
4044 pane.close_all_items(
4045 &CloseAllItems {
4046 save_intent: None,
4047 close_pinned: true,
4048 },
4049 cx,
4050 )
4051 })
4052 .unwrap()
4053 .await
4054 .unwrap();
4055 assert_item_labels(&pane, [], cx);
4056 }
4057
4058 fn init_test(cx: &mut TestAppContext) {
4059 cx.update(|cx| {
4060 let settings_store = SettingsStore::test(cx);
4061 cx.set_global(settings_store);
4062 theme::init(LoadThemes::JustBase, cx);
4063 crate::init_settings(cx);
4064 Project::init_settings(cx);
4065 });
4066 }
4067
4068 fn set_max_tabs(cx: &mut TestAppContext, value: Option<usize>) {
4069 cx.update_global(|store: &mut SettingsStore, cx| {
4070 store.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4071 settings.max_tabs = value.map(|v| NonZero::new(v).unwrap())
4072 });
4073 });
4074 }
4075
4076 fn add_labeled_item(
4077 pane: &View<Pane>,
4078 label: &str,
4079 is_dirty: bool,
4080 cx: &mut VisualTestContext,
4081 ) -> Box<View<TestItem>> {
4082 pane.update(cx, |pane, cx| {
4083 let labeled_item = Box::new(
4084 cx.new_view(|cx| TestItem::new(cx).with_label(label).with_dirty(is_dirty)),
4085 );
4086 pane.add_item(labeled_item.clone(), false, false, None, cx);
4087 labeled_item
4088 })
4089 }
4090
4091 fn set_labeled_items<const COUNT: usize>(
4092 pane: &View<Pane>,
4093 labels: [&str; COUNT],
4094 cx: &mut VisualTestContext,
4095 ) -> [Box<View<TestItem>>; COUNT] {
4096 pane.update(cx, |pane, cx| {
4097 pane.items.clear();
4098 let mut active_item_index = 0;
4099
4100 let mut index = 0;
4101 let items = labels.map(|mut label| {
4102 if label.ends_with('*') {
4103 label = label.trim_end_matches('*');
4104 active_item_index = index;
4105 }
4106
4107 let labeled_item = Box::new(cx.new_view(|cx| TestItem::new(cx).with_label(label)));
4108 pane.add_item(labeled_item.clone(), false, false, None, cx);
4109 index += 1;
4110 labeled_item
4111 });
4112
4113 pane.activate_item(active_item_index, false, false, cx);
4114
4115 items
4116 })
4117 }
4118
4119 // Assert the item label, with the active item label suffixed with a '*'
4120 #[track_caller]
4121 fn assert_item_labels<const COUNT: usize>(
4122 pane: &View<Pane>,
4123 expected_states: [&str; COUNT],
4124 cx: &mut VisualTestContext,
4125 ) {
4126 let actual_states = pane.update(cx, |pane, cx| {
4127 pane.items
4128 .iter()
4129 .enumerate()
4130 .map(|(ix, item)| {
4131 let mut state = item
4132 .to_any()
4133 .downcast::<TestItem>()
4134 .unwrap()
4135 .read(cx)
4136 .label
4137 .clone();
4138 if ix == pane.active_item_index {
4139 state.push('*');
4140 }
4141 if item.is_dirty(cx) {
4142 state.push('^');
4143 }
4144 state
4145 })
4146 .collect::<Vec<_>>()
4147 });
4148 assert_eq!(
4149 actual_states, expected_states,
4150 "pane items do not match expectation"
4151 );
4152 }
4153}