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