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