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