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