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