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