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 self.workspace
1965 .update(cx, |_, cx| {
1966 cx.defer(move |_, cx| move_item(&pane, &pane, id, destination_index, cx));
1967 })
1968 .ok()?;
1969
1970 Some(())
1971 });
1972 }
1973
1974 fn unpin_tab_at(&mut self, ix: usize, cx: &mut ViewContext<Self>) {
1975 maybe!({
1976 let pane = cx.view().clone();
1977 self.pinned_tab_count = self.pinned_tab_count.checked_sub(1)?;
1978 let destination_index = self.pinned_tab_count;
1979
1980 let id = self.item_for_index(ix)?.item_id();
1981
1982 self.workspace
1983 .update(cx, |_, cx| {
1984 cx.defer(move |_, cx| move_item(&pane, &pane, id, destination_index, cx));
1985 })
1986 .ok()?;
1987
1988 Some(())
1989 });
1990 }
1991
1992 fn is_tab_pinned(&self, ix: usize) -> bool {
1993 self.pinned_tab_count > ix
1994 }
1995
1996 fn has_pinned_tabs(&self) -> bool {
1997 self.pinned_tab_count != 0
1998 }
1999
2000 fn render_tab(
2001 &self,
2002 ix: usize,
2003 item: &dyn ItemHandle,
2004 detail: usize,
2005 focus_handle: &FocusHandle,
2006 cx: &mut ViewContext<Pane>,
2007 ) -> impl IntoElement {
2008 let is_active = ix == self.active_item_index;
2009 let is_preview = self
2010 .preview_item_id
2011 .map(|id| id == item.item_id())
2012 .unwrap_or(false);
2013
2014 let label = item.tab_content(
2015 TabContentParams {
2016 detail: Some(detail),
2017 selected: is_active,
2018 preview: is_preview,
2019 },
2020 cx,
2021 );
2022
2023 let item_diagnostic = item
2024 .project_path(cx)
2025 .map_or(None, |project_path| self.diagnostics.get(&project_path));
2026
2027 let decorated_icon = item_diagnostic.map_or(None, |diagnostic| {
2028 let icon = match item.tab_icon(cx) {
2029 Some(icon) => icon,
2030 None => return None,
2031 };
2032
2033 let knockout_item_color = if is_active {
2034 cx.theme().colors().tab_active_background
2035 } else {
2036 cx.theme().colors().tab_bar_background
2037 };
2038
2039 let (icon_decoration, icon_color) = if matches!(diagnostic, &DiagnosticSeverity::ERROR)
2040 {
2041 (IconDecorationKind::X, Color::Error)
2042 } else {
2043 (IconDecorationKind::Triangle, Color::Warning)
2044 };
2045
2046 Some(DecoratedIcon::new(
2047 icon.size(IconSize::Small).color(Color::Muted),
2048 Some(
2049 IconDecoration::new(icon_decoration, knockout_item_color, cx)
2050 .color(icon_color.color(cx))
2051 .position(Point {
2052 x: px(-2.),
2053 y: px(-2.),
2054 }),
2055 ),
2056 ))
2057 });
2058
2059 let icon = if decorated_icon.is_none() {
2060 match item_diagnostic {
2061 Some(&DiagnosticSeverity::ERROR) => None,
2062 Some(&DiagnosticSeverity::WARNING) => None,
2063 _ => item.tab_icon(cx).map(|icon| icon.color(Color::Muted)),
2064 }
2065 .map(|icon| icon.size(IconSize::Small))
2066 } else {
2067 None
2068 };
2069
2070 let settings = ItemSettings::get_global(cx);
2071 let close_side = &settings.close_position;
2072 let always_show_close_button = settings.always_show_close_button;
2073 let indicator = render_item_indicator(item.boxed_clone(), cx);
2074 let item_id = item.item_id();
2075 let is_first_item = ix == 0;
2076 let is_last_item = ix == self.items.len() - 1;
2077 let is_pinned = self.is_tab_pinned(ix);
2078 let position_relative_to_active_item = ix.cmp(&self.active_item_index);
2079
2080 let tab = Tab::new(ix)
2081 .position(if is_first_item {
2082 TabPosition::First
2083 } else if is_last_item {
2084 TabPosition::Last
2085 } else {
2086 TabPosition::Middle(position_relative_to_active_item)
2087 })
2088 .close_side(match close_side {
2089 ClosePosition::Left => ui::TabCloseSide::Start,
2090 ClosePosition::Right => ui::TabCloseSide::End,
2091 })
2092 .toggle_state(is_active)
2093 .on_click(
2094 cx.listener(move |pane: &mut Self, _, cx| pane.activate_item(ix, true, true, cx)),
2095 )
2096 // TODO: This should be a click listener with the middle mouse button instead of a mouse down listener.
2097 .on_mouse_down(
2098 MouseButton::Middle,
2099 cx.listener(move |pane, _event, cx| {
2100 pane.close_item_by_id(item_id, SaveIntent::Close, cx)
2101 .detach_and_log_err(cx);
2102 }),
2103 )
2104 .on_mouse_down(
2105 MouseButton::Left,
2106 cx.listener(move |pane, event: &MouseDownEvent, cx| {
2107 if let Some(id) = pane.preview_item_id {
2108 if id == item_id && event.click_count > 1 {
2109 pane.set_preview_item_id(None, cx);
2110 }
2111 }
2112 }),
2113 )
2114 .on_drag(
2115 DraggedTab {
2116 item: item.boxed_clone(),
2117 pane: cx.view().clone(),
2118 detail,
2119 is_active,
2120 ix,
2121 },
2122 |tab, _, cx| cx.new_view(|_| tab.clone()),
2123 )
2124 .drag_over::<DraggedTab>(|tab, _, cx| {
2125 tab.bg(cx.theme().colors().drop_target_background)
2126 })
2127 .drag_over::<DraggedSelection>(|tab, _, cx| {
2128 tab.bg(cx.theme().colors().drop_target_background)
2129 })
2130 .when_some(self.can_drop_predicate.clone(), |this, p| {
2131 this.can_drop(move |a, cx| p(a, cx))
2132 })
2133 .on_drop(cx.listener(move |this, dragged_tab: &DraggedTab, cx| {
2134 this.drag_split_direction = None;
2135 this.handle_tab_drop(dragged_tab, ix, cx)
2136 }))
2137 .on_drop(cx.listener(move |this, selection: &DraggedSelection, cx| {
2138 this.drag_split_direction = None;
2139 this.handle_dragged_selection_drop(selection, Some(ix), cx)
2140 }))
2141 .on_drop(cx.listener(move |this, paths, cx| {
2142 this.drag_split_direction = None;
2143 this.handle_external_paths_drop(paths, cx)
2144 }))
2145 .when_some(item.tab_tooltip_text(cx), |tab, text| {
2146 tab.tooltip(move |cx| Tooltip::text(text.clone(), cx))
2147 })
2148 .start_slot::<Indicator>(indicator)
2149 .map(|this| {
2150 let end_slot_action: &'static dyn Action;
2151 let end_slot_tooltip_text: &'static str;
2152 let end_slot = if is_pinned {
2153 end_slot_action = &TogglePinTab;
2154 end_slot_tooltip_text = "Unpin Tab";
2155 IconButton::new("unpin tab", IconName::Pin)
2156 .shape(IconButtonShape::Square)
2157 .icon_color(Color::Muted)
2158 .size(ButtonSize::None)
2159 .icon_size(IconSize::XSmall)
2160 .on_click(cx.listener(move |pane, _, cx| {
2161 pane.unpin_tab_at(ix, cx);
2162 }))
2163 } else {
2164 end_slot_action = &CloseActiveItem { save_intent: None };
2165 end_slot_tooltip_text = "Close Tab";
2166 IconButton::new("close tab", IconName::Close)
2167 .when(!always_show_close_button, |button| {
2168 button.visible_on_hover("")
2169 })
2170 .shape(IconButtonShape::Square)
2171 .icon_color(Color::Muted)
2172 .size(ButtonSize::None)
2173 .icon_size(IconSize::XSmall)
2174 .on_click(cx.listener(move |pane, _, cx| {
2175 pane.close_item_by_id(item_id, SaveIntent::Close, cx)
2176 .detach_and_log_err(cx);
2177 }))
2178 }
2179 .map(|this| {
2180 if is_active {
2181 let focus_handle = focus_handle.clone();
2182 this.tooltip(move |cx| {
2183 Tooltip::for_action_in(
2184 end_slot_tooltip_text,
2185 end_slot_action,
2186 &focus_handle,
2187 cx,
2188 )
2189 })
2190 } else {
2191 this.tooltip(move |cx| Tooltip::text(end_slot_tooltip_text, cx))
2192 }
2193 });
2194 this.end_slot(end_slot)
2195 })
2196 .child(
2197 h_flex()
2198 .gap_1()
2199 .items_center()
2200 .children(
2201 std::iter::once(if let Some(decorated_icon) = decorated_icon {
2202 Some(div().child(decorated_icon.into_any_element()))
2203 } else if let Some(icon) = icon {
2204 Some(div().child(icon.into_any_element()))
2205 } else {
2206 None
2207 })
2208 .flatten(),
2209 )
2210 .child(label),
2211 );
2212
2213 let single_entry_to_resolve = {
2214 let item_entries = self.items[ix].project_entry_ids(cx);
2215 if item_entries.len() == 1 {
2216 Some(item_entries[0])
2217 } else {
2218 None
2219 }
2220 };
2221
2222 let is_pinned = self.is_tab_pinned(ix);
2223 let pane = cx.view().downgrade();
2224 let menu_context = item.focus_handle(cx);
2225 right_click_menu(ix).trigger(tab).menu(move |cx| {
2226 let pane = pane.clone();
2227 let menu_context = menu_context.clone();
2228 ContextMenu::build(cx, move |mut menu, cx| {
2229 if let Some(pane) = pane.upgrade() {
2230 menu = menu
2231 .entry(
2232 "Close",
2233 Some(Box::new(CloseActiveItem { save_intent: None })),
2234 cx.handler_for(&pane, move |pane, cx| {
2235 pane.close_item_by_id(item_id, SaveIntent::Close, cx)
2236 .detach_and_log_err(cx);
2237 }),
2238 )
2239 .entry(
2240 "Close Others",
2241 Some(Box::new(CloseInactiveItems {
2242 save_intent: None,
2243 close_pinned: false,
2244 })),
2245 cx.handler_for(&pane, move |pane, cx| {
2246 pane.close_items(cx, SaveIntent::Close, |id| id != item_id)
2247 .detach_and_log_err(cx);
2248 }),
2249 )
2250 .separator()
2251 .entry(
2252 "Close Left",
2253 Some(Box::new(CloseItemsToTheLeft {
2254 close_pinned: false,
2255 })),
2256 cx.handler_for(&pane, move |pane, cx| {
2257 pane.close_items_to_the_left_by_id(
2258 item_id,
2259 &CloseItemsToTheLeft {
2260 close_pinned: false,
2261 },
2262 pane.get_non_closeable_item_ids(false),
2263 cx,
2264 )
2265 .detach_and_log_err(cx);
2266 }),
2267 )
2268 .entry(
2269 "Close Right",
2270 Some(Box::new(CloseItemsToTheRight {
2271 close_pinned: false,
2272 })),
2273 cx.handler_for(&pane, move |pane, cx| {
2274 pane.close_items_to_the_right_by_id(
2275 item_id,
2276 &CloseItemsToTheRight {
2277 close_pinned: false,
2278 },
2279 pane.get_non_closeable_item_ids(false),
2280 cx,
2281 )
2282 .detach_and_log_err(cx);
2283 }),
2284 )
2285 .separator()
2286 .entry(
2287 "Close Clean",
2288 Some(Box::new(CloseCleanItems {
2289 close_pinned: false,
2290 })),
2291 cx.handler_for(&pane, move |pane, cx| {
2292 if let Some(task) = pane.close_clean_items(
2293 &CloseCleanItems {
2294 close_pinned: false,
2295 },
2296 cx,
2297 ) {
2298 task.detach_and_log_err(cx)
2299 }
2300 }),
2301 )
2302 .entry(
2303 "Close All",
2304 Some(Box::new(CloseAllItems {
2305 save_intent: None,
2306 close_pinned: false,
2307 })),
2308 cx.handler_for(&pane, |pane, cx| {
2309 if let Some(task) = pane.close_all_items(
2310 &CloseAllItems {
2311 save_intent: None,
2312 close_pinned: false,
2313 },
2314 cx,
2315 ) {
2316 task.detach_and_log_err(cx)
2317 }
2318 }),
2319 );
2320
2321 let pin_tab_entries = |menu: ContextMenu| {
2322 menu.separator().map(|this| {
2323 if is_pinned {
2324 this.entry(
2325 "Unpin Tab",
2326 Some(TogglePinTab.boxed_clone()),
2327 cx.handler_for(&pane, move |pane, cx| {
2328 pane.unpin_tab_at(ix, cx);
2329 }),
2330 )
2331 } else {
2332 this.entry(
2333 "Pin Tab",
2334 Some(TogglePinTab.boxed_clone()),
2335 cx.handler_for(&pane, move |pane, cx| {
2336 pane.pin_tab_at(ix, cx);
2337 }),
2338 )
2339 }
2340 })
2341 };
2342 if let Some(entry) = single_entry_to_resolve {
2343 let entry_abs_path = pane.read(cx).entry_abs_path(entry, cx);
2344 let parent_abs_path = entry_abs_path
2345 .as_deref()
2346 .and_then(|abs_path| Some(abs_path.parent()?.to_path_buf()));
2347 let relative_path = pane
2348 .read(cx)
2349 .item_for_entry(entry, cx)
2350 .and_then(|item| item.project_path(cx))
2351 .map(|project_path| project_path.path);
2352
2353 let entry_id = entry.to_proto();
2354 menu = menu
2355 .separator()
2356 .when_some(entry_abs_path, |menu, abs_path| {
2357 menu.entry(
2358 "Copy Path",
2359 Some(Box::new(CopyPath)),
2360 cx.handler_for(&pane, move |_, cx| {
2361 cx.write_to_clipboard(ClipboardItem::new_string(
2362 abs_path.to_string_lossy().to_string(),
2363 ));
2364 }),
2365 )
2366 })
2367 .when_some(relative_path, |menu, relative_path| {
2368 menu.entry(
2369 "Copy Relative Path",
2370 Some(Box::new(CopyRelativePath)),
2371 cx.handler_for(&pane, move |_, cx| {
2372 cx.write_to_clipboard(ClipboardItem::new_string(
2373 relative_path.to_string_lossy().to_string(),
2374 ));
2375 }),
2376 )
2377 })
2378 .map(pin_tab_entries)
2379 .separator()
2380 .entry(
2381 "Reveal In Project Panel",
2382 Some(Box::new(RevealInProjectPanel {
2383 entry_id: Some(entry_id),
2384 })),
2385 cx.handler_for(&pane, move |pane, cx| {
2386 pane.project
2387 .update(cx, |_, cx| {
2388 cx.emit(project::Event::RevealInProjectPanel(
2389 ProjectEntryId::from_proto(entry_id),
2390 ))
2391 })
2392 .ok();
2393 }),
2394 )
2395 .when_some(parent_abs_path, |menu, parent_abs_path| {
2396 menu.entry(
2397 "Open in Terminal",
2398 Some(Box::new(OpenInTerminal)),
2399 cx.handler_for(&pane, move |_, cx| {
2400 cx.dispatch_action(
2401 OpenTerminal {
2402 working_directory: parent_abs_path.clone(),
2403 }
2404 .boxed_clone(),
2405 );
2406 }),
2407 )
2408 });
2409 } else {
2410 menu = menu.map(pin_tab_entries);
2411 }
2412 }
2413
2414 menu.context(menu_context)
2415 })
2416 })
2417 }
2418
2419 fn render_tab_bar(&mut self, cx: &mut ViewContext<Pane>) -> impl IntoElement {
2420 let focus_handle = self.focus_handle.clone();
2421 let navigate_backward = IconButton::new("navigate_backward", IconName::ArrowLeft)
2422 .icon_size(IconSize::Small)
2423 .on_click({
2424 let view = cx.view().clone();
2425 move |_, cx| view.update(cx, Self::navigate_backward)
2426 })
2427 .disabled(!self.can_navigate_backward())
2428 .tooltip({
2429 let focus_handle = focus_handle.clone();
2430 move |cx| Tooltip::for_action_in("Go Back", &GoBack, &focus_handle, cx)
2431 });
2432
2433 let navigate_forward = IconButton::new("navigate_forward", IconName::ArrowRight)
2434 .icon_size(IconSize::Small)
2435 .on_click({
2436 let view = cx.view().clone();
2437 move |_, cx| view.update(cx, Self::navigate_forward)
2438 })
2439 .disabled(!self.can_navigate_forward())
2440 .tooltip({
2441 let focus_handle = focus_handle.clone();
2442 move |cx| Tooltip::for_action_in("Go Forward", &GoForward, &focus_handle, cx)
2443 });
2444
2445 let mut tab_items = self
2446 .items
2447 .iter()
2448 .enumerate()
2449 .zip(tab_details(&self.items, cx))
2450 .map(|((ix, item), detail)| self.render_tab(ix, &**item, detail, &focus_handle, cx))
2451 .collect::<Vec<_>>();
2452 let tab_count = tab_items.len();
2453 let unpinned_tabs = tab_items.split_off(self.pinned_tab_count);
2454 let pinned_tabs = tab_items;
2455 TabBar::new("tab_bar")
2456 .when(
2457 self.display_nav_history_buttons.unwrap_or_default(),
2458 |tab_bar| {
2459 tab_bar
2460 .start_child(navigate_backward)
2461 .start_child(navigate_forward)
2462 },
2463 )
2464 .map(|tab_bar| {
2465 let render_tab_buttons = self.render_tab_bar_buttons.clone();
2466 let (left_children, right_children) = render_tab_buttons(self, cx);
2467
2468 tab_bar
2469 .start_children(left_children)
2470 .end_children(right_children)
2471 })
2472 .children(pinned_tabs.len().ne(&0).then(|| {
2473 h_flex()
2474 .children(pinned_tabs)
2475 .border_r_2()
2476 .border_color(cx.theme().colors().border)
2477 }))
2478 .child(
2479 h_flex()
2480 .id("unpinned tabs")
2481 .overflow_x_scroll()
2482 .w_full()
2483 .track_scroll(&self.tab_bar_scroll_handle)
2484 .children(unpinned_tabs)
2485 .child(
2486 div()
2487 .id("tab_bar_drop_target")
2488 .min_w_6()
2489 // HACK: This empty child is currently necessary to force the drop target to appear
2490 // despite us setting a min width above.
2491 .child("")
2492 .h_full()
2493 .flex_grow()
2494 .drag_over::<DraggedTab>(|bar, _, cx| {
2495 bar.bg(cx.theme().colors().drop_target_background)
2496 })
2497 .drag_over::<DraggedSelection>(|bar, _, cx| {
2498 bar.bg(cx.theme().colors().drop_target_background)
2499 })
2500 .on_drop(cx.listener(move |this, dragged_tab: &DraggedTab, cx| {
2501 this.drag_split_direction = None;
2502 this.handle_tab_drop(dragged_tab, this.items.len(), cx)
2503 }))
2504 .on_drop(cx.listener(move |this, selection: &DraggedSelection, cx| {
2505 this.drag_split_direction = None;
2506 this.handle_project_entry_drop(
2507 &selection.active_selection.entry_id,
2508 Some(tab_count),
2509 cx,
2510 )
2511 }))
2512 .on_drop(cx.listener(move |this, paths, cx| {
2513 this.drag_split_direction = None;
2514 this.handle_external_paths_drop(paths, cx)
2515 }))
2516 .on_click(cx.listener(move |this, event: &ClickEvent, cx| {
2517 if event.up.click_count == 2 {
2518 cx.dispatch_action(
2519 this.double_click_dispatch_action.boxed_clone(),
2520 )
2521 }
2522 })),
2523 ),
2524 )
2525 }
2526
2527 pub fn render_menu_overlay(menu: &View<ContextMenu>) -> Div {
2528 div().absolute().bottom_0().right_0().size_0().child(
2529 deferred(anchored().anchor(Corner::TopRight).child(menu.clone())).with_priority(1),
2530 )
2531 }
2532
2533 pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
2534 self.zoomed = zoomed;
2535 cx.notify();
2536 }
2537
2538 pub fn is_zoomed(&self) -> bool {
2539 self.zoomed
2540 }
2541
2542 fn handle_drag_move<T: 'static>(
2543 &mut self,
2544 event: &DragMoveEvent<T>,
2545 cx: &mut ViewContext<Self>,
2546 ) {
2547 let can_split_predicate = self.can_split_predicate.take();
2548 let can_split = match &can_split_predicate {
2549 Some(can_split_predicate) => can_split_predicate(self, event.dragged_item(), cx),
2550 None => false,
2551 };
2552 self.can_split_predicate = can_split_predicate;
2553 if !can_split {
2554 return;
2555 }
2556
2557 let rect = event.bounds.size;
2558
2559 let size = event.bounds.size.width.min(event.bounds.size.height)
2560 * WorkspaceSettings::get_global(cx).drop_target_size;
2561
2562 let relative_cursor = Point::new(
2563 event.event.position.x - event.bounds.left(),
2564 event.event.position.y - event.bounds.top(),
2565 );
2566
2567 let direction = if relative_cursor.x < size
2568 || relative_cursor.x > rect.width - size
2569 || relative_cursor.y < size
2570 || relative_cursor.y > rect.height - size
2571 {
2572 [
2573 SplitDirection::Up,
2574 SplitDirection::Right,
2575 SplitDirection::Down,
2576 SplitDirection::Left,
2577 ]
2578 .iter()
2579 .min_by_key(|side| match side {
2580 SplitDirection::Up => relative_cursor.y,
2581 SplitDirection::Right => rect.width - relative_cursor.x,
2582 SplitDirection::Down => rect.height - relative_cursor.y,
2583 SplitDirection::Left => relative_cursor.x,
2584 })
2585 .cloned()
2586 } else {
2587 None
2588 };
2589
2590 if direction != self.drag_split_direction {
2591 self.drag_split_direction = direction;
2592 }
2593 }
2594
2595 fn handle_tab_drop(&mut self, dragged_tab: &DraggedTab, ix: usize, cx: &mut ViewContext<Self>) {
2596 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2597 if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_tab, cx) {
2598 return;
2599 }
2600 }
2601 let mut to_pane = cx.view().clone();
2602 let split_direction = self.drag_split_direction;
2603 let item_id = dragged_tab.item.item_id();
2604 if let Some(preview_item_id) = self.preview_item_id {
2605 if item_id == preview_item_id {
2606 self.set_preview_item_id(None, cx);
2607 }
2608 }
2609
2610 let from_pane = dragged_tab.pane.clone();
2611 self.workspace
2612 .update(cx, |_, cx| {
2613 cx.defer(move |workspace, cx| {
2614 if let Some(split_direction) = split_direction {
2615 to_pane = workspace.split_pane(to_pane, split_direction, cx);
2616 }
2617 let old_ix = from_pane.read(cx).index_for_item_id(item_id);
2618 let old_len = to_pane.read(cx).items.len();
2619 move_item(&from_pane, &to_pane, item_id, ix, cx);
2620 if to_pane == from_pane {
2621 if let Some(old_index) = old_ix {
2622 to_pane.update(cx, |this, _| {
2623 if old_index < this.pinned_tab_count
2624 && (ix == this.items.len() || ix > this.pinned_tab_count)
2625 {
2626 this.pinned_tab_count -= 1;
2627 } else if this.has_pinned_tabs()
2628 && old_index >= this.pinned_tab_count
2629 && ix < this.pinned_tab_count
2630 {
2631 this.pinned_tab_count += 1;
2632 }
2633 });
2634 }
2635 } else {
2636 to_pane.update(cx, |this, _| {
2637 if this.items.len() > old_len // Did we not deduplicate on drag?
2638 && this.has_pinned_tabs()
2639 && ix < this.pinned_tab_count
2640 {
2641 this.pinned_tab_count += 1;
2642 }
2643 });
2644 from_pane.update(cx, |this, _| {
2645 if let Some(index) = old_ix {
2646 if this.pinned_tab_count > index {
2647 this.pinned_tab_count -= 1;
2648 }
2649 }
2650 })
2651 }
2652 });
2653 })
2654 .log_err();
2655 }
2656
2657 fn handle_dragged_selection_drop(
2658 &mut self,
2659 dragged_selection: &DraggedSelection,
2660 dragged_onto: Option<usize>,
2661 cx: &mut ViewContext<Self>,
2662 ) {
2663 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2664 if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_selection, cx) {
2665 return;
2666 }
2667 }
2668 self.handle_project_entry_drop(
2669 &dragged_selection.active_selection.entry_id,
2670 dragged_onto,
2671 cx,
2672 );
2673 }
2674
2675 fn handle_project_entry_drop(
2676 &mut self,
2677 project_entry_id: &ProjectEntryId,
2678 target: Option<usize>,
2679 cx: &mut ViewContext<Self>,
2680 ) {
2681 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2682 if let ControlFlow::Break(()) = custom_drop_handle(self, project_entry_id, cx) {
2683 return;
2684 }
2685 }
2686 let mut to_pane = cx.view().clone();
2687 let split_direction = self.drag_split_direction;
2688 let project_entry_id = *project_entry_id;
2689 self.workspace
2690 .update(cx, |_, cx| {
2691 cx.defer(move |workspace, cx| {
2692 if let Some(path) = workspace
2693 .project()
2694 .read(cx)
2695 .path_for_entry(project_entry_id, cx)
2696 {
2697 let load_path_task = workspace.load_path(path, cx);
2698 cx.spawn(|workspace, mut cx| async move {
2699 if let Some((project_entry_id, build_item)) =
2700 load_path_task.await.notify_async_err(&mut cx)
2701 {
2702 let (to_pane, new_item_handle) = workspace
2703 .update(&mut cx, |workspace, cx| {
2704 if let Some(split_direction) = split_direction {
2705 to_pane =
2706 workspace.split_pane(to_pane, split_direction, cx);
2707 }
2708 let new_item_handle = to_pane.update(cx, |pane, cx| {
2709 pane.open_item(
2710 project_entry_id,
2711 true,
2712 false,
2713 target,
2714 cx,
2715 build_item,
2716 )
2717 });
2718 (to_pane, new_item_handle)
2719 })
2720 .log_err()?;
2721 to_pane
2722 .update(&mut cx, |this, cx| {
2723 let Some(index) = this.index_for_item(&*new_item_handle)
2724 else {
2725 return;
2726 };
2727
2728 if target.map_or(false, |target| this.is_tab_pinned(target))
2729 {
2730 this.pin_tab_at(index, cx);
2731 }
2732 })
2733 .ok()?
2734 }
2735 Some(())
2736 })
2737 .detach();
2738 };
2739 });
2740 })
2741 .log_err();
2742 }
2743
2744 fn handle_external_paths_drop(&mut self, paths: &ExternalPaths, cx: &mut ViewContext<Self>) {
2745 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2746 if let ControlFlow::Break(()) = custom_drop_handle(self, paths, cx) {
2747 return;
2748 }
2749 }
2750 let mut to_pane = cx.view().clone();
2751 let mut split_direction = self.drag_split_direction;
2752 let paths = paths.paths().to_vec();
2753 let is_remote = self
2754 .workspace
2755 .update(cx, |workspace, cx| {
2756 if workspace.project().read(cx).is_via_collab() {
2757 workspace.show_error(
2758 &anyhow::anyhow!("Cannot drop files on a remote project"),
2759 cx,
2760 );
2761 true
2762 } else {
2763 false
2764 }
2765 })
2766 .unwrap_or(true);
2767 if is_remote {
2768 return;
2769 }
2770
2771 self.workspace
2772 .update(cx, |workspace, cx| {
2773 let fs = Arc::clone(workspace.project().read(cx).fs());
2774 cx.spawn(|workspace, mut cx| async move {
2775 let mut is_file_checks = FuturesUnordered::new();
2776 for path in &paths {
2777 is_file_checks.push(fs.is_file(path))
2778 }
2779 let mut has_files_to_open = false;
2780 while let Some(is_file) = is_file_checks.next().await {
2781 if is_file {
2782 has_files_to_open = true;
2783 break;
2784 }
2785 }
2786 drop(is_file_checks);
2787 if !has_files_to_open {
2788 split_direction = None;
2789 }
2790
2791 if let Ok(open_task) = workspace.update(&mut cx, |workspace, cx| {
2792 if let Some(split_direction) = split_direction {
2793 to_pane = workspace.split_pane(to_pane, split_direction, cx);
2794 }
2795 workspace.open_paths(
2796 paths,
2797 OpenVisible::OnlyDirectories,
2798 Some(to_pane.downgrade()),
2799 cx,
2800 )
2801 }) {
2802 let opened_items: Vec<_> = open_task.await;
2803 _ = workspace.update(&mut cx, |workspace, cx| {
2804 for item in opened_items.into_iter().flatten() {
2805 if let Err(e) = item {
2806 workspace.show_error(&e, cx);
2807 }
2808 }
2809 });
2810 }
2811 })
2812 .detach();
2813 })
2814 .log_err();
2815 }
2816
2817 pub fn display_nav_history_buttons(&mut self, display: Option<bool>) {
2818 self.display_nav_history_buttons = display;
2819 }
2820
2821 fn get_non_closeable_item_ids(&self, close_pinned: bool) -> Vec<EntityId> {
2822 if close_pinned {
2823 return vec![];
2824 }
2825
2826 self.items
2827 .iter()
2828 .map(|item| item.item_id())
2829 .filter(|item_id| {
2830 if let Some(ix) = self.index_for_item_id(*item_id) {
2831 self.is_tab_pinned(ix)
2832 } else {
2833 true
2834 }
2835 })
2836 .collect()
2837 }
2838
2839 pub fn drag_split_direction(&self) -> Option<SplitDirection> {
2840 self.drag_split_direction
2841 }
2842
2843 pub fn set_zoom_out_on_close(&mut self, zoom_out_on_close: bool) {
2844 self.zoom_out_on_close = zoom_out_on_close;
2845 }
2846}
2847
2848impl FocusableView for Pane {
2849 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
2850 self.focus_handle.clone()
2851 }
2852}
2853
2854impl Render for Pane {
2855 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2856 let mut key_context = KeyContext::new_with_defaults();
2857 key_context.add("Pane");
2858 if self.active_item().is_none() {
2859 key_context.add("EmptyPane");
2860 }
2861
2862 let should_display_tab_bar = self.should_display_tab_bar.clone();
2863 let display_tab_bar = should_display_tab_bar(cx);
2864 let Some(project) = self.project.upgrade() else {
2865 return div().track_focus(&self.focus_handle(cx));
2866 };
2867 let is_local = project.read(cx).is_local();
2868
2869 v_flex()
2870 .key_context(key_context)
2871 .track_focus(&self.focus_handle(cx))
2872 .size_full()
2873 .flex_none()
2874 .overflow_hidden()
2875 .on_action(cx.listener(|pane, _: &AlternateFile, cx| {
2876 pane.alternate_file(cx);
2877 }))
2878 .on_action(cx.listener(|pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx)))
2879 .on_action(cx.listener(|pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx)))
2880 .on_action(cx.listener(|pane, _: &SplitHorizontal, cx| {
2881 pane.split(SplitDirection::horizontal(cx), cx)
2882 }))
2883 .on_action(cx.listener(|pane, _: &SplitVertical, cx| {
2884 pane.split(SplitDirection::vertical(cx), cx)
2885 }))
2886 .on_action(
2887 cx.listener(|pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx)),
2888 )
2889 .on_action(cx.listener(|pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx)))
2890 .on_action(cx.listener(|pane, _: &GoBack, cx| pane.navigate_backward(cx)))
2891 .on_action(cx.listener(|pane, _: &GoForward, cx| pane.navigate_forward(cx)))
2892 .on_action(cx.listener(|pane, _: &JoinIntoNext, cx| pane.join_into_next(cx)))
2893 .on_action(cx.listener(|pane, _: &JoinAll, cx| pane.join_all(cx)))
2894 .on_action(cx.listener(Pane::toggle_zoom))
2895 .on_action(cx.listener(|pane: &mut Pane, action: &ActivateItem, cx| {
2896 pane.activate_item(action.0, true, true, cx);
2897 }))
2898 .on_action(cx.listener(|pane: &mut Pane, _: &ActivateLastItem, cx| {
2899 pane.activate_item(pane.items.len() - 1, true, true, cx);
2900 }))
2901 .on_action(cx.listener(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
2902 pane.activate_prev_item(true, cx);
2903 }))
2904 .on_action(cx.listener(|pane: &mut Pane, _: &ActivateNextItem, cx| {
2905 pane.activate_next_item(true, cx);
2906 }))
2907 .on_action(cx.listener(|pane, _: &SwapItemLeft, cx| pane.swap_item_left(cx)))
2908 .on_action(cx.listener(|pane, _: &SwapItemRight, cx| pane.swap_item_right(cx)))
2909 .on_action(cx.listener(|pane, action, cx| {
2910 pane.toggle_pin_tab(action, cx);
2911 }))
2912 .when(PreviewTabsSettings::get_global(cx).enabled, |this| {
2913 this.on_action(cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, cx| {
2914 if let Some(active_item_id) = pane.active_item().map(|i| i.item_id()) {
2915 if pane.is_active_preview_item(active_item_id) {
2916 pane.set_preview_item_id(None, cx);
2917 } else {
2918 pane.set_preview_item_id(Some(active_item_id), cx);
2919 }
2920 }
2921 }))
2922 })
2923 .on_action(
2924 cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
2925 if let Some(task) = pane.close_active_item(action, cx) {
2926 task.detach_and_log_err(cx)
2927 }
2928 }),
2929 )
2930 .on_action(
2931 cx.listener(|pane: &mut Self, action: &CloseInactiveItems, cx| {
2932 if let Some(task) = pane.close_inactive_items(action, cx) {
2933 task.detach_and_log_err(cx)
2934 }
2935 }),
2936 )
2937 .on_action(
2938 cx.listener(|pane: &mut Self, action: &CloseCleanItems, cx| {
2939 if let Some(task) = pane.close_clean_items(action, cx) {
2940 task.detach_and_log_err(cx)
2941 }
2942 }),
2943 )
2944 .on_action(
2945 cx.listener(|pane: &mut Self, action: &CloseItemsToTheLeft, cx| {
2946 if let Some(task) = pane.close_items_to_the_left(action, cx) {
2947 task.detach_and_log_err(cx)
2948 }
2949 }),
2950 )
2951 .on_action(
2952 cx.listener(|pane: &mut Self, action: &CloseItemsToTheRight, cx| {
2953 if let Some(task) = pane.close_items_to_the_right(action, cx) {
2954 task.detach_and_log_err(cx)
2955 }
2956 }),
2957 )
2958 .on_action(cx.listener(|pane: &mut Self, action: &CloseAllItems, cx| {
2959 if let Some(task) = pane.close_all_items(action, cx) {
2960 task.detach_and_log_err(cx)
2961 }
2962 }))
2963 .on_action(
2964 cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
2965 if let Some(task) = pane.close_active_item(action, cx) {
2966 task.detach_and_log_err(cx)
2967 }
2968 }),
2969 )
2970 .on_action(
2971 cx.listener(|pane: &mut Self, action: &RevealInProjectPanel, cx| {
2972 let entry_id = action
2973 .entry_id
2974 .map(ProjectEntryId::from_proto)
2975 .or_else(|| pane.active_item()?.project_entry_ids(cx).first().copied());
2976 if let Some(entry_id) = entry_id {
2977 pane.project
2978 .update(cx, |_, cx| {
2979 cx.emit(project::Event::RevealInProjectPanel(entry_id))
2980 })
2981 .ok();
2982 }
2983 }),
2984 )
2985 .when(self.active_item().is_some() && display_tab_bar, |pane| {
2986 pane.child(self.render_tab_bar(cx))
2987 })
2988 .child({
2989 let has_worktrees = project.read(cx).worktrees(cx).next().is_some();
2990 // main content
2991 div()
2992 .flex_1()
2993 .relative()
2994 .group("")
2995 .overflow_hidden()
2996 .on_drag_move::<DraggedTab>(cx.listener(Self::handle_drag_move))
2997 .on_drag_move::<DraggedSelection>(cx.listener(Self::handle_drag_move))
2998 .when(is_local, |div| {
2999 div.on_drag_move::<ExternalPaths>(cx.listener(Self::handle_drag_move))
3000 })
3001 .map(|div| {
3002 if let Some(item) = self.active_item() {
3003 div.v_flex()
3004 .size_full()
3005 .overflow_hidden()
3006 .child(self.toolbar.clone())
3007 .child(item.to_any())
3008 } else {
3009 let placeholder = div.h_flex().size_full().justify_center();
3010 if has_worktrees {
3011 placeholder
3012 } else {
3013 placeholder.child(
3014 Label::new("Open a file or project to get started.")
3015 .color(Color::Muted),
3016 )
3017 }
3018 }
3019 })
3020 .child(
3021 // drag target
3022 div()
3023 .invisible()
3024 .absolute()
3025 .bg(cx.theme().colors().drop_target_background)
3026 .group_drag_over::<DraggedTab>("", |style| style.visible())
3027 .group_drag_over::<DraggedSelection>("", |style| style.visible())
3028 .when(is_local, |div| {
3029 div.group_drag_over::<ExternalPaths>("", |style| style.visible())
3030 })
3031 .when_some(self.can_drop_predicate.clone(), |this, p| {
3032 this.can_drop(move |a, cx| p(a, cx))
3033 })
3034 .on_drop(cx.listener(move |this, dragged_tab, cx| {
3035 this.handle_tab_drop(dragged_tab, this.active_item_index(), cx)
3036 }))
3037 .on_drop(cx.listener(move |this, selection: &DraggedSelection, cx| {
3038 this.handle_dragged_selection_drop(selection, None, cx)
3039 }))
3040 .on_drop(cx.listener(move |this, paths, cx| {
3041 this.handle_external_paths_drop(paths, cx)
3042 }))
3043 .map(|div| {
3044 let size = DefiniteLength::Fraction(0.5);
3045 match self.drag_split_direction {
3046 None => div.top_0().right_0().bottom_0().left_0(),
3047 Some(SplitDirection::Up) => {
3048 div.top_0().left_0().right_0().h(size)
3049 }
3050 Some(SplitDirection::Down) => {
3051 div.left_0().bottom_0().right_0().h(size)
3052 }
3053 Some(SplitDirection::Left) => {
3054 div.top_0().left_0().bottom_0().w(size)
3055 }
3056 Some(SplitDirection::Right) => {
3057 div.top_0().bottom_0().right_0().w(size)
3058 }
3059 }
3060 }),
3061 )
3062 })
3063 .on_mouse_down(
3064 MouseButton::Navigate(NavigationDirection::Back),
3065 cx.listener(|pane, _, cx| {
3066 if let Some(workspace) = pane.workspace.upgrade() {
3067 let pane = cx.view().downgrade();
3068 cx.window_context().defer(move |cx| {
3069 workspace.update(cx, |workspace, cx| {
3070 workspace.go_back(pane, cx).detach_and_log_err(cx)
3071 })
3072 })
3073 }
3074 }),
3075 )
3076 .on_mouse_down(
3077 MouseButton::Navigate(NavigationDirection::Forward),
3078 cx.listener(|pane, _, cx| {
3079 if let Some(workspace) = pane.workspace.upgrade() {
3080 let pane = cx.view().downgrade();
3081 cx.window_context().defer(move |cx| {
3082 workspace.update(cx, |workspace, cx| {
3083 workspace.go_forward(pane, cx).detach_and_log_err(cx)
3084 })
3085 })
3086 }
3087 }),
3088 )
3089 }
3090}
3091
3092impl ItemNavHistory {
3093 pub fn push<D: 'static + Send + Any>(&mut self, data: Option<D>, cx: &mut WindowContext) {
3094 self.history
3095 .push(data, self.item.clone(), self.is_preview, cx);
3096 }
3097
3098 pub fn pop_backward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
3099 self.history.pop(NavigationMode::GoingBack, cx)
3100 }
3101
3102 pub fn pop_forward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
3103 self.history.pop(NavigationMode::GoingForward, cx)
3104 }
3105}
3106
3107impl NavHistory {
3108 pub fn for_each_entry(
3109 &self,
3110 cx: &AppContext,
3111 mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
3112 ) {
3113 let borrowed_history = self.0.lock();
3114 borrowed_history
3115 .forward_stack
3116 .iter()
3117 .chain(borrowed_history.backward_stack.iter())
3118 .chain(borrowed_history.closed_stack.iter())
3119 .for_each(|entry| {
3120 if let Some(project_and_abs_path) =
3121 borrowed_history.paths_by_item.get(&entry.item.id())
3122 {
3123 f(entry, project_and_abs_path.clone());
3124 } else if let Some(item) = entry.item.upgrade() {
3125 if let Some(path) = item.project_path(cx) {
3126 f(entry, (path, None));
3127 }
3128 }
3129 })
3130 }
3131
3132 pub fn set_mode(&mut self, mode: NavigationMode) {
3133 self.0.lock().mode = mode;
3134 }
3135
3136 pub fn mode(&self) -> NavigationMode {
3137 self.0.lock().mode
3138 }
3139
3140 pub fn disable(&mut self) {
3141 self.0.lock().mode = NavigationMode::Disabled;
3142 }
3143
3144 pub fn enable(&mut self) {
3145 self.0.lock().mode = NavigationMode::Normal;
3146 }
3147
3148 pub fn pop(&mut self, mode: NavigationMode, cx: &mut WindowContext) -> Option<NavigationEntry> {
3149 let mut state = self.0.lock();
3150 let entry = match mode {
3151 NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
3152 return None
3153 }
3154 NavigationMode::GoingBack => &mut state.backward_stack,
3155 NavigationMode::GoingForward => &mut state.forward_stack,
3156 NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
3157 }
3158 .pop_back();
3159 if entry.is_some() {
3160 state.did_update(cx);
3161 }
3162 entry
3163 }
3164
3165 pub fn push<D: 'static + Send + Any>(
3166 &mut self,
3167 data: Option<D>,
3168 item: Arc<dyn WeakItemHandle>,
3169 is_preview: bool,
3170 cx: &mut WindowContext,
3171 ) {
3172 let state = &mut *self.0.lock();
3173 match state.mode {
3174 NavigationMode::Disabled => {}
3175 NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
3176 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3177 state.backward_stack.pop_front();
3178 }
3179 state.backward_stack.push_back(NavigationEntry {
3180 item,
3181 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3182 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3183 is_preview,
3184 });
3185 state.forward_stack.clear();
3186 }
3187 NavigationMode::GoingBack => {
3188 if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3189 state.forward_stack.pop_front();
3190 }
3191 state.forward_stack.push_back(NavigationEntry {
3192 item,
3193 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3194 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3195 is_preview,
3196 });
3197 }
3198 NavigationMode::GoingForward => {
3199 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3200 state.backward_stack.pop_front();
3201 }
3202 state.backward_stack.push_back(NavigationEntry {
3203 item,
3204 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3205 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3206 is_preview,
3207 });
3208 }
3209 NavigationMode::ClosingItem => {
3210 if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3211 state.closed_stack.pop_front();
3212 }
3213 state.closed_stack.push_back(NavigationEntry {
3214 item,
3215 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3216 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3217 is_preview,
3218 });
3219 }
3220 }
3221 state.did_update(cx);
3222 }
3223
3224 pub fn remove_item(&mut self, item_id: EntityId) {
3225 let mut state = self.0.lock();
3226 state.paths_by_item.remove(&item_id);
3227 state
3228 .backward_stack
3229 .retain(|entry| entry.item.id() != item_id);
3230 state
3231 .forward_stack
3232 .retain(|entry| entry.item.id() != item_id);
3233 state
3234 .closed_stack
3235 .retain(|entry| entry.item.id() != item_id);
3236 }
3237
3238 pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option<PathBuf>)> {
3239 self.0.lock().paths_by_item.get(&item_id).cloned()
3240 }
3241}
3242
3243impl NavHistoryState {
3244 pub fn did_update(&self, cx: &mut WindowContext) {
3245 if let Some(pane) = self.pane.upgrade() {
3246 cx.defer(move |cx| {
3247 pane.update(cx, |pane, cx| pane.history_updated(cx));
3248 });
3249 }
3250 }
3251}
3252
3253fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
3254 let path = buffer_path
3255 .as_ref()
3256 .and_then(|p| {
3257 p.path
3258 .to_str()
3259 .and_then(|s| if s.is_empty() { None } else { Some(s) })
3260 })
3261 .unwrap_or("This buffer");
3262 let path = truncate_and_remove_front(path, 80);
3263 format!("{path} contains unsaved edits. Do you want to save it?")
3264}
3265
3266pub fn tab_details(items: &[Box<dyn ItemHandle>], cx: &AppContext) -> Vec<usize> {
3267 let mut tab_details = items.iter().map(|_| 0).collect::<Vec<_>>();
3268 let mut tab_descriptions = HashMap::default();
3269 let mut done = false;
3270 while !done {
3271 done = true;
3272
3273 // Store item indices by their tab description.
3274 for (ix, (item, detail)) in items.iter().zip(&tab_details).enumerate() {
3275 if let Some(description) = item.tab_description(*detail, cx) {
3276 if *detail == 0
3277 || Some(&description) != item.tab_description(detail - 1, cx).as_ref()
3278 {
3279 tab_descriptions
3280 .entry(description)
3281 .or_insert(Vec::new())
3282 .push(ix);
3283 }
3284 }
3285 }
3286
3287 // If two or more items have the same tab description, increase their level
3288 // of detail and try again.
3289 for (_, item_ixs) in tab_descriptions.drain() {
3290 if item_ixs.len() > 1 {
3291 done = false;
3292 for ix in item_ixs {
3293 tab_details[ix] += 1;
3294 }
3295 }
3296 }
3297 }
3298
3299 tab_details
3300}
3301
3302pub fn render_item_indicator(item: Box<dyn ItemHandle>, cx: &WindowContext) -> Option<Indicator> {
3303 maybe!({
3304 let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
3305 (true, _) => Color::Warning,
3306 (_, true) => Color::Accent,
3307 (false, false) => return None,
3308 };
3309
3310 Some(Indicator::dot().color(indicator_color))
3311 })
3312}
3313
3314impl Render for DraggedTab {
3315 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3316 let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
3317 let label = self.item.tab_content(
3318 TabContentParams {
3319 detail: Some(self.detail),
3320 selected: false,
3321 preview: false,
3322 },
3323 cx,
3324 );
3325 Tab::new("")
3326 .toggle_state(self.is_active)
3327 .child(label)
3328 .render(cx)
3329 .font(ui_font)
3330 }
3331}
3332
3333#[cfg(test)]
3334mod tests {
3335 use std::num::NonZero;
3336
3337 use super::*;
3338 use crate::item::test::{TestItem, TestProjectItem};
3339 use gpui::{TestAppContext, VisualTestContext};
3340 use project::FakeFs;
3341 use settings::SettingsStore;
3342 use theme::LoadThemes;
3343
3344 #[gpui::test]
3345 async fn test_remove_active_empty(cx: &mut TestAppContext) {
3346 init_test(cx);
3347 let fs = FakeFs::new(cx.executor());
3348
3349 let project = Project::test(fs, None, cx).await;
3350 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3351 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3352
3353 pane.update(cx, |pane, cx| {
3354 assert!(pane
3355 .close_active_item(&CloseActiveItem { save_intent: None }, cx)
3356 .is_none())
3357 });
3358 }
3359
3360 #[gpui::test]
3361 async fn test_add_item_capped_to_max_tabs(cx: &mut TestAppContext) {
3362 init_test(cx);
3363 let fs = FakeFs::new(cx.executor());
3364
3365 let project = Project::test(fs, None, cx).await;
3366 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3367 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3368
3369 for i in 0..7 {
3370 add_labeled_item(&pane, format!("{}", i).as_str(), false, cx);
3371 }
3372 set_max_tabs(cx, Some(5));
3373 add_labeled_item(&pane, "7", false, cx);
3374 // Remove items to respect the max tab cap.
3375 assert_item_labels(&pane, ["3", "4", "5", "6", "7*"], cx);
3376 pane.update(cx, |pane, cx| {
3377 pane.activate_item(0, false, false, cx);
3378 });
3379 add_labeled_item(&pane, "X", false, cx);
3380 // Respect activation order.
3381 assert_item_labels(&pane, ["3", "X*", "5", "6", "7"], cx);
3382
3383 for i in 0..7 {
3384 add_labeled_item(&pane, format!("D{}", i).as_str(), true, cx);
3385 }
3386 // Keeps dirty items, even over max tab cap.
3387 assert_item_labels(
3388 &pane,
3389 ["D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6*^"],
3390 cx,
3391 );
3392
3393 set_max_tabs(cx, None);
3394 for i in 0..7 {
3395 add_labeled_item(&pane, format!("N{}", i).as_str(), false, cx);
3396 }
3397 // No cap when max tabs is None.
3398 assert_item_labels(
3399 &pane,
3400 [
3401 "D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6^", "N0", "N1", "N2", "N3", "N4",
3402 "N5", "N6*",
3403 ],
3404 cx,
3405 );
3406 }
3407
3408 #[gpui::test]
3409 async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
3410 init_test(cx);
3411 let fs = FakeFs::new(cx.executor());
3412
3413 let project = Project::test(fs, None, cx).await;
3414 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3415 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3416
3417 // 1. Add with a destination index
3418 // a. Add before the active item
3419 set_labeled_items(&pane, ["A", "B*", "C"], cx);
3420 pane.update(cx, |pane, cx| {
3421 pane.add_item(
3422 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3423 false,
3424 false,
3425 Some(0),
3426 cx,
3427 );
3428 });
3429 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
3430
3431 // b. Add after the active item
3432 set_labeled_items(&pane, ["A", "B*", "C"], cx);
3433 pane.update(cx, |pane, cx| {
3434 pane.add_item(
3435 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3436 false,
3437 false,
3438 Some(2),
3439 cx,
3440 );
3441 });
3442 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3443
3444 // c. Add at the end of the item list (including off the length)
3445 set_labeled_items(&pane, ["A", "B*", "C"], cx);
3446 pane.update(cx, |pane, cx| {
3447 pane.add_item(
3448 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3449 false,
3450 false,
3451 Some(5),
3452 cx,
3453 );
3454 });
3455 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3456
3457 // 2. Add without a destination index
3458 // a. Add with active item at the start of the item list
3459 set_labeled_items(&pane, ["A*", "B", "C"], cx);
3460 pane.update(cx, |pane, cx| {
3461 pane.add_item(
3462 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3463 false,
3464 false,
3465 None,
3466 cx,
3467 );
3468 });
3469 set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
3470
3471 // b. Add with active item at the end of the item list
3472 set_labeled_items(&pane, ["A", "B", "C*"], cx);
3473 pane.update(cx, |pane, cx| {
3474 pane.add_item(
3475 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3476 false,
3477 false,
3478 None,
3479 cx,
3480 );
3481 });
3482 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3483 }
3484
3485 #[gpui::test]
3486 async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
3487 init_test(cx);
3488 let fs = FakeFs::new(cx.executor());
3489
3490 let project = Project::test(fs, None, cx).await;
3491 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3492 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3493
3494 // 1. Add with a destination index
3495 // 1a. Add before the active item
3496 let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3497 pane.update(cx, |pane, cx| {
3498 pane.add_item(d, false, false, Some(0), cx);
3499 });
3500 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
3501
3502 // 1b. Add after the active item
3503 let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3504 pane.update(cx, |pane, cx| {
3505 pane.add_item(d, false, false, Some(2), cx);
3506 });
3507 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3508
3509 // 1c. Add at the end of the item list (including off the length)
3510 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3511 pane.update(cx, |pane, cx| {
3512 pane.add_item(a, false, false, Some(5), cx);
3513 });
3514 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3515
3516 // 1d. Add same item to active index
3517 let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3518 pane.update(cx, |pane, cx| {
3519 pane.add_item(b, false, false, Some(1), cx);
3520 });
3521 assert_item_labels(&pane, ["A", "B*", "C"], cx);
3522
3523 // 1e. Add item to index after same item in last position
3524 let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3525 pane.update(cx, |pane, cx| {
3526 pane.add_item(c, false, false, Some(2), cx);
3527 });
3528 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3529
3530 // 2. Add without a destination index
3531 // 2a. Add with active item at the start of the item list
3532 let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
3533 pane.update(cx, |pane, cx| {
3534 pane.add_item(d, false, false, None, cx);
3535 });
3536 assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
3537
3538 // 2b. Add with active item at the end of the item list
3539 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
3540 pane.update(cx, |pane, cx| {
3541 pane.add_item(a, false, false, None, cx);
3542 });
3543 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3544
3545 // 2c. Add active item to active item at end of list
3546 let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
3547 pane.update(cx, |pane, cx| {
3548 pane.add_item(c, false, false, None, cx);
3549 });
3550 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3551
3552 // 2d. Add active item to active item at start of list
3553 let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
3554 pane.update(cx, |pane, cx| {
3555 pane.add_item(a, false, false, None, cx);
3556 });
3557 assert_item_labels(&pane, ["A*", "B", "C"], cx);
3558 }
3559
3560 #[gpui::test]
3561 async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
3562 init_test(cx);
3563 let fs = FakeFs::new(cx.executor());
3564
3565 let project = Project::test(fs, None, cx).await;
3566 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3567 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3568
3569 // singleton view
3570 pane.update(cx, |pane, cx| {
3571 pane.add_item(
3572 Box::new(cx.new_view(|cx| {
3573 TestItem::new(cx)
3574 .with_singleton(true)
3575 .with_label("buffer 1")
3576 .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
3577 })),
3578 false,
3579 false,
3580 None,
3581 cx,
3582 );
3583 });
3584 assert_item_labels(&pane, ["buffer 1*"], cx);
3585
3586 // new singleton view with the same project entry
3587 pane.update(cx, |pane, cx| {
3588 pane.add_item(
3589 Box::new(cx.new_view(|cx| {
3590 TestItem::new(cx)
3591 .with_singleton(true)
3592 .with_label("buffer 1")
3593 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3594 })),
3595 false,
3596 false,
3597 None,
3598 cx,
3599 );
3600 });
3601 assert_item_labels(&pane, ["buffer 1*"], cx);
3602
3603 // new singleton view with different project entry
3604 pane.update(cx, |pane, cx| {
3605 pane.add_item(
3606 Box::new(cx.new_view(|cx| {
3607 TestItem::new(cx)
3608 .with_singleton(true)
3609 .with_label("buffer 2")
3610 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
3611 })),
3612 false,
3613 false,
3614 None,
3615 cx,
3616 );
3617 });
3618 assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
3619
3620 // new multibuffer view with the same project entry
3621 pane.update(cx, |pane, cx| {
3622 pane.add_item(
3623 Box::new(cx.new_view(|cx| {
3624 TestItem::new(cx)
3625 .with_singleton(false)
3626 .with_label("multibuffer 1")
3627 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3628 })),
3629 false,
3630 false,
3631 None,
3632 cx,
3633 );
3634 });
3635 assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
3636
3637 // another multibuffer view with the same project entry
3638 pane.update(cx, |pane, cx| {
3639 pane.add_item(
3640 Box::new(cx.new_view(|cx| {
3641 TestItem::new(cx)
3642 .with_singleton(false)
3643 .with_label("multibuffer 1b")
3644 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3645 })),
3646 false,
3647 false,
3648 None,
3649 cx,
3650 );
3651 });
3652 assert_item_labels(
3653 &pane,
3654 ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
3655 cx,
3656 );
3657 }
3658
3659 #[gpui::test]
3660 async fn test_remove_item_ordering_history(cx: &mut TestAppContext) {
3661 init_test(cx);
3662 let fs = FakeFs::new(cx.executor());
3663
3664 let project = Project::test(fs, None, cx).await;
3665 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3666 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3667
3668 add_labeled_item(&pane, "A", false, cx);
3669 add_labeled_item(&pane, "B", false, cx);
3670 add_labeled_item(&pane, "C", false, cx);
3671 add_labeled_item(&pane, "D", false, cx);
3672 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3673
3674 pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
3675 add_labeled_item(&pane, "1", false, cx);
3676 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
3677
3678 pane.update(cx, |pane, cx| {
3679 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3680 })
3681 .unwrap()
3682 .await
3683 .unwrap();
3684 assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
3685
3686 pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
3687 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3688
3689 pane.update(cx, |pane, cx| {
3690 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3691 })
3692 .unwrap()
3693 .await
3694 .unwrap();
3695 assert_item_labels(&pane, ["A", "B*", "C"], cx);
3696
3697 pane.update(cx, |pane, cx| {
3698 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3699 })
3700 .unwrap()
3701 .await
3702 .unwrap();
3703 assert_item_labels(&pane, ["A", "C*"], cx);
3704
3705 pane.update(cx, |pane, cx| {
3706 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3707 })
3708 .unwrap()
3709 .await
3710 .unwrap();
3711 assert_item_labels(&pane, ["A*"], cx);
3712 }
3713
3714 #[gpui::test]
3715 async fn test_remove_item_ordering_neighbour(cx: &mut TestAppContext) {
3716 init_test(cx);
3717 cx.update_global::<SettingsStore, ()>(|s, cx| {
3718 s.update_user_settings::<ItemSettings>(cx, |s| {
3719 s.activate_on_close = Some(ActivateOnClose::Neighbour);
3720 });
3721 });
3722 let fs = FakeFs::new(cx.executor());
3723
3724 let project = Project::test(fs, None, cx).await;
3725 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3726 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3727
3728 add_labeled_item(&pane, "A", false, cx);
3729 add_labeled_item(&pane, "B", false, cx);
3730 add_labeled_item(&pane, "C", false, cx);
3731 add_labeled_item(&pane, "D", false, cx);
3732 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3733
3734 pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
3735 add_labeled_item(&pane, "1", false, cx);
3736 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
3737
3738 pane.update(cx, |pane, cx| {
3739 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3740 })
3741 .unwrap()
3742 .await
3743 .unwrap();
3744 assert_item_labels(&pane, ["A", "B", "C*", "D"], cx);
3745
3746 pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
3747 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3748
3749 pane.update(cx, |pane, cx| {
3750 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3751 })
3752 .unwrap()
3753 .await
3754 .unwrap();
3755 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3756
3757 pane.update(cx, |pane, cx| {
3758 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3759 })
3760 .unwrap()
3761 .await
3762 .unwrap();
3763 assert_item_labels(&pane, ["A", "B*"], cx);
3764
3765 pane.update(cx, |pane, cx| {
3766 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3767 })
3768 .unwrap()
3769 .await
3770 .unwrap();
3771 assert_item_labels(&pane, ["A*"], cx);
3772 }
3773
3774 #[gpui::test]
3775 async fn test_remove_item_ordering_left_neighbour(cx: &mut TestAppContext) {
3776 init_test(cx);
3777 cx.update_global::<SettingsStore, ()>(|s, cx| {
3778 s.update_user_settings::<ItemSettings>(cx, |s| {
3779 s.activate_on_close = Some(ActivateOnClose::LeftNeighbour);
3780 });
3781 });
3782 let fs = FakeFs::new(cx.executor());
3783
3784 let project = Project::test(fs, None, cx).await;
3785 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3786 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3787
3788 add_labeled_item(&pane, "A", false, cx);
3789 add_labeled_item(&pane, "B", false, cx);
3790 add_labeled_item(&pane, "C", false, cx);
3791 add_labeled_item(&pane, "D", false, cx);
3792 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3793
3794 pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
3795 add_labeled_item(&pane, "1", false, cx);
3796 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
3797
3798 pane.update(cx, |pane, cx| {
3799 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3800 })
3801 .unwrap()
3802 .await
3803 .unwrap();
3804 assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
3805
3806 pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
3807 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3808
3809 pane.update(cx, |pane, cx| {
3810 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3811 })
3812 .unwrap()
3813 .await
3814 .unwrap();
3815 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3816
3817 pane.update(cx, |pane, cx| pane.activate_item(0, false, false, cx));
3818 assert_item_labels(&pane, ["A*", "B", "C"], cx);
3819
3820 pane.update(cx, |pane, cx| {
3821 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3822 })
3823 .unwrap()
3824 .await
3825 .unwrap();
3826 assert_item_labels(&pane, ["B*", "C"], cx);
3827
3828 pane.update(cx, |pane, cx| {
3829 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3830 })
3831 .unwrap()
3832 .await
3833 .unwrap();
3834 assert_item_labels(&pane, ["C*"], cx);
3835 }
3836
3837 #[gpui::test]
3838 async fn test_close_inactive_items(cx: &mut TestAppContext) {
3839 init_test(cx);
3840 let fs = FakeFs::new(cx.executor());
3841
3842 let project = Project::test(fs, None, cx).await;
3843 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3844 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3845
3846 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
3847
3848 pane.update(cx, |pane, cx| {
3849 pane.close_inactive_items(
3850 &CloseInactiveItems {
3851 save_intent: None,
3852 close_pinned: false,
3853 },
3854 cx,
3855 )
3856 })
3857 .unwrap()
3858 .await
3859 .unwrap();
3860 assert_item_labels(&pane, ["C*"], cx);
3861 }
3862
3863 #[gpui::test]
3864 async fn test_close_clean_items(cx: &mut TestAppContext) {
3865 init_test(cx);
3866 let fs = FakeFs::new(cx.executor());
3867
3868 let project = Project::test(fs, None, cx).await;
3869 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3870 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3871
3872 add_labeled_item(&pane, "A", true, cx);
3873 add_labeled_item(&pane, "B", false, cx);
3874 add_labeled_item(&pane, "C", true, cx);
3875 add_labeled_item(&pane, "D", false, cx);
3876 add_labeled_item(&pane, "E", false, cx);
3877 assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
3878
3879 pane.update(cx, |pane, cx| {
3880 pane.close_clean_items(
3881 &CloseCleanItems {
3882 close_pinned: false,
3883 },
3884 cx,
3885 )
3886 })
3887 .unwrap()
3888 .await
3889 .unwrap();
3890 assert_item_labels(&pane, ["A^", "C*^"], cx);
3891 }
3892
3893 #[gpui::test]
3894 async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
3895 init_test(cx);
3896 let fs = FakeFs::new(cx.executor());
3897
3898 let project = Project::test(fs, None, cx).await;
3899 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3900 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3901
3902 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
3903
3904 pane.update(cx, |pane, cx| {
3905 pane.close_items_to_the_left(
3906 &CloseItemsToTheLeft {
3907 close_pinned: false,
3908 },
3909 cx,
3910 )
3911 })
3912 .unwrap()
3913 .await
3914 .unwrap();
3915 assert_item_labels(&pane, ["C*", "D", "E"], cx);
3916 }
3917
3918 #[gpui::test]
3919 async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
3920 init_test(cx);
3921 let fs = FakeFs::new(cx.executor());
3922
3923 let project = Project::test(fs, None, cx).await;
3924 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3925 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3926
3927 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
3928
3929 pane.update(cx, |pane, cx| {
3930 pane.close_items_to_the_right(
3931 &CloseItemsToTheRight {
3932 close_pinned: false,
3933 },
3934 cx,
3935 )
3936 })
3937 .unwrap()
3938 .await
3939 .unwrap();
3940 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3941 }
3942
3943 #[gpui::test]
3944 async fn test_close_all_items(cx: &mut TestAppContext) {
3945 init_test(cx);
3946 let fs = FakeFs::new(cx.executor());
3947
3948 let project = Project::test(fs, None, cx).await;
3949 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3950 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3951
3952 let item_a = add_labeled_item(&pane, "A", false, cx);
3953 add_labeled_item(&pane, "B", false, cx);
3954 add_labeled_item(&pane, "C", false, cx);
3955 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3956
3957 pane.update(cx, |pane, cx| {
3958 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
3959 pane.pin_tab_at(ix, cx);
3960 pane.close_all_items(
3961 &CloseAllItems {
3962 save_intent: None,
3963 close_pinned: false,
3964 },
3965 cx,
3966 )
3967 })
3968 .unwrap()
3969 .await
3970 .unwrap();
3971 assert_item_labels(&pane, ["A*"], cx);
3972
3973 pane.update(cx, |pane, cx| {
3974 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
3975 pane.unpin_tab_at(ix, cx);
3976 pane.close_all_items(
3977 &CloseAllItems {
3978 save_intent: None,
3979 close_pinned: false,
3980 },
3981 cx,
3982 )
3983 })
3984 .unwrap()
3985 .await
3986 .unwrap();
3987
3988 assert_item_labels(&pane, [], cx);
3989
3990 add_labeled_item(&pane, "A", true, cx).update(cx, |item, cx| {
3991 item.project_items
3992 .push(TestProjectItem::new(1, "A.txt", cx))
3993 });
3994 add_labeled_item(&pane, "B", true, cx).update(cx, |item, cx| {
3995 item.project_items
3996 .push(TestProjectItem::new(2, "B.txt", cx))
3997 });
3998 add_labeled_item(&pane, "C", true, cx).update(cx, |item, cx| {
3999 item.project_items
4000 .push(TestProjectItem::new(3, "C.txt", cx))
4001 });
4002 assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
4003
4004 let save = pane
4005 .update(cx, |pane, cx| {
4006 pane.close_all_items(
4007 &CloseAllItems {
4008 save_intent: None,
4009 close_pinned: false,
4010 },
4011 cx,
4012 )
4013 })
4014 .unwrap();
4015
4016 cx.executor().run_until_parked();
4017 cx.simulate_prompt_answer(2);
4018 save.await.unwrap();
4019 assert_item_labels(&pane, [], cx);
4020
4021 add_labeled_item(&pane, "A", true, cx);
4022 add_labeled_item(&pane, "B", true, cx);
4023 add_labeled_item(&pane, "C", true, cx);
4024 assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
4025 let save = pane
4026 .update(cx, |pane, cx| {
4027 pane.close_all_items(
4028 &CloseAllItems {
4029 save_intent: None,
4030 close_pinned: false,
4031 },
4032 cx,
4033 )
4034 })
4035 .unwrap();
4036
4037 cx.executor().run_until_parked();
4038 cx.simulate_prompt_answer(2);
4039 save.await.unwrap();
4040 assert_item_labels(&pane, [], cx);
4041 }
4042
4043 #[gpui::test]
4044 async fn test_close_all_items_including_pinned(cx: &mut TestAppContext) {
4045 init_test(cx);
4046 let fs = FakeFs::new(cx.executor());
4047
4048 let project = Project::test(fs, None, cx).await;
4049 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
4050 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4051
4052 let item_a = add_labeled_item(&pane, "A", false, cx);
4053 add_labeled_item(&pane, "B", false, cx);
4054 add_labeled_item(&pane, "C", false, cx);
4055 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4056
4057 pane.update(cx, |pane, cx| {
4058 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4059 pane.pin_tab_at(ix, cx);
4060 pane.close_all_items(
4061 &CloseAllItems {
4062 save_intent: None,
4063 close_pinned: true,
4064 },
4065 cx,
4066 )
4067 })
4068 .unwrap()
4069 .await
4070 .unwrap();
4071 assert_item_labels(&pane, [], cx);
4072 }
4073
4074 fn init_test(cx: &mut TestAppContext) {
4075 cx.update(|cx| {
4076 let settings_store = SettingsStore::test(cx);
4077 cx.set_global(settings_store);
4078 theme::init(LoadThemes::JustBase, cx);
4079 crate::init_settings(cx);
4080 Project::init_settings(cx);
4081 });
4082 }
4083
4084 fn set_max_tabs(cx: &mut TestAppContext, value: Option<usize>) {
4085 cx.update_global(|store: &mut SettingsStore, cx| {
4086 store.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4087 settings.max_tabs = value.map(|v| NonZero::new(v).unwrap())
4088 });
4089 });
4090 }
4091
4092 fn add_labeled_item(
4093 pane: &View<Pane>,
4094 label: &str,
4095 is_dirty: bool,
4096 cx: &mut VisualTestContext,
4097 ) -> Box<View<TestItem>> {
4098 pane.update(cx, |pane, cx| {
4099 let labeled_item = Box::new(
4100 cx.new_view(|cx| TestItem::new(cx).with_label(label).with_dirty(is_dirty)),
4101 );
4102 pane.add_item(labeled_item.clone(), false, false, None, cx);
4103 labeled_item
4104 })
4105 }
4106
4107 fn set_labeled_items<const COUNT: usize>(
4108 pane: &View<Pane>,
4109 labels: [&str; COUNT],
4110 cx: &mut VisualTestContext,
4111 ) -> [Box<View<TestItem>>; COUNT] {
4112 pane.update(cx, |pane, cx| {
4113 pane.items.clear();
4114 let mut active_item_index = 0;
4115
4116 let mut index = 0;
4117 let items = labels.map(|mut label| {
4118 if label.ends_with('*') {
4119 label = label.trim_end_matches('*');
4120 active_item_index = index;
4121 }
4122
4123 let labeled_item = Box::new(cx.new_view(|cx| TestItem::new(cx).with_label(label)));
4124 pane.add_item(labeled_item.clone(), false, false, None, cx);
4125 index += 1;
4126 labeled_item
4127 });
4128
4129 pane.activate_item(active_item_index, false, false, cx);
4130
4131 items
4132 })
4133 }
4134
4135 // Assert the item label, with the active item label suffixed with a '*'
4136 #[track_caller]
4137 fn assert_item_labels<const COUNT: usize>(
4138 pane: &View<Pane>,
4139 expected_states: [&str; COUNT],
4140 cx: &mut VisualTestContext,
4141 ) {
4142 let actual_states = pane.update(cx, |pane, cx| {
4143 pane.items
4144 .iter()
4145 .enumerate()
4146 .map(|(ix, item)| {
4147 let mut state = item
4148 .to_any()
4149 .downcast::<TestItem>()
4150 .unwrap()
4151 .read(cx)
4152 .label
4153 .clone();
4154 if ix == pane.active_item_index {
4155 state.push('*');
4156 }
4157 if item.is_dirty(cx) {
4158 state.push('^');
4159 }
4160 state
4161 })
4162 .collect::<Vec<_>>()
4163 });
4164 assert_eq!(
4165 actual_states, expected_states,
4166 "pane items do not match expectation"
4167 );
4168 }
4169}