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