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