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, AnchorCorner, AnyElement,
18 AppContext, AsyncWindowContext, ClickEvent, ClipboardItem, 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(AnchorCorner::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(AnchorCorner::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(
2510 anchored()
2511 .anchor(AnchorCorner::TopRight)
2512 .child(menu.clone()),
2513 )
2514 .with_priority(1),
2515 )
2516 }
2517
2518 pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
2519 self.zoomed = zoomed;
2520 cx.notify();
2521 }
2522
2523 pub fn is_zoomed(&self) -> bool {
2524 self.zoomed
2525 }
2526
2527 fn handle_drag_move<T: 'static>(
2528 &mut self,
2529 event: &DragMoveEvent<T>,
2530 cx: &mut ViewContext<Self>,
2531 ) {
2532 let can_split_predicate = self.can_split_predicate.take();
2533 let can_split = match &can_split_predicate {
2534 Some(can_split_predicate) => can_split_predicate(self, event.dragged_item(), cx),
2535 None => false,
2536 };
2537 self.can_split_predicate = can_split_predicate;
2538 if !can_split {
2539 return;
2540 }
2541
2542 let rect = event.bounds.size;
2543
2544 let size = event.bounds.size.width.min(event.bounds.size.height)
2545 * WorkspaceSettings::get_global(cx).drop_target_size;
2546
2547 let relative_cursor = Point::new(
2548 event.event.position.x - event.bounds.left(),
2549 event.event.position.y - event.bounds.top(),
2550 );
2551
2552 let direction = if relative_cursor.x < size
2553 || relative_cursor.x > rect.width - size
2554 || relative_cursor.y < size
2555 || relative_cursor.y > rect.height - size
2556 {
2557 [
2558 SplitDirection::Up,
2559 SplitDirection::Right,
2560 SplitDirection::Down,
2561 SplitDirection::Left,
2562 ]
2563 .iter()
2564 .min_by_key(|side| match side {
2565 SplitDirection::Up => relative_cursor.y,
2566 SplitDirection::Right => rect.width - relative_cursor.x,
2567 SplitDirection::Down => rect.height - relative_cursor.y,
2568 SplitDirection::Left => relative_cursor.x,
2569 })
2570 .cloned()
2571 } else {
2572 None
2573 };
2574
2575 if direction != self.drag_split_direction {
2576 self.drag_split_direction = direction;
2577 }
2578 }
2579
2580 fn handle_tab_drop(
2581 &mut self,
2582 dragged_tab: &DraggedTab,
2583 ix: usize,
2584 cx: &mut ViewContext<'_, Self>,
2585 ) {
2586 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2587 if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_tab, cx) {
2588 return;
2589 }
2590 }
2591 let mut to_pane = cx.view().clone();
2592 let split_direction = self.drag_split_direction;
2593 let item_id = dragged_tab.item.item_id();
2594 if let Some(preview_item_id) = self.preview_item_id {
2595 if item_id == preview_item_id {
2596 self.set_preview_item_id(None, cx);
2597 }
2598 }
2599
2600 let from_pane = dragged_tab.pane.clone();
2601 self.workspace
2602 .update(cx, |_, cx| {
2603 cx.defer(move |workspace, cx| {
2604 if let Some(split_direction) = split_direction {
2605 to_pane = workspace.split_pane(to_pane, split_direction, cx);
2606 }
2607 let old_ix = from_pane.read(cx).index_for_item_id(item_id);
2608 let old_len = to_pane.read(cx).items.len();
2609 move_item(&from_pane, &to_pane, item_id, ix, cx);
2610 if to_pane == from_pane {
2611 if let Some(old_index) = old_ix {
2612 to_pane.update(cx, |this, _| {
2613 if old_index < this.pinned_tab_count
2614 && (ix == this.items.len() || ix > this.pinned_tab_count)
2615 {
2616 this.pinned_tab_count -= 1;
2617 } else if this.has_pinned_tabs()
2618 && old_index >= this.pinned_tab_count
2619 && ix < this.pinned_tab_count
2620 {
2621 this.pinned_tab_count += 1;
2622 }
2623 });
2624 }
2625 } else {
2626 to_pane.update(cx, |this, _| {
2627 if this.items.len() > old_len // Did we not deduplicate on drag?
2628 && this.has_pinned_tabs()
2629 && ix < this.pinned_tab_count
2630 {
2631 this.pinned_tab_count += 1;
2632 }
2633 });
2634 from_pane.update(cx, |this, _| {
2635 if let Some(index) = old_ix {
2636 if this.pinned_tab_count > index {
2637 this.pinned_tab_count -= 1;
2638 }
2639 }
2640 })
2641 }
2642 });
2643 })
2644 .log_err();
2645 }
2646
2647 fn handle_dragged_selection_drop(
2648 &mut self,
2649 dragged_selection: &DraggedSelection,
2650 dragged_onto: Option<usize>,
2651 cx: &mut ViewContext<'_, Self>,
2652 ) {
2653 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2654 if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_selection, cx) {
2655 return;
2656 }
2657 }
2658 self.handle_project_entry_drop(
2659 &dragged_selection.active_selection.entry_id,
2660 dragged_onto,
2661 cx,
2662 );
2663 }
2664
2665 fn handle_project_entry_drop(
2666 &mut self,
2667 project_entry_id: &ProjectEntryId,
2668 target: Option<usize>,
2669 cx: &mut ViewContext<'_, Self>,
2670 ) {
2671 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2672 if let ControlFlow::Break(()) = custom_drop_handle(self, project_entry_id, cx) {
2673 return;
2674 }
2675 }
2676 let mut to_pane = cx.view().clone();
2677 let split_direction = self.drag_split_direction;
2678 let project_entry_id = *project_entry_id;
2679 self.workspace
2680 .update(cx, |_, cx| {
2681 cx.defer(move |workspace, cx| {
2682 if let Some(path) = workspace
2683 .project()
2684 .read(cx)
2685 .path_for_entry(project_entry_id, cx)
2686 {
2687 let load_path_task = workspace.load_path(path, cx);
2688 cx.spawn(|workspace, mut cx| async move {
2689 if let Some((project_entry_id, build_item)) =
2690 load_path_task.await.notify_async_err(&mut cx)
2691 {
2692 let (to_pane, new_item_handle) = workspace
2693 .update(&mut cx, |workspace, cx| {
2694 if let Some(split_direction) = split_direction {
2695 to_pane =
2696 workspace.split_pane(to_pane, split_direction, cx);
2697 }
2698 let new_item_handle = to_pane.update(cx, |pane, cx| {
2699 pane.open_item(
2700 project_entry_id,
2701 true,
2702 false,
2703 target,
2704 cx,
2705 build_item,
2706 )
2707 });
2708 (to_pane, new_item_handle)
2709 })
2710 .log_err()?;
2711 to_pane
2712 .update(&mut cx, |this, cx| {
2713 let Some(index) = this.index_for_item(&*new_item_handle)
2714 else {
2715 return;
2716 };
2717
2718 if target.map_or(false, |target| this.is_tab_pinned(target))
2719 {
2720 this.pin_tab_at(index, cx);
2721 }
2722 })
2723 .ok()?
2724 }
2725 Some(())
2726 })
2727 .detach();
2728 };
2729 });
2730 })
2731 .log_err();
2732 }
2733
2734 fn handle_external_paths_drop(
2735 &mut self,
2736 paths: &ExternalPaths,
2737 cx: &mut ViewContext<'_, Self>,
2738 ) {
2739 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2740 if let ControlFlow::Break(()) = custom_drop_handle(self, paths, cx) {
2741 return;
2742 }
2743 }
2744 let mut to_pane = cx.view().clone();
2745 let mut split_direction = self.drag_split_direction;
2746 let paths = paths.paths().to_vec();
2747 let is_remote = self
2748 .workspace
2749 .update(cx, |workspace, cx| {
2750 if workspace.project().read(cx).is_via_collab() {
2751 workspace.show_error(
2752 &anyhow::anyhow!("Cannot drop files on a remote project"),
2753 cx,
2754 );
2755 true
2756 } else {
2757 false
2758 }
2759 })
2760 .unwrap_or(true);
2761 if is_remote {
2762 return;
2763 }
2764
2765 self.workspace
2766 .update(cx, |workspace, cx| {
2767 let fs = Arc::clone(workspace.project().read(cx).fs());
2768 cx.spawn(|workspace, mut cx| async move {
2769 let mut is_file_checks = FuturesUnordered::new();
2770 for path in &paths {
2771 is_file_checks.push(fs.is_file(path))
2772 }
2773 let mut has_files_to_open = false;
2774 while let Some(is_file) = is_file_checks.next().await {
2775 if is_file {
2776 has_files_to_open = true;
2777 break;
2778 }
2779 }
2780 drop(is_file_checks);
2781 if !has_files_to_open {
2782 split_direction = None;
2783 }
2784
2785 if let Ok(open_task) = workspace.update(&mut cx, |workspace, cx| {
2786 if let Some(split_direction) = split_direction {
2787 to_pane = workspace.split_pane(to_pane, split_direction, cx);
2788 }
2789 workspace.open_paths(
2790 paths,
2791 OpenVisible::OnlyDirectories,
2792 Some(to_pane.downgrade()),
2793 cx,
2794 )
2795 }) {
2796 let opened_items: Vec<_> = open_task.await;
2797 _ = workspace.update(&mut cx, |workspace, cx| {
2798 for item in opened_items.into_iter().flatten() {
2799 if let Err(e) = item {
2800 workspace.show_error(&e, cx);
2801 }
2802 }
2803 });
2804 }
2805 })
2806 .detach();
2807 })
2808 .log_err();
2809 }
2810
2811 pub fn display_nav_history_buttons(&mut self, display: Option<bool>) {
2812 self.display_nav_history_buttons = display;
2813 }
2814
2815 fn get_non_closeable_item_ids(&self, close_pinned: bool) -> Vec<EntityId> {
2816 if close_pinned {
2817 return vec![];
2818 }
2819
2820 self.items
2821 .iter()
2822 .map(|item| item.item_id())
2823 .filter(|item_id| {
2824 if let Some(ix) = self.index_for_item_id(*item_id) {
2825 self.is_tab_pinned(ix)
2826 } else {
2827 true
2828 }
2829 })
2830 .collect()
2831 }
2832
2833 pub fn drag_split_direction(&self) -> Option<SplitDirection> {
2834 self.drag_split_direction
2835 }
2836
2837 pub fn set_zoom_out_on_close(&mut self, zoom_out_on_close: bool) {
2838 self.zoom_out_on_close = zoom_out_on_close;
2839 }
2840}
2841
2842impl FocusableView for Pane {
2843 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
2844 self.focus_handle.clone()
2845 }
2846}
2847
2848impl Render for Pane {
2849 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2850 let mut key_context = KeyContext::new_with_defaults();
2851 key_context.add("Pane");
2852 if self.active_item().is_none() {
2853 key_context.add("EmptyPane");
2854 }
2855
2856 let should_display_tab_bar = self.should_display_tab_bar.clone();
2857 let display_tab_bar = should_display_tab_bar(cx);
2858 let is_local = self.project.read(cx).is_local();
2859
2860 v_flex()
2861 .key_context(key_context)
2862 .track_focus(&self.focus_handle(cx))
2863 .size_full()
2864 .flex_none()
2865 .overflow_hidden()
2866 .on_action(cx.listener(|pane, _: &AlternateFile, cx| {
2867 pane.alternate_file(cx);
2868 }))
2869 .on_action(cx.listener(|pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx)))
2870 .on_action(cx.listener(|pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx)))
2871 .on_action(cx.listener(|pane, _: &SplitHorizontal, cx| {
2872 pane.split(SplitDirection::horizontal(cx), cx)
2873 }))
2874 .on_action(cx.listener(|pane, _: &SplitVertical, cx| {
2875 pane.split(SplitDirection::vertical(cx), cx)
2876 }))
2877 .on_action(
2878 cx.listener(|pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx)),
2879 )
2880 .on_action(cx.listener(|pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx)))
2881 .on_action(cx.listener(|pane, _: &GoBack, cx| pane.navigate_backward(cx)))
2882 .on_action(cx.listener(|pane, _: &GoForward, cx| pane.navigate_forward(cx)))
2883 .on_action(cx.listener(|pane, _: &JoinIntoNext, cx| pane.join_into_next(cx)))
2884 .on_action(cx.listener(|pane, _: &JoinAll, cx| pane.join_all(cx)))
2885 .on_action(cx.listener(Pane::toggle_zoom))
2886 .on_action(cx.listener(|pane: &mut Pane, action: &ActivateItem, cx| {
2887 pane.activate_item(action.0, true, true, cx);
2888 }))
2889 .on_action(cx.listener(|pane: &mut Pane, _: &ActivateLastItem, cx| {
2890 pane.activate_item(pane.items.len() - 1, true, true, cx);
2891 }))
2892 .on_action(cx.listener(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
2893 pane.activate_prev_item(true, cx);
2894 }))
2895 .on_action(cx.listener(|pane: &mut Pane, _: &ActivateNextItem, cx| {
2896 pane.activate_next_item(true, cx);
2897 }))
2898 .on_action(cx.listener(|pane, _: &SwapItemLeft, cx| pane.swap_item_left(cx)))
2899 .on_action(cx.listener(|pane, _: &SwapItemRight, cx| pane.swap_item_right(cx)))
2900 .on_action(cx.listener(|pane, action, cx| {
2901 pane.toggle_pin_tab(action, cx);
2902 }))
2903 .when(PreviewTabsSettings::get_global(cx).enabled, |this| {
2904 this.on_action(cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, cx| {
2905 if let Some(active_item_id) = pane.active_item().map(|i| i.item_id()) {
2906 if pane.is_active_preview_item(active_item_id) {
2907 pane.set_preview_item_id(None, cx);
2908 } else {
2909 pane.set_preview_item_id(Some(active_item_id), cx);
2910 }
2911 }
2912 }))
2913 })
2914 .on_action(
2915 cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
2916 if let Some(task) = pane.close_active_item(action, cx) {
2917 task.detach_and_log_err(cx)
2918 }
2919 }),
2920 )
2921 .on_action(
2922 cx.listener(|pane: &mut Self, action: &CloseInactiveItems, cx| {
2923 if let Some(task) = pane.close_inactive_items(action, cx) {
2924 task.detach_and_log_err(cx)
2925 }
2926 }),
2927 )
2928 .on_action(
2929 cx.listener(|pane: &mut Self, action: &CloseCleanItems, cx| {
2930 if let Some(task) = pane.close_clean_items(action, cx) {
2931 task.detach_and_log_err(cx)
2932 }
2933 }),
2934 )
2935 .on_action(
2936 cx.listener(|pane: &mut Self, action: &CloseItemsToTheLeft, cx| {
2937 if let Some(task) = pane.close_items_to_the_left(action, cx) {
2938 task.detach_and_log_err(cx)
2939 }
2940 }),
2941 )
2942 .on_action(
2943 cx.listener(|pane: &mut Self, action: &CloseItemsToTheRight, cx| {
2944 if let Some(task) = pane.close_items_to_the_right(action, cx) {
2945 task.detach_and_log_err(cx)
2946 }
2947 }),
2948 )
2949 .on_action(cx.listener(|pane: &mut Self, action: &CloseAllItems, cx| {
2950 if let Some(task) = pane.close_all_items(action, cx) {
2951 task.detach_and_log_err(cx)
2952 }
2953 }))
2954 .on_action(
2955 cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
2956 if let Some(task) = pane.close_active_item(action, cx) {
2957 task.detach_and_log_err(cx)
2958 }
2959 }),
2960 )
2961 .on_action(
2962 cx.listener(|pane: &mut Self, action: &RevealInProjectPanel, cx| {
2963 let entry_id = action
2964 .entry_id
2965 .map(ProjectEntryId::from_proto)
2966 .or_else(|| pane.active_item()?.project_entry_ids(cx).first().copied());
2967 if let Some(entry_id) = entry_id {
2968 pane.project.update(cx, |_, cx| {
2969 cx.emit(project::Event::RevealInProjectPanel(entry_id))
2970 });
2971 }
2972 }),
2973 )
2974 .when(self.active_item().is_some() && display_tab_bar, |pane| {
2975 pane.child(self.render_tab_bar(cx))
2976 })
2977 .child({
2978 let has_worktrees = self.project.read(cx).worktrees(cx).next().is_some();
2979 // main content
2980 div()
2981 .flex_1()
2982 .relative()
2983 .group("")
2984 .overflow_hidden()
2985 .on_drag_move::<DraggedTab>(cx.listener(Self::handle_drag_move))
2986 .on_drag_move::<DraggedSelection>(cx.listener(Self::handle_drag_move))
2987 .when(is_local, |div| {
2988 div.on_drag_move::<ExternalPaths>(cx.listener(Self::handle_drag_move))
2989 })
2990 .map(|div| {
2991 if let Some(item) = self.active_item() {
2992 div.v_flex()
2993 .size_full()
2994 .overflow_hidden()
2995 .child(self.toolbar.clone())
2996 .child(item.to_any())
2997 } else {
2998 let placeholder = div.h_flex().size_full().justify_center();
2999 if has_worktrees {
3000 placeholder
3001 } else {
3002 placeholder.child(
3003 Label::new("Open a file or project to get started.")
3004 .color(Color::Muted),
3005 )
3006 }
3007 }
3008 })
3009 .child(
3010 // drag target
3011 div()
3012 .invisible()
3013 .absolute()
3014 .bg(cx.theme().colors().drop_target_background)
3015 .group_drag_over::<DraggedTab>("", |style| style.visible())
3016 .group_drag_over::<DraggedSelection>("", |style| style.visible())
3017 .when(is_local, |div| {
3018 div.group_drag_over::<ExternalPaths>("", |style| style.visible())
3019 })
3020 .when_some(self.can_drop_predicate.clone(), |this, p| {
3021 this.can_drop(move |a, cx| p(a, cx))
3022 })
3023 .on_drop(cx.listener(move |this, dragged_tab, cx| {
3024 this.handle_tab_drop(dragged_tab, this.active_item_index(), cx)
3025 }))
3026 .on_drop(cx.listener(move |this, selection: &DraggedSelection, cx| {
3027 this.handle_dragged_selection_drop(selection, None, cx)
3028 }))
3029 .on_drop(cx.listener(move |this, paths, cx| {
3030 this.handle_external_paths_drop(paths, cx)
3031 }))
3032 .map(|div| {
3033 let size = DefiniteLength::Fraction(0.5);
3034 match self.drag_split_direction {
3035 None => div.top_0().right_0().bottom_0().left_0(),
3036 Some(SplitDirection::Up) => {
3037 div.top_0().left_0().right_0().h(size)
3038 }
3039 Some(SplitDirection::Down) => {
3040 div.left_0().bottom_0().right_0().h(size)
3041 }
3042 Some(SplitDirection::Left) => {
3043 div.top_0().left_0().bottom_0().w(size)
3044 }
3045 Some(SplitDirection::Right) => {
3046 div.top_0().bottom_0().right_0().w(size)
3047 }
3048 }
3049 }),
3050 )
3051 })
3052 .on_mouse_down(
3053 MouseButton::Navigate(NavigationDirection::Back),
3054 cx.listener(|pane, _, cx| {
3055 if let Some(workspace) = pane.workspace.upgrade() {
3056 let pane = cx.view().downgrade();
3057 cx.window_context().defer(move |cx| {
3058 workspace.update(cx, |workspace, cx| {
3059 workspace.go_back(pane, cx).detach_and_log_err(cx)
3060 })
3061 })
3062 }
3063 }),
3064 )
3065 .on_mouse_down(
3066 MouseButton::Navigate(NavigationDirection::Forward),
3067 cx.listener(|pane, _, cx| {
3068 if let Some(workspace) = pane.workspace.upgrade() {
3069 let pane = cx.view().downgrade();
3070 cx.window_context().defer(move |cx| {
3071 workspace.update(cx, |workspace, cx| {
3072 workspace.go_forward(pane, cx).detach_and_log_err(cx)
3073 })
3074 })
3075 }
3076 }),
3077 )
3078 }
3079}
3080
3081impl ItemNavHistory {
3082 pub fn push<D: 'static + Send + Any>(&mut self, data: Option<D>, cx: &mut WindowContext) {
3083 self.history
3084 .push(data, self.item.clone(), self.is_preview, cx);
3085 }
3086
3087 pub fn pop_backward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
3088 self.history.pop(NavigationMode::GoingBack, cx)
3089 }
3090
3091 pub fn pop_forward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
3092 self.history.pop(NavigationMode::GoingForward, cx)
3093 }
3094}
3095
3096impl NavHistory {
3097 pub fn for_each_entry(
3098 &self,
3099 cx: &AppContext,
3100 mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
3101 ) {
3102 let borrowed_history = self.0.lock();
3103 borrowed_history
3104 .forward_stack
3105 .iter()
3106 .chain(borrowed_history.backward_stack.iter())
3107 .chain(borrowed_history.closed_stack.iter())
3108 .for_each(|entry| {
3109 if let Some(project_and_abs_path) =
3110 borrowed_history.paths_by_item.get(&entry.item.id())
3111 {
3112 f(entry, project_and_abs_path.clone());
3113 } else if let Some(item) = entry.item.upgrade() {
3114 if let Some(path) = item.project_path(cx) {
3115 f(entry, (path, None));
3116 }
3117 }
3118 })
3119 }
3120
3121 pub fn set_mode(&mut self, mode: NavigationMode) {
3122 self.0.lock().mode = mode;
3123 }
3124
3125 pub fn mode(&self) -> NavigationMode {
3126 self.0.lock().mode
3127 }
3128
3129 pub fn disable(&mut self) {
3130 self.0.lock().mode = NavigationMode::Disabled;
3131 }
3132
3133 pub fn enable(&mut self) {
3134 self.0.lock().mode = NavigationMode::Normal;
3135 }
3136
3137 pub fn pop(&mut self, mode: NavigationMode, cx: &mut WindowContext) -> Option<NavigationEntry> {
3138 let mut state = self.0.lock();
3139 let entry = match mode {
3140 NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
3141 return None
3142 }
3143 NavigationMode::GoingBack => &mut state.backward_stack,
3144 NavigationMode::GoingForward => &mut state.forward_stack,
3145 NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
3146 }
3147 .pop_back();
3148 if entry.is_some() {
3149 state.did_update(cx);
3150 }
3151 entry
3152 }
3153
3154 pub fn push<D: 'static + Send + Any>(
3155 &mut self,
3156 data: Option<D>,
3157 item: Arc<dyn WeakItemHandle>,
3158 is_preview: bool,
3159 cx: &mut WindowContext,
3160 ) {
3161 let state = &mut *self.0.lock();
3162 match state.mode {
3163 NavigationMode::Disabled => {}
3164 NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
3165 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3166 state.backward_stack.pop_front();
3167 }
3168 state.backward_stack.push_back(NavigationEntry {
3169 item,
3170 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3171 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3172 is_preview,
3173 });
3174 state.forward_stack.clear();
3175 }
3176 NavigationMode::GoingBack => {
3177 if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3178 state.forward_stack.pop_front();
3179 }
3180 state.forward_stack.push_back(NavigationEntry {
3181 item,
3182 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3183 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3184 is_preview,
3185 });
3186 }
3187 NavigationMode::GoingForward => {
3188 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3189 state.backward_stack.pop_front();
3190 }
3191 state.backward_stack.push_back(NavigationEntry {
3192 item,
3193 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3194 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3195 is_preview,
3196 });
3197 }
3198 NavigationMode::ClosingItem => {
3199 if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3200 state.closed_stack.pop_front();
3201 }
3202 state.closed_stack.push_back(NavigationEntry {
3203 item,
3204 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3205 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3206 is_preview,
3207 });
3208 }
3209 }
3210 state.did_update(cx);
3211 }
3212
3213 pub fn remove_item(&mut self, item_id: EntityId) {
3214 let mut state = self.0.lock();
3215 state.paths_by_item.remove(&item_id);
3216 state
3217 .backward_stack
3218 .retain(|entry| entry.item.id() != item_id);
3219 state
3220 .forward_stack
3221 .retain(|entry| entry.item.id() != item_id);
3222 state
3223 .closed_stack
3224 .retain(|entry| entry.item.id() != item_id);
3225 }
3226
3227 pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option<PathBuf>)> {
3228 self.0.lock().paths_by_item.get(&item_id).cloned()
3229 }
3230}
3231
3232impl NavHistoryState {
3233 pub fn did_update(&self, cx: &mut WindowContext) {
3234 if let Some(pane) = self.pane.upgrade() {
3235 cx.defer(move |cx| {
3236 pane.update(cx, |pane, cx| pane.history_updated(cx));
3237 });
3238 }
3239 }
3240}
3241
3242fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
3243 let path = buffer_path
3244 .as_ref()
3245 .and_then(|p| {
3246 p.path
3247 .to_str()
3248 .and_then(|s| if s.is_empty() { None } else { Some(s) })
3249 })
3250 .unwrap_or("This buffer");
3251 let path = truncate_and_remove_front(path, 80);
3252 format!("{path} contains unsaved edits. Do you want to save it?")
3253}
3254
3255pub fn tab_details(items: &[Box<dyn ItemHandle>], cx: &AppContext) -> Vec<usize> {
3256 let mut tab_details = items.iter().map(|_| 0).collect::<Vec<_>>();
3257 let mut tab_descriptions = HashMap::default();
3258 let mut done = false;
3259 while !done {
3260 done = true;
3261
3262 // Store item indices by their tab description.
3263 for (ix, (item, detail)) in items.iter().zip(&tab_details).enumerate() {
3264 if let Some(description) = item.tab_description(*detail, cx) {
3265 if *detail == 0
3266 || Some(&description) != item.tab_description(detail - 1, cx).as_ref()
3267 {
3268 tab_descriptions
3269 .entry(description)
3270 .or_insert(Vec::new())
3271 .push(ix);
3272 }
3273 }
3274 }
3275
3276 // If two or more items have the same tab description, increase their level
3277 // of detail and try again.
3278 for (_, item_ixs) in tab_descriptions.drain() {
3279 if item_ixs.len() > 1 {
3280 done = false;
3281 for ix in item_ixs {
3282 tab_details[ix] += 1;
3283 }
3284 }
3285 }
3286 }
3287
3288 tab_details
3289}
3290
3291pub fn render_item_indicator(item: Box<dyn ItemHandle>, cx: &WindowContext) -> Option<Indicator> {
3292 maybe!({
3293 let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
3294 (true, _) => Color::Warning,
3295 (_, true) => Color::Accent,
3296 (false, false) => return None,
3297 };
3298
3299 Some(Indicator::dot().color(indicator_color))
3300 })
3301}
3302
3303impl Render for DraggedTab {
3304 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3305 let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
3306 let label = self.item.tab_content(
3307 TabContentParams {
3308 detail: Some(self.detail),
3309 selected: false,
3310 preview: false,
3311 },
3312 cx,
3313 );
3314 Tab::new("")
3315 .toggle_state(self.is_active)
3316 .child(label)
3317 .render(cx)
3318 .font(ui_font)
3319 }
3320}
3321
3322#[cfg(test)]
3323mod tests {
3324 use std::num::NonZero;
3325
3326 use super::*;
3327 use crate::item::test::{TestItem, TestProjectItem};
3328 use gpui::{TestAppContext, VisualTestContext};
3329 use project::FakeFs;
3330 use settings::SettingsStore;
3331 use theme::LoadThemes;
3332
3333 #[gpui::test]
3334 async fn test_remove_active_empty(cx: &mut TestAppContext) {
3335 init_test(cx);
3336 let fs = FakeFs::new(cx.executor());
3337
3338 let project = Project::test(fs, None, cx).await;
3339 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3340 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3341
3342 pane.update(cx, |pane, cx| {
3343 assert!(pane
3344 .close_active_item(&CloseActiveItem { save_intent: None }, cx)
3345 .is_none())
3346 });
3347 }
3348
3349 #[gpui::test]
3350 async fn test_add_item_capped_to_max_tabs(cx: &mut TestAppContext) {
3351 init_test(cx);
3352 let fs = FakeFs::new(cx.executor());
3353
3354 let project = Project::test(fs, None, cx).await;
3355 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3356 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3357
3358 for i in 0..7 {
3359 add_labeled_item(&pane, format!("{}", i).as_str(), false, cx);
3360 }
3361 set_max_tabs(cx, Some(5));
3362 add_labeled_item(&pane, "7", false, cx);
3363 // Remove items to respect the max tab cap.
3364 assert_item_labels(&pane, ["3", "4", "5", "6", "7*"], cx);
3365 pane.update(cx, |pane, cx| {
3366 pane.activate_item(0, false, false, cx);
3367 });
3368 add_labeled_item(&pane, "X", false, cx);
3369 // Respect activation order.
3370 assert_item_labels(&pane, ["3", "X*", "5", "6", "7"], cx);
3371
3372 for i in 0..7 {
3373 add_labeled_item(&pane, format!("D{}", i).as_str(), true, cx);
3374 }
3375 // Keeps dirty items, even over max tab cap.
3376 assert_item_labels(
3377 &pane,
3378 ["D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6*^"],
3379 cx,
3380 );
3381
3382 set_max_tabs(cx, None);
3383 for i in 0..7 {
3384 add_labeled_item(&pane, format!("N{}", i).as_str(), false, cx);
3385 }
3386 // No cap when max tabs is None.
3387 assert_item_labels(
3388 &pane,
3389 [
3390 "D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6^", "N0", "N1", "N2", "N3", "N4",
3391 "N5", "N6*",
3392 ],
3393 cx,
3394 );
3395 }
3396
3397 #[gpui::test]
3398 async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
3399 init_test(cx);
3400 let fs = FakeFs::new(cx.executor());
3401
3402 let project = Project::test(fs, None, cx).await;
3403 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3404 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3405
3406 // 1. Add with a destination index
3407 // a. Add before the active item
3408 set_labeled_items(&pane, ["A", "B*", "C"], cx);
3409 pane.update(cx, |pane, cx| {
3410 pane.add_item(
3411 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3412 false,
3413 false,
3414 Some(0),
3415 cx,
3416 );
3417 });
3418 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
3419
3420 // b. Add after the active item
3421 set_labeled_items(&pane, ["A", "B*", "C"], cx);
3422 pane.update(cx, |pane, cx| {
3423 pane.add_item(
3424 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3425 false,
3426 false,
3427 Some(2),
3428 cx,
3429 );
3430 });
3431 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3432
3433 // c. Add at the end of the item list (including off the length)
3434 set_labeled_items(&pane, ["A", "B*", "C"], cx);
3435 pane.update(cx, |pane, cx| {
3436 pane.add_item(
3437 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3438 false,
3439 false,
3440 Some(5),
3441 cx,
3442 );
3443 });
3444 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3445
3446 // 2. Add without a destination index
3447 // a. Add with active item at the start of the item list
3448 set_labeled_items(&pane, ["A*", "B", "C"], cx);
3449 pane.update(cx, |pane, cx| {
3450 pane.add_item(
3451 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3452 false,
3453 false,
3454 None,
3455 cx,
3456 );
3457 });
3458 set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
3459
3460 // b. Add with active item at the end of the item list
3461 set_labeled_items(&pane, ["A", "B", "C*"], cx);
3462 pane.update(cx, |pane, cx| {
3463 pane.add_item(
3464 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3465 false,
3466 false,
3467 None,
3468 cx,
3469 );
3470 });
3471 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3472 }
3473
3474 #[gpui::test]
3475 async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
3476 init_test(cx);
3477 let fs = FakeFs::new(cx.executor());
3478
3479 let project = Project::test(fs, None, cx).await;
3480 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3481 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3482
3483 // 1. Add with a destination index
3484 // 1a. Add before the active item
3485 let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3486 pane.update(cx, |pane, cx| {
3487 pane.add_item(d, false, false, Some(0), cx);
3488 });
3489 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
3490
3491 // 1b. Add after the active item
3492 let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3493 pane.update(cx, |pane, cx| {
3494 pane.add_item(d, false, false, Some(2), cx);
3495 });
3496 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3497
3498 // 1c. Add at the end of the item list (including off the length)
3499 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3500 pane.update(cx, |pane, cx| {
3501 pane.add_item(a, false, false, Some(5), cx);
3502 });
3503 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3504
3505 // 1d. Add same item to active index
3506 let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3507 pane.update(cx, |pane, cx| {
3508 pane.add_item(b, false, false, Some(1), cx);
3509 });
3510 assert_item_labels(&pane, ["A", "B*", "C"], cx);
3511
3512 // 1e. Add item to index after same item in last position
3513 let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3514 pane.update(cx, |pane, cx| {
3515 pane.add_item(c, false, false, Some(2), cx);
3516 });
3517 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3518
3519 // 2. Add without a destination index
3520 // 2a. Add with active item at the start of the item list
3521 let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
3522 pane.update(cx, |pane, cx| {
3523 pane.add_item(d, false, false, None, cx);
3524 });
3525 assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
3526
3527 // 2b. Add with active item at the end of the item list
3528 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
3529 pane.update(cx, |pane, cx| {
3530 pane.add_item(a, false, false, None, cx);
3531 });
3532 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3533
3534 // 2c. Add active item to active item at end of list
3535 let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
3536 pane.update(cx, |pane, cx| {
3537 pane.add_item(c, false, false, None, cx);
3538 });
3539 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3540
3541 // 2d. Add active item to active item at start of list
3542 let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
3543 pane.update(cx, |pane, cx| {
3544 pane.add_item(a, false, false, None, cx);
3545 });
3546 assert_item_labels(&pane, ["A*", "B", "C"], cx);
3547 }
3548
3549 #[gpui::test]
3550 async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
3551 init_test(cx);
3552 let fs = FakeFs::new(cx.executor());
3553
3554 let project = Project::test(fs, None, cx).await;
3555 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3556 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3557
3558 // singleton view
3559 pane.update(cx, |pane, cx| {
3560 pane.add_item(
3561 Box::new(cx.new_view(|cx| {
3562 TestItem::new(cx)
3563 .with_singleton(true)
3564 .with_label("buffer 1")
3565 .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
3566 })),
3567 false,
3568 false,
3569 None,
3570 cx,
3571 );
3572 });
3573 assert_item_labels(&pane, ["buffer 1*"], cx);
3574
3575 // new singleton view with the same project entry
3576 pane.update(cx, |pane, cx| {
3577 pane.add_item(
3578 Box::new(cx.new_view(|cx| {
3579 TestItem::new(cx)
3580 .with_singleton(true)
3581 .with_label("buffer 1")
3582 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3583 })),
3584 false,
3585 false,
3586 None,
3587 cx,
3588 );
3589 });
3590 assert_item_labels(&pane, ["buffer 1*"], cx);
3591
3592 // new singleton view with different project entry
3593 pane.update(cx, |pane, cx| {
3594 pane.add_item(
3595 Box::new(cx.new_view(|cx| {
3596 TestItem::new(cx)
3597 .with_singleton(true)
3598 .with_label("buffer 2")
3599 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
3600 })),
3601 false,
3602 false,
3603 None,
3604 cx,
3605 );
3606 });
3607 assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
3608
3609 // new multibuffer view with the same project entry
3610 pane.update(cx, |pane, cx| {
3611 pane.add_item(
3612 Box::new(cx.new_view(|cx| {
3613 TestItem::new(cx)
3614 .with_singleton(false)
3615 .with_label("multibuffer 1")
3616 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3617 })),
3618 false,
3619 false,
3620 None,
3621 cx,
3622 );
3623 });
3624 assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
3625
3626 // another multibuffer view with the same project entry
3627 pane.update(cx, |pane, cx| {
3628 pane.add_item(
3629 Box::new(cx.new_view(|cx| {
3630 TestItem::new(cx)
3631 .with_singleton(false)
3632 .with_label("multibuffer 1b")
3633 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3634 })),
3635 false,
3636 false,
3637 None,
3638 cx,
3639 );
3640 });
3641 assert_item_labels(
3642 &pane,
3643 ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
3644 cx,
3645 );
3646 }
3647
3648 #[gpui::test]
3649 async fn test_remove_item_ordering_history(cx: &mut TestAppContext) {
3650 init_test(cx);
3651 let fs = FakeFs::new(cx.executor());
3652
3653 let project = Project::test(fs, None, cx).await;
3654 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3655 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3656
3657 add_labeled_item(&pane, "A", false, cx);
3658 add_labeled_item(&pane, "B", false, cx);
3659 add_labeled_item(&pane, "C", false, cx);
3660 add_labeled_item(&pane, "D", false, cx);
3661 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3662
3663 pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
3664 add_labeled_item(&pane, "1", false, cx);
3665 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
3666
3667 pane.update(cx, |pane, cx| {
3668 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3669 })
3670 .unwrap()
3671 .await
3672 .unwrap();
3673 assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
3674
3675 pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
3676 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3677
3678 pane.update(cx, |pane, cx| {
3679 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3680 })
3681 .unwrap()
3682 .await
3683 .unwrap();
3684 assert_item_labels(&pane, ["A", "B*", "C"], cx);
3685
3686 pane.update(cx, |pane, cx| {
3687 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3688 })
3689 .unwrap()
3690 .await
3691 .unwrap();
3692 assert_item_labels(&pane, ["A", "C*"], cx);
3693
3694 pane.update(cx, |pane, cx| {
3695 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3696 })
3697 .unwrap()
3698 .await
3699 .unwrap();
3700 assert_item_labels(&pane, ["A*"], cx);
3701 }
3702
3703 #[gpui::test]
3704 async fn test_remove_item_ordering_neighbour(cx: &mut TestAppContext) {
3705 init_test(cx);
3706 cx.update_global::<SettingsStore, ()>(|s, cx| {
3707 s.update_user_settings::<ItemSettings>(cx, |s| {
3708 s.activate_on_close = Some(ActivateOnClose::Neighbour);
3709 });
3710 });
3711 let fs = FakeFs::new(cx.executor());
3712
3713 let project = Project::test(fs, None, cx).await;
3714 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3715 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3716
3717 add_labeled_item(&pane, "A", false, cx);
3718 add_labeled_item(&pane, "B", false, cx);
3719 add_labeled_item(&pane, "C", false, cx);
3720 add_labeled_item(&pane, "D", false, cx);
3721 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3722
3723 pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
3724 add_labeled_item(&pane, "1", false, cx);
3725 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
3726
3727 pane.update(cx, |pane, cx| {
3728 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3729 })
3730 .unwrap()
3731 .await
3732 .unwrap();
3733 assert_item_labels(&pane, ["A", "B", "C*", "D"], cx);
3734
3735 pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
3736 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3737
3738 pane.update(cx, |pane, cx| {
3739 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3740 })
3741 .unwrap()
3742 .await
3743 .unwrap();
3744 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3745
3746 pane.update(cx, |pane, cx| {
3747 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3748 })
3749 .unwrap()
3750 .await
3751 .unwrap();
3752 assert_item_labels(&pane, ["A", "B*"], cx);
3753
3754 pane.update(cx, |pane, cx| {
3755 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3756 })
3757 .unwrap()
3758 .await
3759 .unwrap();
3760 assert_item_labels(&pane, ["A*"], cx);
3761 }
3762
3763 #[gpui::test]
3764 async fn test_remove_item_ordering_left_neighbour(cx: &mut TestAppContext) {
3765 init_test(cx);
3766 cx.update_global::<SettingsStore, ()>(|s, cx| {
3767 s.update_user_settings::<ItemSettings>(cx, |s| {
3768 s.activate_on_close = Some(ActivateOnClose::LeftNeighbour);
3769 });
3770 });
3771 let fs = FakeFs::new(cx.executor());
3772
3773 let project = Project::test(fs, None, cx).await;
3774 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3775 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3776
3777 add_labeled_item(&pane, "A", false, cx);
3778 add_labeled_item(&pane, "B", false, cx);
3779 add_labeled_item(&pane, "C", false, cx);
3780 add_labeled_item(&pane, "D", false, cx);
3781 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3782
3783 pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
3784 add_labeled_item(&pane, "1", false, cx);
3785 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
3786
3787 pane.update(cx, |pane, cx| {
3788 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3789 })
3790 .unwrap()
3791 .await
3792 .unwrap();
3793 assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
3794
3795 pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
3796 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3797
3798 pane.update(cx, |pane, cx| {
3799 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3800 })
3801 .unwrap()
3802 .await
3803 .unwrap();
3804 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3805
3806 pane.update(cx, |pane, cx| pane.activate_item(0, false, false, cx));
3807 assert_item_labels(&pane, ["A*", "B", "C"], cx);
3808
3809 pane.update(cx, |pane, cx| {
3810 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3811 })
3812 .unwrap()
3813 .await
3814 .unwrap();
3815 assert_item_labels(&pane, ["B*", "C"], cx);
3816
3817 pane.update(cx, |pane, cx| {
3818 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3819 })
3820 .unwrap()
3821 .await
3822 .unwrap();
3823 assert_item_labels(&pane, ["C*"], cx);
3824 }
3825
3826 #[gpui::test]
3827 async fn test_close_inactive_items(cx: &mut TestAppContext) {
3828 init_test(cx);
3829 let fs = FakeFs::new(cx.executor());
3830
3831 let project = Project::test(fs, None, cx).await;
3832 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3833 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3834
3835 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
3836
3837 pane.update(cx, |pane, cx| {
3838 pane.close_inactive_items(
3839 &CloseInactiveItems {
3840 save_intent: None,
3841 close_pinned: false,
3842 },
3843 cx,
3844 )
3845 })
3846 .unwrap()
3847 .await
3848 .unwrap();
3849 assert_item_labels(&pane, ["C*"], cx);
3850 }
3851
3852 #[gpui::test]
3853 async fn test_close_clean_items(cx: &mut TestAppContext) {
3854 init_test(cx);
3855 let fs = FakeFs::new(cx.executor());
3856
3857 let project = Project::test(fs, None, cx).await;
3858 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3859 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3860
3861 add_labeled_item(&pane, "A", true, cx);
3862 add_labeled_item(&pane, "B", false, cx);
3863 add_labeled_item(&pane, "C", true, cx);
3864 add_labeled_item(&pane, "D", false, cx);
3865 add_labeled_item(&pane, "E", false, cx);
3866 assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
3867
3868 pane.update(cx, |pane, cx| {
3869 pane.close_clean_items(
3870 &CloseCleanItems {
3871 close_pinned: false,
3872 },
3873 cx,
3874 )
3875 })
3876 .unwrap()
3877 .await
3878 .unwrap();
3879 assert_item_labels(&pane, ["A^", "C*^"], cx);
3880 }
3881
3882 #[gpui::test]
3883 async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
3884 init_test(cx);
3885 let fs = FakeFs::new(cx.executor());
3886
3887 let project = Project::test(fs, None, cx).await;
3888 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3889 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3890
3891 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
3892
3893 pane.update(cx, |pane, cx| {
3894 pane.close_items_to_the_left(
3895 &CloseItemsToTheLeft {
3896 close_pinned: false,
3897 },
3898 cx,
3899 )
3900 })
3901 .unwrap()
3902 .await
3903 .unwrap();
3904 assert_item_labels(&pane, ["C*", "D", "E"], cx);
3905 }
3906
3907 #[gpui::test]
3908 async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
3909 init_test(cx);
3910 let fs = FakeFs::new(cx.executor());
3911
3912 let project = Project::test(fs, None, cx).await;
3913 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3914 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3915
3916 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
3917
3918 pane.update(cx, |pane, cx| {
3919 pane.close_items_to_the_right(
3920 &CloseItemsToTheRight {
3921 close_pinned: false,
3922 },
3923 cx,
3924 )
3925 })
3926 .unwrap()
3927 .await
3928 .unwrap();
3929 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3930 }
3931
3932 #[gpui::test]
3933 async fn test_close_all_items(cx: &mut TestAppContext) {
3934 init_test(cx);
3935 let fs = FakeFs::new(cx.executor());
3936
3937 let project = Project::test(fs, None, cx).await;
3938 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3939 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3940
3941 let item_a = add_labeled_item(&pane, "A", false, cx);
3942 add_labeled_item(&pane, "B", false, cx);
3943 add_labeled_item(&pane, "C", false, cx);
3944 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3945
3946 pane.update(cx, |pane, cx| {
3947 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
3948 pane.pin_tab_at(ix, cx);
3949 pane.close_all_items(
3950 &CloseAllItems {
3951 save_intent: None,
3952 close_pinned: false,
3953 },
3954 cx,
3955 )
3956 })
3957 .unwrap()
3958 .await
3959 .unwrap();
3960 assert_item_labels(&pane, ["A*"], cx);
3961
3962 pane.update(cx, |pane, cx| {
3963 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
3964 pane.unpin_tab_at(ix, cx);
3965 pane.close_all_items(
3966 &CloseAllItems {
3967 save_intent: None,
3968 close_pinned: false,
3969 },
3970 cx,
3971 )
3972 })
3973 .unwrap()
3974 .await
3975 .unwrap();
3976
3977 assert_item_labels(&pane, [], cx);
3978
3979 add_labeled_item(&pane, "A", true, cx).update(cx, |item, cx| {
3980 item.project_items
3981 .push(TestProjectItem::new(1, "A.txt", cx))
3982 });
3983 add_labeled_item(&pane, "B", true, cx).update(cx, |item, cx| {
3984 item.project_items
3985 .push(TestProjectItem::new(2, "B.txt", cx))
3986 });
3987 add_labeled_item(&pane, "C", true, cx).update(cx, |item, cx| {
3988 item.project_items
3989 .push(TestProjectItem::new(3, "C.txt", cx))
3990 });
3991 assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
3992
3993 let save = pane
3994 .update(cx, |pane, cx| {
3995 pane.close_all_items(
3996 &CloseAllItems {
3997 save_intent: None,
3998 close_pinned: false,
3999 },
4000 cx,
4001 )
4002 })
4003 .unwrap();
4004
4005 cx.executor().run_until_parked();
4006 cx.simulate_prompt_answer(2);
4007 save.await.unwrap();
4008 assert_item_labels(&pane, [], cx);
4009
4010 add_labeled_item(&pane, "A", true, cx);
4011 add_labeled_item(&pane, "B", true, cx);
4012 add_labeled_item(&pane, "C", true, cx);
4013 assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
4014 let save = pane
4015 .update(cx, |pane, cx| {
4016 pane.close_all_items(
4017 &CloseAllItems {
4018 save_intent: None,
4019 close_pinned: false,
4020 },
4021 cx,
4022 )
4023 })
4024 .unwrap();
4025
4026 cx.executor().run_until_parked();
4027 cx.simulate_prompt_answer(2);
4028 save.await.unwrap();
4029 assert_item_labels(&pane, [], cx);
4030 }
4031
4032 #[gpui::test]
4033 async fn test_close_all_items_including_pinned(cx: &mut TestAppContext) {
4034 init_test(cx);
4035 let fs = FakeFs::new(cx.executor());
4036
4037 let project = Project::test(fs, None, cx).await;
4038 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
4039 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4040
4041 let item_a = add_labeled_item(&pane, "A", false, cx);
4042 add_labeled_item(&pane, "B", false, cx);
4043 add_labeled_item(&pane, "C", false, cx);
4044 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4045
4046 pane.update(cx, |pane, cx| {
4047 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4048 pane.pin_tab_at(ix, cx);
4049 pane.close_all_items(
4050 &CloseAllItems {
4051 save_intent: None,
4052 close_pinned: true,
4053 },
4054 cx,
4055 )
4056 })
4057 .unwrap()
4058 .await
4059 .unwrap();
4060 assert_item_labels(&pane, [], cx);
4061 }
4062
4063 fn init_test(cx: &mut TestAppContext) {
4064 cx.update(|cx| {
4065 let settings_store = SettingsStore::test(cx);
4066 cx.set_global(settings_store);
4067 theme::init(LoadThemes::JustBase, cx);
4068 crate::init_settings(cx);
4069 Project::init_settings(cx);
4070 });
4071 }
4072
4073 fn set_max_tabs(cx: &mut TestAppContext, value: Option<usize>) {
4074 cx.update_global(|store: &mut SettingsStore, cx| {
4075 store.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4076 settings.max_tabs = value.map(|v| NonZero::new(v).unwrap())
4077 });
4078 });
4079 }
4080
4081 fn add_labeled_item(
4082 pane: &View<Pane>,
4083 label: &str,
4084 is_dirty: bool,
4085 cx: &mut VisualTestContext,
4086 ) -> Box<View<TestItem>> {
4087 pane.update(cx, |pane, cx| {
4088 let labeled_item = Box::new(
4089 cx.new_view(|cx| TestItem::new(cx).with_label(label).with_dirty(is_dirty)),
4090 );
4091 pane.add_item(labeled_item.clone(), false, false, None, cx);
4092 labeled_item
4093 })
4094 }
4095
4096 fn set_labeled_items<const COUNT: usize>(
4097 pane: &View<Pane>,
4098 labels: [&str; COUNT],
4099 cx: &mut VisualTestContext,
4100 ) -> [Box<View<TestItem>>; COUNT] {
4101 pane.update(cx, |pane, cx| {
4102 pane.items.clear();
4103 let mut active_item_index = 0;
4104
4105 let mut index = 0;
4106 let items = labels.map(|mut label| {
4107 if label.ends_with('*') {
4108 label = label.trim_end_matches('*');
4109 active_item_index = index;
4110 }
4111
4112 let labeled_item = Box::new(cx.new_view(|cx| TestItem::new(cx).with_label(label)));
4113 pane.add_item(labeled_item.clone(), false, false, None, cx);
4114 index += 1;
4115 labeled_item
4116 });
4117
4118 pane.activate_item(active_item_index, false, false, cx);
4119
4120 items
4121 })
4122 }
4123
4124 // Assert the item label, with the active item label suffixed with a '*'
4125 #[track_caller]
4126 fn assert_item_labels<const COUNT: usize>(
4127 pane: &View<Pane>,
4128 expected_states: [&str; COUNT],
4129 cx: &mut VisualTestContext,
4130 ) {
4131 let actual_states = pane.update(cx, |pane, cx| {
4132 pane.items
4133 .iter()
4134 .enumerate()
4135 .map(|(ix, item)| {
4136 let mut state = item
4137 .to_any()
4138 .downcast::<TestItem>()
4139 .unwrap()
4140 .read(cx)
4141 .label
4142 .clone();
4143 if ix == pane.active_item_index {
4144 state.push('*');
4145 }
4146 if item.is_dirty(cx) {
4147 state.push('^');
4148 }
4149 state
4150 })
4151 .collect::<Vec<_>>()
4152 });
4153 assert_eq!(
4154 actual_states, expected_states,
4155 "pane items do not match expectation"
4156 );
4157 }
4158}