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 self.tab_bar_scroll_handle.scroll_to_item(index);
1017 cx.notify();
1018 }
1019 }
1020
1021 pub fn activate_prev_item(&mut self, activate_pane: bool, cx: &mut ViewContext<Self>) {
1022 let mut index = self.active_item_index;
1023 if index > 0 {
1024 index -= 1;
1025 } else if !self.items.is_empty() {
1026 index = self.items.len() - 1;
1027 }
1028 self.activate_item(index, activate_pane, activate_pane, cx);
1029 }
1030
1031 pub fn activate_next_item(&mut self, activate_pane: bool, cx: &mut ViewContext<Self>) {
1032 let mut index = self.active_item_index;
1033 if index + 1 < self.items.len() {
1034 index += 1;
1035 } else {
1036 index = 0;
1037 }
1038 self.activate_item(index, activate_pane, activate_pane, cx);
1039 }
1040
1041 pub fn close_active_item(
1042 &mut self,
1043 action: &CloseActiveItem,
1044 cx: &mut ViewContext<Self>,
1045 ) -> Option<Task<Result<()>>> {
1046 if self.items.is_empty() {
1047 // Close the window when there's no active items to close, if configured
1048 if WorkspaceSettings::get_global(cx)
1049 .when_closing_with_no_tabs
1050 .should_close()
1051 {
1052 cx.dispatch_action(Box::new(CloseWindow));
1053 }
1054
1055 return None;
1056 }
1057 let active_item_id = self.items[self.active_item_index].item_id();
1058 Some(self.close_item_by_id(
1059 active_item_id,
1060 action.save_intent.unwrap_or(SaveIntent::Close),
1061 cx,
1062 ))
1063 }
1064
1065 pub fn close_item_by_id(
1066 &mut self,
1067 item_id_to_close: EntityId,
1068 save_intent: SaveIntent,
1069 cx: &mut ViewContext<Self>,
1070 ) -> Task<Result<()>> {
1071 self.close_items(cx, save_intent, move |view_id| view_id == item_id_to_close)
1072 }
1073
1074 pub fn close_inactive_items(
1075 &mut self,
1076 action: &CloseInactiveItems,
1077 cx: &mut ViewContext<Self>,
1078 ) -> Option<Task<Result<()>>> {
1079 if self.items.is_empty() {
1080 return None;
1081 }
1082
1083 let active_item_id = self.items[self.active_item_index].item_id();
1084 Some(self.close_items(
1085 cx,
1086 action.save_intent.unwrap_or(SaveIntent::Close),
1087 move |item_id| item_id != active_item_id,
1088 ))
1089 }
1090
1091 pub fn close_clean_items(
1092 &mut self,
1093 _: &CloseCleanItems,
1094 cx: &mut ViewContext<Self>,
1095 ) -> Option<Task<Result<()>>> {
1096 let item_ids: Vec<_> = self
1097 .items()
1098 .filter(|item| !item.is_dirty(cx))
1099 .map(|item| item.item_id())
1100 .collect();
1101 Some(self.close_items(cx, SaveIntent::Close, move |item_id| {
1102 item_ids.contains(&item_id)
1103 }))
1104 }
1105
1106 pub fn close_items_to_the_left(
1107 &mut self,
1108 _: &CloseItemsToTheLeft,
1109 cx: &mut ViewContext<Self>,
1110 ) -> Option<Task<Result<()>>> {
1111 if self.items.is_empty() {
1112 return None;
1113 }
1114 let active_item_id = self.items[self.active_item_index].item_id();
1115 Some(self.close_items_to_the_left_by_id(active_item_id, cx))
1116 }
1117
1118 pub fn close_items_to_the_left_by_id(
1119 &mut self,
1120 item_id: EntityId,
1121 cx: &mut ViewContext<Self>,
1122 ) -> Task<Result<()>> {
1123 let item_ids: Vec<_> = self
1124 .items()
1125 .take_while(|item| item.item_id() != item_id)
1126 .map(|item| item.item_id())
1127 .collect();
1128 self.close_items(cx, SaveIntent::Close, move |item_id| {
1129 item_ids.contains(&item_id)
1130 })
1131 }
1132
1133 pub fn close_items_to_the_right(
1134 &mut self,
1135 _: &CloseItemsToTheRight,
1136 cx: &mut ViewContext<Self>,
1137 ) -> Option<Task<Result<()>>> {
1138 if self.items.is_empty() {
1139 return None;
1140 }
1141 let active_item_id = self.items[self.active_item_index].item_id();
1142 Some(self.close_items_to_the_right_by_id(active_item_id, cx))
1143 }
1144
1145 pub fn close_items_to_the_right_by_id(
1146 &mut self,
1147 item_id: EntityId,
1148 cx: &mut ViewContext<Self>,
1149 ) -> Task<Result<()>> {
1150 let item_ids: Vec<_> = self
1151 .items()
1152 .rev()
1153 .take_while(|item| item.item_id() != item_id)
1154 .map(|item| item.item_id())
1155 .collect();
1156 self.close_items(cx, SaveIntent::Close, move |item_id| {
1157 item_ids.contains(&item_id)
1158 })
1159 }
1160
1161 pub fn close_all_items(
1162 &mut self,
1163 action: &CloseAllItems,
1164 cx: &mut ViewContext<Self>,
1165 ) -> Option<Task<Result<()>>> {
1166 if self.items.is_empty() {
1167 return None;
1168 }
1169
1170 Some(
1171 self.close_items(cx, action.save_intent.unwrap_or(SaveIntent::Close), |_| {
1172 true
1173 }),
1174 )
1175 }
1176
1177 pub(super) fn file_names_for_prompt(
1178 items: &mut dyn Iterator<Item = &Box<dyn ItemHandle>>,
1179 all_dirty_items: usize,
1180 cx: &AppContext,
1181 ) -> (String, String) {
1182 /// Quantity of item paths displayed in prompt prior to cutoff..
1183 const FILE_NAMES_CUTOFF_POINT: usize = 10;
1184 let mut file_names: Vec<_> = items
1185 .filter_map(|item| {
1186 item.project_path(cx).and_then(|project_path| {
1187 project_path
1188 .path
1189 .file_name()
1190 .and_then(|name| name.to_str().map(ToOwned::to_owned))
1191 })
1192 })
1193 .take(FILE_NAMES_CUTOFF_POINT)
1194 .collect();
1195 let should_display_followup_text =
1196 all_dirty_items > FILE_NAMES_CUTOFF_POINT || file_names.len() != all_dirty_items;
1197 if should_display_followup_text {
1198 let not_shown_files = all_dirty_items - file_names.len();
1199 if not_shown_files == 1 {
1200 file_names.push(".. 1 file not shown".into());
1201 } else {
1202 file_names.push(format!(".. {} files not shown", not_shown_files));
1203 }
1204 }
1205 (
1206 format!(
1207 "Do you want to save changes to the following {} files?",
1208 all_dirty_items
1209 ),
1210 file_names.join("\n"),
1211 )
1212 }
1213
1214 pub fn close_items(
1215 &mut self,
1216 cx: &mut ViewContext<Pane>,
1217 mut save_intent: SaveIntent,
1218 should_close: impl Fn(EntityId) -> bool,
1219 ) -> Task<Result<()>> {
1220 // Find the items to close.
1221 let mut items_to_close = Vec::new();
1222 let mut dirty_items = Vec::new();
1223 for item in &self.items {
1224 if should_close(item.item_id()) {
1225 items_to_close.push(item.boxed_clone());
1226 if item.is_dirty(cx) {
1227 dirty_items.push(item.boxed_clone());
1228 }
1229 }
1230 }
1231
1232 let active_item_id = self.active_item().map(|item| item.item_id());
1233
1234 items_to_close.sort_by_key(|item| {
1235 // Put the currently active item at the end, because if the currently active item is not closed last
1236 // closing the currently active item will cause the focus to switch to another item
1237 // This will cause Zed to expand the content of the currently active item
1238 active_item_id.filter(|&id| id == item.item_id()).is_some()
1239 // If a buffer is open both in a singleton editor and in a multibuffer, make sure
1240 // to focus the singleton buffer when prompting to save that buffer, as opposed
1241 // to focusing the multibuffer, because this gives the user a more clear idea
1242 // of what content they would be saving.
1243 || !item.is_singleton(cx)
1244 });
1245
1246 let workspace = self.workspace.clone();
1247 cx.spawn(|pane, mut cx| async move {
1248 if save_intent == SaveIntent::Close && dirty_items.len() > 1 {
1249 let answer = pane.update(&mut cx, |_, cx| {
1250 let (prompt, detail) =
1251 Self::file_names_for_prompt(&mut dirty_items.iter(), dirty_items.len(), cx);
1252 cx.prompt(
1253 PromptLevel::Warning,
1254 &prompt,
1255 Some(&detail),
1256 &["Save all", "Discard all", "Cancel"],
1257 )
1258 })?;
1259 match answer.await {
1260 Ok(0) => save_intent = SaveIntent::SaveAll,
1261 Ok(1) => save_intent = SaveIntent::Skip,
1262 _ => {}
1263 }
1264 }
1265 let mut saved_project_items_ids = HashSet::default();
1266 for item in items_to_close.clone() {
1267 // Find the item's current index and its set of project item models. Avoid
1268 // storing these in advance, in case they have changed since this task
1269 // was started.
1270 let (item_ix, mut project_item_ids) = pane.update(&mut cx, |pane, cx| {
1271 (pane.index_for_item(&*item), item.project_item_model_ids(cx))
1272 })?;
1273 let item_ix = if let Some(ix) = item_ix {
1274 ix
1275 } else {
1276 continue;
1277 };
1278
1279 // Check if this view has any project items that are not open anywhere else
1280 // in the workspace, AND that the user has not already been prompted to save.
1281 // If there are any such project entries, prompt the user to save this item.
1282 let project = workspace.update(&mut cx, |workspace, cx| {
1283 for item in workspace.items(cx) {
1284 if !items_to_close
1285 .iter()
1286 .any(|item_to_close| item_to_close.item_id() == item.item_id())
1287 {
1288 let other_project_item_ids = item.project_item_model_ids(cx);
1289 project_item_ids.retain(|id| !other_project_item_ids.contains(id));
1290 }
1291 }
1292 workspace.project().clone()
1293 })?;
1294 let should_save = project_item_ids
1295 .iter()
1296 .any(|id| saved_project_items_ids.insert(*id));
1297
1298 if should_save
1299 && !Self::save_item(
1300 project.clone(),
1301 &pane,
1302 item_ix,
1303 &*item,
1304 save_intent,
1305 &mut cx,
1306 )
1307 .await?
1308 {
1309 break;
1310 }
1311
1312 // Remove the item from the pane.
1313 pane.update(&mut cx, |pane, cx| {
1314 if let Some(item_ix) = pane
1315 .items
1316 .iter()
1317 .position(|i| i.item_id() == item.item_id())
1318 {
1319 pane.remove_item(item_ix, false, true, cx);
1320 }
1321 })
1322 .ok();
1323 }
1324
1325 pane.update(&mut cx, |_, cx| cx.notify()).ok();
1326 Ok(())
1327 })
1328 }
1329
1330 pub fn remove_item(
1331 &mut self,
1332 item_index: usize,
1333 activate_pane: bool,
1334 close_pane_if_empty: bool,
1335 cx: &mut ViewContext<Self>,
1336 ) {
1337 self._remove_item(item_index, activate_pane, close_pane_if_empty, None, cx)
1338 }
1339
1340 pub fn remove_item_and_focus_on_pane(
1341 &mut self,
1342 item_index: usize,
1343 activate_pane: bool,
1344 focus_on_pane_if_closed: View<Pane>,
1345 cx: &mut ViewContext<Self>,
1346 ) {
1347 self._remove_item(
1348 item_index,
1349 activate_pane,
1350 true,
1351 Some(focus_on_pane_if_closed),
1352 cx,
1353 )
1354 }
1355
1356 fn _remove_item(
1357 &mut self,
1358 item_index: usize,
1359 activate_pane: bool,
1360 close_pane_if_empty: bool,
1361 focus_on_pane_if_closed: Option<View<Pane>>,
1362 cx: &mut ViewContext<Self>,
1363 ) {
1364 self.activation_history
1365 .retain(|entry| entry.entity_id != self.items[item_index].item_id());
1366
1367 if item_index == self.active_item_index {
1368 let index_to_activate = self
1369 .activation_history
1370 .pop()
1371 .and_then(|last_activated_item| {
1372 self.items.iter().enumerate().find_map(|(index, item)| {
1373 (item.item_id() == last_activated_item.entity_id).then_some(index)
1374 })
1375 })
1376 // We didn't have a valid activation history entry, so fallback
1377 // to activating the item to the left
1378 .unwrap_or_else(|| item_index.min(self.items.len()).saturating_sub(1));
1379
1380 let should_activate = activate_pane || self.has_focus(cx);
1381 if self.items.len() == 1 && should_activate {
1382 self.focus_handle.focus(cx);
1383 } else {
1384 self.activate_item(index_to_activate, should_activate, should_activate, cx);
1385 }
1386 }
1387
1388 cx.emit(Event::RemoveItem { idx: item_index });
1389
1390 let item = self.items.remove(item_index);
1391
1392 cx.emit(Event::RemovedItem {
1393 item_id: item.item_id(),
1394 });
1395 if self.items.is_empty() {
1396 item.deactivated(cx);
1397 if close_pane_if_empty {
1398 self.update_toolbar(cx);
1399 cx.emit(Event::Remove {
1400 focus_on_pane: focus_on_pane_if_closed,
1401 });
1402 }
1403 }
1404
1405 if item_index < self.active_item_index {
1406 self.active_item_index -= 1;
1407 }
1408
1409 let mode = self.nav_history.mode();
1410 self.nav_history.set_mode(NavigationMode::ClosingItem);
1411 item.deactivated(cx);
1412 self.nav_history.set_mode(mode);
1413
1414 if self.is_active_preview_item(item.item_id()) {
1415 self.set_preview_item_id(None, cx);
1416 }
1417
1418 if let Some(path) = item.project_path(cx) {
1419 let abs_path = self
1420 .nav_history
1421 .0
1422 .lock()
1423 .paths_by_item
1424 .get(&item.item_id())
1425 .and_then(|(_, abs_path)| abs_path.clone());
1426
1427 self.nav_history
1428 .0
1429 .lock()
1430 .paths_by_item
1431 .insert(item.item_id(), (path, abs_path));
1432 } else {
1433 self.nav_history
1434 .0
1435 .lock()
1436 .paths_by_item
1437 .remove(&item.item_id());
1438 }
1439
1440 if self.items.is_empty() && close_pane_if_empty && self.zoomed {
1441 cx.emit(Event::ZoomOut);
1442 }
1443
1444 cx.notify();
1445 }
1446
1447 pub async fn save_item(
1448 project: Model<Project>,
1449 pane: &WeakView<Pane>,
1450 item_ix: usize,
1451 item: &dyn ItemHandle,
1452 save_intent: SaveIntent,
1453 cx: &mut AsyncWindowContext,
1454 ) -> Result<bool> {
1455 const CONFLICT_MESSAGE: &str =
1456 "This file has changed on disk since you started editing it. Do you want to overwrite it?";
1457
1458 if save_intent == SaveIntent::Skip {
1459 return Ok(true);
1460 }
1461
1462 let (mut has_conflict, mut is_dirty, mut can_save, can_save_as) = cx.update(|cx| {
1463 (
1464 item.has_conflict(cx),
1465 item.is_dirty(cx),
1466 item.can_save(cx),
1467 item.is_singleton(cx),
1468 )
1469 })?;
1470
1471 // when saving a single buffer, we ignore whether or not it's dirty.
1472 if save_intent == SaveIntent::Save || save_intent == SaveIntent::SaveWithoutFormat {
1473 is_dirty = true;
1474 }
1475
1476 if save_intent == SaveIntent::SaveAs {
1477 is_dirty = true;
1478 has_conflict = false;
1479 can_save = false;
1480 }
1481
1482 if save_intent == SaveIntent::Overwrite {
1483 has_conflict = false;
1484 }
1485
1486 let should_format = save_intent != SaveIntent::SaveWithoutFormat;
1487
1488 if has_conflict && can_save {
1489 let answer = pane.update(cx, |pane, cx| {
1490 pane.activate_item(item_ix, true, true, cx);
1491 cx.prompt(
1492 PromptLevel::Warning,
1493 CONFLICT_MESSAGE,
1494 None,
1495 &["Overwrite", "Discard", "Cancel"],
1496 )
1497 })?;
1498 match answer.await {
1499 Ok(0) => {
1500 pane.update(cx, |_, cx| item.save(should_format, project, cx))?
1501 .await?
1502 }
1503 Ok(1) => pane.update(cx, |_, cx| item.reload(project, cx))?.await?,
1504 _ => return Ok(false),
1505 }
1506 } else if is_dirty && (can_save || can_save_as) {
1507 if save_intent == SaveIntent::Close {
1508 let will_autosave = cx.update(|cx| {
1509 matches!(
1510 item.workspace_settings(cx).autosave,
1511 AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange
1512 ) && Self::can_autosave_item(item, cx)
1513 })?;
1514 if !will_autosave {
1515 let item_id = item.item_id();
1516 let answer_task = pane.update(cx, |pane, cx| {
1517 if pane.save_modals_spawned.insert(item_id) {
1518 pane.activate_item(item_ix, true, true, cx);
1519 let prompt = dirty_message_for(item.project_path(cx));
1520 Some(cx.prompt(
1521 PromptLevel::Warning,
1522 &prompt,
1523 None,
1524 &["Save", "Don't Save", "Cancel"],
1525 ))
1526 } else {
1527 None
1528 }
1529 })?;
1530 if let Some(answer_task) = answer_task {
1531 let answer = answer_task.await;
1532 pane.update(cx, |pane, _| {
1533 if !pane.save_modals_spawned.remove(&item_id) {
1534 debug_panic!(
1535 "save modal was not present in spawned modals after awaiting for its answer"
1536 )
1537 }
1538 })?;
1539 match answer {
1540 Ok(0) => {}
1541 Ok(1) => {
1542 // Don't save this file
1543 pane.update(cx, |_, cx| item.discarded(project, cx))
1544 .log_err();
1545 return Ok(true);
1546 }
1547 _ => return Ok(false), // Cancel
1548 }
1549 } else {
1550 return Ok(false);
1551 }
1552 }
1553 }
1554
1555 if can_save {
1556 pane.update(cx, |_, cx| item.save(should_format, project, cx))?
1557 .await?;
1558 } else if can_save_as {
1559 let abs_path = pane.update(cx, |pane, cx| {
1560 pane.workspace
1561 .update(cx, |workspace, cx| workspace.prompt_for_new_path(cx))
1562 })??;
1563 if let Some(abs_path) = abs_path.await.ok().flatten() {
1564 pane.update(cx, |pane, cx| {
1565 if let Some(item) = pane.item_for_path(abs_path.clone(), cx) {
1566 if let Some(idx) = pane.index_for_item(&*item) {
1567 pane.remove_item(idx, false, false, cx);
1568 }
1569 }
1570
1571 item.save_as(project, abs_path, cx)
1572 })?
1573 .await?;
1574 } else {
1575 return Ok(false);
1576 }
1577 }
1578 }
1579
1580 pane.update(cx, |_, cx| {
1581 cx.emit(Event::UserSavedItem {
1582 item: item.downgrade_item(),
1583 save_intent,
1584 });
1585 true
1586 })
1587 }
1588
1589 fn can_autosave_item(item: &dyn ItemHandle, cx: &AppContext) -> bool {
1590 let is_deleted = item.project_entry_ids(cx).is_empty();
1591 item.is_dirty(cx) && !item.has_conflict(cx) && item.can_save(cx) && !is_deleted
1592 }
1593
1594 pub fn autosave_item(
1595 item: &dyn ItemHandle,
1596 project: Model<Project>,
1597 cx: &mut WindowContext,
1598 ) -> Task<Result<()>> {
1599 let format =
1600 if let AutosaveSetting::AfterDelay { .. } = item.workspace_settings(cx).autosave {
1601 false
1602 } else {
1603 true
1604 };
1605 if Self::can_autosave_item(item, cx) {
1606 item.save(format, project, cx)
1607 } else {
1608 Task::ready(Ok(()))
1609 }
1610 }
1611
1612 pub fn focus(&mut self, cx: &mut ViewContext<Pane>) {
1613 cx.focus(&self.focus_handle);
1614 }
1615
1616 pub fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
1617 if let Some(active_item) = self.active_item() {
1618 let focus_handle = active_item.focus_handle(cx);
1619 cx.focus(&focus_handle);
1620 }
1621 }
1622
1623 pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext<Self>) {
1624 cx.emit(Event::Split(direction));
1625 }
1626
1627 pub fn toolbar(&self) -> &View<Toolbar> {
1628 &self.toolbar
1629 }
1630
1631 pub fn handle_deleted_project_item(
1632 &mut self,
1633 entry_id: ProjectEntryId,
1634 cx: &mut ViewContext<Pane>,
1635 ) -> Option<()> {
1636 let (item_index_to_delete, item_id) = self.items().enumerate().find_map(|(i, item)| {
1637 if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
1638 Some((i, item.item_id()))
1639 } else {
1640 None
1641 }
1642 })?;
1643
1644 self.remove_item(item_index_to_delete, false, true, cx);
1645 self.nav_history.remove_item(item_id);
1646
1647 Some(())
1648 }
1649
1650 fn update_toolbar(&mut self, cx: &mut ViewContext<Self>) {
1651 let active_item = self
1652 .items
1653 .get(self.active_item_index)
1654 .map(|item| item.as_ref());
1655 self.toolbar.update(cx, |toolbar, cx| {
1656 toolbar.set_active_item(active_item, cx);
1657 });
1658 }
1659
1660 fn update_status_bar(&mut self, cx: &mut ViewContext<Self>) {
1661 let workspace = self.workspace.clone();
1662 let pane = cx.view().clone();
1663
1664 cx.window_context().defer(move |cx| {
1665 let Ok(status_bar) = workspace.update(cx, |workspace, _| workspace.status_bar.clone())
1666 else {
1667 return;
1668 };
1669
1670 status_bar.update(cx, move |status_bar, cx| {
1671 status_bar.set_active_pane(&pane, cx);
1672 });
1673 });
1674 }
1675
1676 fn entry_abs_path(&self, entry: ProjectEntryId, cx: &WindowContext) -> Option<PathBuf> {
1677 let worktree = self
1678 .workspace
1679 .upgrade()?
1680 .read(cx)
1681 .project()
1682 .read(cx)
1683 .worktree_for_entry(entry, cx)?
1684 .read(cx);
1685 let entry = worktree.entry_for_id(entry)?;
1686 let abs_path = worktree.absolutize(&entry.path).ok()?;
1687 if entry.is_symlink {
1688 abs_path.canonicalize().ok()
1689 } else {
1690 Some(abs_path)
1691 }
1692 }
1693
1694 fn copy_relative_path(&mut self, _: &CopyRelativePath, cx: &mut ViewContext<Self>) {
1695 if let Some(clipboard_text) = self
1696 .active_item()
1697 .as_ref()
1698 .and_then(|entry| entry.project_path(cx))
1699 .map(|p| p.path.to_string_lossy().to_string())
1700 {
1701 cx.write_to_clipboard(ClipboardItem::new_string(clipboard_text));
1702 }
1703 }
1704
1705 pub fn icon_color(selected: bool) -> Color {
1706 if selected {
1707 Color::Default
1708 } else {
1709 Color::Muted
1710 }
1711 }
1712
1713 pub fn git_aware_icon_color(
1714 git_status: Option<GitFileStatus>,
1715 ignored: bool,
1716 selected: bool,
1717 ) -> Color {
1718 if ignored {
1719 Color::Ignored
1720 } else {
1721 match git_status {
1722 Some(GitFileStatus::Added) => Color::Created,
1723 Some(GitFileStatus::Modified) => Color::Modified,
1724 Some(GitFileStatus::Conflict) => Color::Conflict,
1725 None => Self::icon_color(selected),
1726 }
1727 }
1728 }
1729
1730 fn toggle_pin_tab(&mut self, _: &TogglePinTab, cx: &mut ViewContext<'_, Self>) {
1731 if self.items.is_empty() {
1732 return;
1733 }
1734 let active_tab_ix = self.active_item_index();
1735 if self.is_tab_pinned(active_tab_ix) {
1736 self.unpin_tab_at(active_tab_ix, cx);
1737 } else {
1738 self.pin_tab_at(active_tab_ix, cx);
1739 }
1740 }
1741
1742 fn pin_tab_at(&mut self, ix: usize, cx: &mut ViewContext<'_, Self>) {
1743 maybe!({
1744 let pane = cx.view().clone();
1745 let destination_index = self.pinned_tab_count;
1746 self.pinned_tab_count += 1;
1747 let id = self.item_for_index(ix)?.item_id();
1748
1749 self.workspace
1750 .update(cx, |_, cx| {
1751 cx.defer(move |this, cx| {
1752 this.move_item(pane.clone(), pane, id, destination_index, cx)
1753 });
1754 })
1755 .ok()?;
1756
1757 Some(())
1758 });
1759 }
1760
1761 fn unpin_tab_at(&mut self, ix: usize, cx: &mut ViewContext<'_, Self>) {
1762 maybe!({
1763 let pane = cx.view().clone();
1764 self.pinned_tab_count = self.pinned_tab_count.checked_sub(1).unwrap();
1765 let destination_index = self.pinned_tab_count;
1766
1767 let id = self.item_for_index(ix)?.item_id();
1768
1769 self.workspace
1770 .update(cx, |_, cx| {
1771 cx.defer(move |this, cx| {
1772 this.move_item(pane.clone(), pane, id, destination_index, cx)
1773 });
1774 })
1775 .ok()?;
1776
1777 Some(())
1778 });
1779 }
1780
1781 fn is_tab_pinned(&self, ix: usize) -> bool {
1782 self.pinned_tab_count > ix
1783 }
1784
1785 fn has_pinned_tabs(&self) -> bool {
1786 self.pinned_tab_count != 0
1787 }
1788
1789 fn render_tab(
1790 &self,
1791 ix: usize,
1792 item: &dyn ItemHandle,
1793 detail: usize,
1794 cx: &mut ViewContext<'_, Pane>,
1795 ) -> impl IntoElement {
1796 let project_path = item.project_path(cx);
1797
1798 let is_active = ix == self.active_item_index;
1799 let is_preview = self
1800 .preview_item_id
1801 .map(|id| id == item.item_id())
1802 .unwrap_or(false);
1803
1804 let label = item.tab_content(
1805 TabContentParams {
1806 detail: Some(detail),
1807 selected: is_active,
1808 preview: is_preview,
1809 },
1810 cx,
1811 );
1812
1813 let icon_color = if ItemSettings::get_global(cx).git_status {
1814 project_path
1815 .as_ref()
1816 .and_then(|path| self.project.read(cx).entry_for_path(&path, cx))
1817 .map(|entry| {
1818 Self::git_aware_icon_color(entry.git_status, entry.is_ignored, is_active)
1819 })
1820 .unwrap_or_else(|| Self::icon_color(is_active))
1821 } else {
1822 Self::icon_color(is_active)
1823 };
1824
1825 let icon = item.tab_icon(cx);
1826 let close_side = &ItemSettings::get_global(cx).close_position;
1827 let indicator = render_item_indicator(item.boxed_clone(), cx);
1828 let item_id = item.item_id();
1829 let is_first_item = ix == 0;
1830 let is_last_item = ix == self.items.len() - 1;
1831 let is_pinned = self.is_tab_pinned(ix);
1832 let position_relative_to_active_item = ix.cmp(&self.active_item_index);
1833
1834 let tab = Tab::new(ix)
1835 .position(if is_first_item {
1836 TabPosition::First
1837 } else if is_last_item {
1838 TabPosition::Last
1839 } else {
1840 TabPosition::Middle(position_relative_to_active_item)
1841 })
1842 .close_side(match close_side {
1843 ClosePosition::Left => ui::TabCloseSide::Start,
1844 ClosePosition::Right => ui::TabCloseSide::End,
1845 })
1846 .selected(is_active)
1847 .on_click(
1848 cx.listener(move |pane: &mut Self, _, cx| pane.activate_item(ix, true, true, cx)),
1849 )
1850 // TODO: This should be a click listener with the middle mouse button instead of a mouse down listener.
1851 .on_mouse_down(
1852 MouseButton::Middle,
1853 cx.listener(move |pane, _event, cx| {
1854 pane.close_item_by_id(item_id, SaveIntent::Close, cx)
1855 .detach_and_log_err(cx);
1856 }),
1857 )
1858 .on_mouse_down(
1859 MouseButton::Left,
1860 cx.listener(move |pane, event: &MouseDownEvent, cx| {
1861 if let Some(id) = pane.preview_item_id {
1862 if id == item_id && event.click_count > 1 {
1863 pane.set_preview_item_id(None, cx);
1864 }
1865 }
1866 }),
1867 )
1868 .on_drag(
1869 DraggedTab {
1870 item: item.boxed_clone(),
1871 pane: cx.view().clone(),
1872 detail,
1873 is_active,
1874 ix,
1875 },
1876 |tab, cx| cx.new_view(|_| tab.clone()),
1877 )
1878 .drag_over::<DraggedTab>(|tab, _, cx| {
1879 tab.bg(cx.theme().colors().drop_target_background)
1880 })
1881 .drag_over::<DraggedSelection>(|tab, _, cx| {
1882 tab.bg(cx.theme().colors().drop_target_background)
1883 })
1884 .when_some(self.can_drop_predicate.clone(), |this, p| {
1885 this.can_drop(move |a, cx| p(a, cx))
1886 })
1887 .on_drop(cx.listener(move |this, dragged_tab: &DraggedTab, cx| {
1888 this.drag_split_direction = None;
1889 this.handle_tab_drop(dragged_tab, ix, cx)
1890 }))
1891 .on_drop(cx.listener(move |this, selection: &DraggedSelection, cx| {
1892 this.drag_split_direction = None;
1893 this.handle_dragged_selection_drop(selection, cx)
1894 }))
1895 .on_drop(cx.listener(move |this, paths, cx| {
1896 this.drag_split_direction = None;
1897 this.handle_external_paths_drop(paths, cx)
1898 }))
1899 .when_some(item.tab_tooltip_text(cx), |tab, text| {
1900 tab.tooltip(move |cx| Tooltip::text(text.clone(), cx))
1901 })
1902 .start_slot::<Indicator>(indicator)
1903 .map(|this| {
1904 let end_slot = if is_pinned {
1905 IconButton::new("unpin tab", IconName::PinAlt)
1906 .shape(IconButtonShape::Square)
1907 .icon_color(Color::Muted)
1908 .size(ButtonSize::None)
1909 .icon_size(IconSize::XSmall)
1910 .on_click(cx.listener(move |pane, _, cx| {
1911 pane.unpin_tab_at(ix, cx);
1912 }))
1913 .tooltip(|cx| Tooltip::text("Unpin Tab", cx))
1914 } else {
1915 IconButton::new("close tab", IconName::Close)
1916 .visible_on_hover("")
1917 .shape(IconButtonShape::Square)
1918 .icon_color(Color::Muted)
1919 .size(ButtonSize::None)
1920 .icon_size(IconSize::XSmall)
1921 .on_click(cx.listener(move |pane, _, cx| {
1922 pane.close_item_by_id(item_id, SaveIntent::Close, cx)
1923 .detach_and_log_err(cx);
1924 }))
1925 };
1926 this.end_slot(end_slot)
1927 })
1928 .child(
1929 h_flex()
1930 .gap_1()
1931 .children(icon.map(|icon| icon.size(IconSize::Small).color(icon_color)))
1932 .child(label),
1933 );
1934
1935 let single_entry_to_resolve = {
1936 let item_entries = self.items[ix].project_entry_ids(cx);
1937 if item_entries.len() == 1 {
1938 Some(item_entries[0])
1939 } else {
1940 None
1941 }
1942 };
1943
1944 let is_pinned = self.is_tab_pinned(ix);
1945 let pane = cx.view().downgrade();
1946 right_click_menu(ix).trigger(tab).menu(move |cx| {
1947 let pane = pane.clone();
1948 ContextMenu::build(cx, move |mut menu, cx| {
1949 if let Some(pane) = pane.upgrade() {
1950 menu = menu
1951 .entry(
1952 "Close",
1953 Some(Box::new(CloseActiveItem { save_intent: None })),
1954 cx.handler_for(&pane, move |pane, cx| {
1955 pane.close_item_by_id(item_id, SaveIntent::Close, cx)
1956 .detach_and_log_err(cx);
1957 }),
1958 )
1959 .entry(
1960 "Close Others",
1961 Some(Box::new(CloseInactiveItems { save_intent: None })),
1962 cx.handler_for(&pane, move |pane, cx| {
1963 pane.close_items(cx, SaveIntent::Close, |id| id != item_id)
1964 .detach_and_log_err(cx);
1965 }),
1966 )
1967 .separator()
1968 .entry(
1969 "Close Left",
1970 Some(Box::new(CloseItemsToTheLeft)),
1971 cx.handler_for(&pane, move |pane, cx| {
1972 pane.close_items_to_the_left_by_id(item_id, cx)
1973 .detach_and_log_err(cx);
1974 }),
1975 )
1976 .entry(
1977 "Close Right",
1978 Some(Box::new(CloseItemsToTheRight)),
1979 cx.handler_for(&pane, move |pane, cx| {
1980 pane.close_items_to_the_right_by_id(item_id, cx)
1981 .detach_and_log_err(cx);
1982 }),
1983 )
1984 .separator()
1985 .entry(
1986 "Close Clean",
1987 Some(Box::new(CloseCleanItems)),
1988 cx.handler_for(&pane, move |pane, cx| {
1989 if let Some(task) = pane.close_clean_items(&CloseCleanItems, cx) {
1990 task.detach_and_log_err(cx)
1991 }
1992 }),
1993 )
1994 .entry(
1995 "Close All",
1996 Some(Box::new(CloseAllItems { save_intent: None })),
1997 cx.handler_for(&pane, |pane, cx| {
1998 if let Some(task) =
1999 pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
2000 {
2001 task.detach_and_log_err(cx)
2002 }
2003 }),
2004 );
2005
2006 let pin_tab_entries = |menu: ContextMenu| {
2007 menu.separator().map(|this| {
2008 if is_pinned {
2009 this.entry(
2010 "Unpin Tab",
2011 Some(TogglePinTab.boxed_clone()),
2012 cx.handler_for(&pane, move |pane, cx| {
2013 pane.unpin_tab_at(ix, cx);
2014 }),
2015 )
2016 } else {
2017 this.entry(
2018 "Pin Tab",
2019 Some(TogglePinTab.boxed_clone()),
2020 cx.handler_for(&pane, move |pane, cx| {
2021 pane.pin_tab_at(ix, cx);
2022 }),
2023 )
2024 }
2025 })
2026 };
2027 if let Some(entry) = single_entry_to_resolve {
2028 let entry_abs_path = pane.read(cx).entry_abs_path(entry, cx);
2029 let parent_abs_path = entry_abs_path
2030 .as_deref()
2031 .and_then(|abs_path| Some(abs_path.parent()?.to_path_buf()));
2032
2033 let entry_id = entry.to_proto();
2034 menu = menu
2035 .separator()
2036 .when_some(entry_abs_path, |menu, abs_path| {
2037 menu.entry(
2038 "Copy Path",
2039 Some(Box::new(CopyPath)),
2040 cx.handler_for(&pane, move |_, cx| {
2041 cx.write_to_clipboard(ClipboardItem::new_string(
2042 abs_path.to_string_lossy().to_string(),
2043 ));
2044 }),
2045 )
2046 })
2047 .entry(
2048 "Copy Relative Path",
2049 Some(Box::new(CopyRelativePath)),
2050 cx.handler_for(&pane, move |pane, cx| {
2051 pane.copy_relative_path(&CopyRelativePath, cx);
2052 }),
2053 )
2054 .map(pin_tab_entries)
2055 .separator()
2056 .entry(
2057 "Reveal In Project Panel",
2058 Some(Box::new(RevealInProjectPanel {
2059 entry_id: Some(entry_id),
2060 })),
2061 cx.handler_for(&pane, move |pane, cx| {
2062 pane.project.update(cx, |_, cx| {
2063 cx.emit(project::Event::RevealInProjectPanel(
2064 ProjectEntryId::from_proto(entry_id),
2065 ))
2066 });
2067 }),
2068 )
2069 .when_some(parent_abs_path, |menu, parent_abs_path| {
2070 menu.entry(
2071 "Open in Terminal",
2072 Some(Box::new(OpenInTerminal)),
2073 cx.handler_for(&pane, move |_, cx| {
2074 cx.dispatch_action(
2075 OpenTerminal {
2076 working_directory: parent_abs_path.clone(),
2077 }
2078 .boxed_clone(),
2079 );
2080 }),
2081 )
2082 });
2083 } else {
2084 menu = menu.map(pin_tab_entries);
2085 }
2086 }
2087
2088 menu
2089 })
2090 })
2091 }
2092
2093 fn render_tab_bar(&mut self, cx: &mut ViewContext<'_, Pane>) -> impl IntoElement {
2094 let focus_handle = self.focus_handle.clone();
2095 let navigate_backward = IconButton::new("navigate_backward", IconName::ArrowLeft)
2096 .shape(IconButtonShape::Square)
2097 .icon_size(IconSize::Small)
2098 .on_click({
2099 let view = cx.view().clone();
2100 move |_, cx| view.update(cx, Self::navigate_backward)
2101 })
2102 .disabled(!self.can_navigate_backward())
2103 .tooltip({
2104 let focus_handle = focus_handle.clone();
2105 move |cx| Tooltip::for_action_in("Go Back", &GoBack, &focus_handle, cx)
2106 });
2107
2108 let navigate_forward = IconButton::new("navigate_forward", IconName::ArrowRight)
2109 .shape(IconButtonShape::Square)
2110 .icon_size(IconSize::Small)
2111 .on_click({
2112 let view = cx.view().clone();
2113 move |_, cx| view.update(cx, Self::navigate_forward)
2114 })
2115 .disabled(!self.can_navigate_forward())
2116 .tooltip({
2117 let focus_handle = focus_handle.clone();
2118 move |cx| Tooltip::for_action_in("Go Forward", &GoForward, &focus_handle, cx)
2119 });
2120
2121 let mut tab_items = self
2122 .items
2123 .iter()
2124 .enumerate()
2125 .zip(tab_details(&self.items, cx))
2126 .map(|((ix, item), detail)| self.render_tab(ix, &**item, detail, cx))
2127 .collect::<Vec<_>>();
2128
2129 let unpinned_tabs = tab_items.split_off(self.pinned_tab_count);
2130 let pinned_tabs = tab_items;
2131 TabBar::new("tab_bar")
2132 .when(
2133 self.display_nav_history_buttons.unwrap_or_default(),
2134 |tab_bar| {
2135 tab_bar
2136 .start_child(navigate_backward)
2137 .start_child(navigate_forward)
2138 },
2139 )
2140 .map(|tab_bar| {
2141 let render_tab_buttons = self.render_tab_bar_buttons.clone();
2142 let (left_children, right_children) = render_tab_buttons(self, cx);
2143
2144 tab_bar
2145 .start_children(left_children)
2146 .end_children(right_children)
2147 })
2148 .children(pinned_tabs.len().ne(&0).then(|| {
2149 h_flex()
2150 .children(pinned_tabs)
2151 .border_r_2()
2152 .border_color(cx.theme().colors().border)
2153 }))
2154 .child(
2155 h_flex()
2156 .id("unpinned tabs")
2157 .overflow_x_scroll()
2158 .w_full()
2159 .track_scroll(&self.tab_bar_scroll_handle)
2160 .children(unpinned_tabs)
2161 .child(
2162 div()
2163 .id("tab_bar_drop_target")
2164 .min_w_6()
2165 // HACK: This empty child is currently necessary to force the drop target to appear
2166 // despite us setting a min width above.
2167 .child("")
2168 .h_full()
2169 .flex_grow()
2170 .drag_over::<DraggedTab>(|bar, _, cx| {
2171 bar.bg(cx.theme().colors().drop_target_background)
2172 })
2173 .drag_over::<DraggedSelection>(|bar, _, cx| {
2174 bar.bg(cx.theme().colors().drop_target_background)
2175 })
2176 .on_drop(cx.listener(move |this, dragged_tab: &DraggedTab, cx| {
2177 this.drag_split_direction = None;
2178 this.handle_tab_drop(dragged_tab, this.items.len(), cx)
2179 }))
2180 .on_drop(cx.listener(move |this, selection: &DraggedSelection, cx| {
2181 this.drag_split_direction = None;
2182 this.handle_project_entry_drop(
2183 &selection.active_selection.entry_id,
2184 cx,
2185 )
2186 }))
2187 .on_drop(cx.listener(move |this, paths, cx| {
2188 this.drag_split_direction = None;
2189 this.handle_external_paths_drop(paths, cx)
2190 }))
2191 .on_click(cx.listener(move |this, event: &ClickEvent, cx| {
2192 if event.up.click_count == 2 {
2193 cx.dispatch_action(
2194 this.double_click_dispatch_action.boxed_clone(),
2195 )
2196 }
2197 })),
2198 ),
2199 )
2200 }
2201
2202 pub fn render_menu_overlay(menu: &View<ContextMenu>) -> Div {
2203 div().absolute().bottom_0().right_0().size_0().child(
2204 deferred(
2205 anchored()
2206 .anchor(AnchorCorner::TopRight)
2207 .child(menu.clone()),
2208 )
2209 .with_priority(1),
2210 )
2211 }
2212
2213 pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
2214 self.zoomed = zoomed;
2215 cx.notify();
2216 }
2217
2218 pub fn is_zoomed(&self) -> bool {
2219 self.zoomed
2220 }
2221
2222 fn handle_drag_move<T>(&mut self, event: &DragMoveEvent<T>, cx: &mut ViewContext<Self>) {
2223 if !self.can_split {
2224 return;
2225 }
2226
2227 let rect = event.bounds.size;
2228
2229 let size = event.bounds.size.width.min(event.bounds.size.height)
2230 * WorkspaceSettings::get_global(cx).drop_target_size;
2231
2232 let relative_cursor = Point::new(
2233 event.event.position.x - event.bounds.left(),
2234 event.event.position.y - event.bounds.top(),
2235 );
2236
2237 let direction = if relative_cursor.x < size
2238 || relative_cursor.x > rect.width - size
2239 || relative_cursor.y < size
2240 || relative_cursor.y > rect.height - size
2241 {
2242 [
2243 SplitDirection::Up,
2244 SplitDirection::Right,
2245 SplitDirection::Down,
2246 SplitDirection::Left,
2247 ]
2248 .iter()
2249 .min_by_key(|side| match side {
2250 SplitDirection::Up => relative_cursor.y,
2251 SplitDirection::Right => rect.width - relative_cursor.x,
2252 SplitDirection::Down => rect.height - relative_cursor.y,
2253 SplitDirection::Left => relative_cursor.x,
2254 })
2255 .cloned()
2256 } else {
2257 None
2258 };
2259
2260 if direction != self.drag_split_direction {
2261 self.drag_split_direction = direction;
2262 }
2263 }
2264
2265 fn handle_tab_drop(
2266 &mut self,
2267 dragged_tab: &DraggedTab,
2268 ix: usize,
2269 cx: &mut ViewContext<'_, Self>,
2270 ) {
2271 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2272 if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_tab, cx) {
2273 return;
2274 }
2275 }
2276 let mut to_pane = cx.view().clone();
2277 let split_direction = self.drag_split_direction;
2278 let item_id = dragged_tab.item.item_id();
2279 if let Some(preview_item_id) = self.preview_item_id {
2280 if item_id == preview_item_id {
2281 self.set_preview_item_id(None, cx);
2282 }
2283 }
2284
2285 let from_pane = dragged_tab.pane.clone();
2286 self.workspace
2287 .update(cx, |_, cx| {
2288 cx.defer(move |workspace, cx| {
2289 if let Some(split_direction) = split_direction {
2290 to_pane = workspace.split_pane(to_pane, split_direction, cx);
2291 }
2292 let old_ix = from_pane.read(cx).index_for_item_id(item_id);
2293 if to_pane == from_pane {
2294 if let Some(old_index) = old_ix {
2295 to_pane.update(cx, |this, _| {
2296 if old_index < this.pinned_tab_count
2297 && (ix == this.items.len() || ix > this.pinned_tab_count)
2298 {
2299 this.pinned_tab_count -= 1;
2300 } else if this.has_pinned_tabs()
2301 && old_index >= this.pinned_tab_count
2302 && ix < this.pinned_tab_count
2303 {
2304 this.pinned_tab_count += 1;
2305 }
2306 });
2307 }
2308 } else {
2309 to_pane.update(cx, |this, _| {
2310 if this.has_pinned_tabs() && ix < this.pinned_tab_count {
2311 this.pinned_tab_count += 1;
2312 }
2313 });
2314 from_pane.update(cx, |this, _| {
2315 if let Some(index) = old_ix {
2316 if this.pinned_tab_count > index {
2317 this.pinned_tab_count -= 1;
2318 }
2319 }
2320 })
2321 }
2322 workspace.move_item(from_pane.clone(), to_pane.clone(), item_id, ix, cx);
2323 });
2324 })
2325 .log_err();
2326 }
2327
2328 fn handle_dragged_selection_drop(
2329 &mut self,
2330 dragged_selection: &DraggedSelection,
2331 cx: &mut ViewContext<'_, Self>,
2332 ) {
2333 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2334 if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_selection, cx) {
2335 return;
2336 }
2337 }
2338 self.handle_project_entry_drop(&dragged_selection.active_selection.entry_id, cx);
2339 }
2340
2341 fn handle_project_entry_drop(
2342 &mut self,
2343 project_entry_id: &ProjectEntryId,
2344 cx: &mut ViewContext<'_, Self>,
2345 ) {
2346 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2347 if let ControlFlow::Break(()) = custom_drop_handle(self, project_entry_id, cx) {
2348 return;
2349 }
2350 }
2351 let mut to_pane = cx.view().clone();
2352 let split_direction = self.drag_split_direction;
2353 let project_entry_id = *project_entry_id;
2354 self.workspace
2355 .update(cx, |_, cx| {
2356 cx.defer(move |workspace, cx| {
2357 if let Some(path) = workspace
2358 .project()
2359 .read(cx)
2360 .path_for_entry(project_entry_id, cx)
2361 {
2362 let load_path_task = workspace.load_path(path, cx);
2363 cx.spawn(|workspace, mut cx| async move {
2364 if let Some((project_entry_id, build_item)) =
2365 load_path_task.await.notify_async_err(&mut cx)
2366 {
2367 let (to_pane, new_item_handle) = workspace
2368 .update(&mut cx, |workspace, cx| {
2369 if let Some(split_direction) = split_direction {
2370 to_pane =
2371 workspace.split_pane(to_pane, split_direction, cx);
2372 }
2373 let new_item_handle = to_pane.update(cx, |pane, cx| {
2374 pane.open_item(
2375 project_entry_id,
2376 true,
2377 false,
2378 cx,
2379 build_item,
2380 )
2381 });
2382 (to_pane, new_item_handle)
2383 })
2384 .log_err()?;
2385 to_pane
2386 .update(&mut cx, |this, cx| {
2387 let Some(index) = this.index_for_item(&*new_item_handle)
2388 else {
2389 return;
2390 };
2391 if !this.is_tab_pinned(index) {
2392 this.pin_tab_at(index, cx);
2393 }
2394 })
2395 .ok()?
2396 }
2397 Some(())
2398 })
2399 .detach();
2400 };
2401 });
2402 })
2403 .log_err();
2404 }
2405
2406 fn handle_external_paths_drop(
2407 &mut self,
2408 paths: &ExternalPaths,
2409 cx: &mut ViewContext<'_, Self>,
2410 ) {
2411 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2412 if let ControlFlow::Break(()) = custom_drop_handle(self, paths, cx) {
2413 return;
2414 }
2415 }
2416 let mut to_pane = cx.view().clone();
2417 let mut split_direction = self.drag_split_direction;
2418 let paths = paths.paths().to_vec();
2419 let is_remote = self
2420 .workspace
2421 .update(cx, |workspace, cx| {
2422 if workspace.project().read(cx).is_via_collab() {
2423 workspace.show_error(
2424 &anyhow::anyhow!("Cannot drop files on a remote project"),
2425 cx,
2426 );
2427 true
2428 } else {
2429 false
2430 }
2431 })
2432 .unwrap_or(true);
2433 if is_remote {
2434 return;
2435 }
2436
2437 self.workspace
2438 .update(cx, |workspace, cx| {
2439 let fs = Arc::clone(workspace.project().read(cx).fs());
2440 cx.spawn(|workspace, mut cx| async move {
2441 let mut is_file_checks = FuturesUnordered::new();
2442 for path in &paths {
2443 is_file_checks.push(fs.is_file(path))
2444 }
2445 let mut has_files_to_open = false;
2446 while let Some(is_file) = is_file_checks.next().await {
2447 if is_file {
2448 has_files_to_open = true;
2449 break;
2450 }
2451 }
2452 drop(is_file_checks);
2453 if !has_files_to_open {
2454 split_direction = None;
2455 }
2456
2457 if let Some(open_task) = workspace
2458 .update(&mut cx, |workspace, cx| {
2459 if let Some(split_direction) = split_direction {
2460 to_pane = workspace.split_pane(to_pane, split_direction, cx);
2461 }
2462 workspace.open_paths(
2463 paths,
2464 OpenVisible::OnlyDirectories,
2465 Some(to_pane.downgrade()),
2466 cx,
2467 )
2468 })
2469 .ok()
2470 {
2471 let opened_items: Vec<_> = open_task.await;
2472 _ = workspace.update(&mut cx, |workspace, cx| {
2473 for item in opened_items.into_iter().flatten() {
2474 if let Err(e) = item {
2475 workspace.show_error(&e, cx);
2476 }
2477 }
2478 });
2479 }
2480 })
2481 .detach();
2482 })
2483 .log_err();
2484 }
2485
2486 pub fn display_nav_history_buttons(&mut self, display: Option<bool>) {
2487 self.display_nav_history_buttons = display;
2488 }
2489}
2490
2491impl FocusableView for Pane {
2492 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
2493 self.focus_handle.clone()
2494 }
2495}
2496
2497impl Render for Pane {
2498 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2499 let mut key_context = KeyContext::new_with_defaults();
2500 key_context.add("Pane");
2501 if self.active_item().is_none() {
2502 key_context.add("EmptyPane");
2503 }
2504
2505 let should_display_tab_bar = self.should_display_tab_bar.clone();
2506 let display_tab_bar = should_display_tab_bar(cx);
2507
2508 v_flex()
2509 .key_context(key_context)
2510 .track_focus(&self.focus_handle)
2511 .size_full()
2512 .flex_none()
2513 .overflow_hidden()
2514 .on_action(cx.listener(|pane, _: &AlternateFile, cx| {
2515 pane.alternate_file(cx);
2516 }))
2517 .on_action(cx.listener(|pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx)))
2518 .on_action(cx.listener(|pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx)))
2519 .on_action(cx.listener(|pane, _: &SplitHorizontal, cx| {
2520 pane.split(SplitDirection::horizontal(cx), cx)
2521 }))
2522 .on_action(cx.listener(|pane, _: &SplitVertical, cx| {
2523 pane.split(SplitDirection::vertical(cx), cx)
2524 }))
2525 .on_action(
2526 cx.listener(|pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx)),
2527 )
2528 .on_action(cx.listener(|pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx)))
2529 .on_action(cx.listener(|pane, _: &GoBack, cx| pane.navigate_backward(cx)))
2530 .on_action(cx.listener(|pane, _: &GoForward, cx| pane.navigate_forward(cx)))
2531 .on_action(cx.listener(|pane, _: &JoinIntoNext, cx| pane.join_into_next(cx)))
2532 .on_action(cx.listener(Pane::toggle_zoom))
2533 .on_action(cx.listener(|pane: &mut Pane, action: &ActivateItem, cx| {
2534 pane.activate_item(action.0, true, true, cx);
2535 }))
2536 .on_action(cx.listener(|pane: &mut Pane, _: &ActivateLastItem, cx| {
2537 pane.activate_item(pane.items.len() - 1, true, true, cx);
2538 }))
2539 .on_action(cx.listener(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
2540 pane.activate_prev_item(true, cx);
2541 }))
2542 .on_action(cx.listener(|pane: &mut Pane, _: &ActivateNextItem, cx| {
2543 pane.activate_next_item(true, cx);
2544 }))
2545 .on_action(cx.listener(|pane, action, cx| {
2546 pane.toggle_pin_tab(action, cx);
2547 }))
2548 .when(PreviewTabsSettings::get_global(cx).enabled, |this| {
2549 this.on_action(cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, cx| {
2550 if let Some(active_item_id) = pane.active_item().map(|i| i.item_id()) {
2551 if pane.is_active_preview_item(active_item_id) {
2552 pane.set_preview_item_id(None, cx);
2553 } else {
2554 pane.set_preview_item_id(Some(active_item_id), cx);
2555 }
2556 }
2557 }))
2558 })
2559 .on_action(
2560 cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
2561 if let Some(task) = pane.close_active_item(action, cx) {
2562 task.detach_and_log_err(cx)
2563 }
2564 }),
2565 )
2566 .on_action(
2567 cx.listener(|pane: &mut Self, action: &CloseInactiveItems, cx| {
2568 if let Some(task) = pane.close_inactive_items(action, cx) {
2569 task.detach_and_log_err(cx)
2570 }
2571 }),
2572 )
2573 .on_action(
2574 cx.listener(|pane: &mut Self, action: &CloseCleanItems, cx| {
2575 if let Some(task) = pane.close_clean_items(action, cx) {
2576 task.detach_and_log_err(cx)
2577 }
2578 }),
2579 )
2580 .on_action(
2581 cx.listener(|pane: &mut Self, action: &CloseItemsToTheLeft, cx| {
2582 if let Some(task) = pane.close_items_to_the_left(action, cx) {
2583 task.detach_and_log_err(cx)
2584 }
2585 }),
2586 )
2587 .on_action(
2588 cx.listener(|pane: &mut Self, action: &CloseItemsToTheRight, cx| {
2589 if let Some(task) = pane.close_items_to_the_right(action, cx) {
2590 task.detach_and_log_err(cx)
2591 }
2592 }),
2593 )
2594 .on_action(cx.listener(|pane: &mut Self, action: &CloseAllItems, cx| {
2595 if let Some(task) = pane.close_all_items(action, cx) {
2596 task.detach_and_log_err(cx)
2597 }
2598 }))
2599 .on_action(
2600 cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
2601 if let Some(task) = pane.close_active_item(action, cx) {
2602 task.detach_and_log_err(cx)
2603 }
2604 }),
2605 )
2606 .on_action(
2607 cx.listener(|pane: &mut Self, action: &RevealInProjectPanel, cx| {
2608 let entry_id = action
2609 .entry_id
2610 .map(ProjectEntryId::from_proto)
2611 .or_else(|| pane.active_item()?.project_entry_ids(cx).first().copied());
2612 if let Some(entry_id) = entry_id {
2613 pane.project.update(cx, |_, cx| {
2614 cx.emit(project::Event::RevealInProjectPanel(entry_id))
2615 });
2616 }
2617 }),
2618 )
2619 .when(self.active_item().is_some() && display_tab_bar, |pane| {
2620 pane.child(self.render_tab_bar(cx))
2621 })
2622 .child({
2623 let has_worktrees = self.project.read(cx).worktrees(cx).next().is_some();
2624 // main content
2625 div()
2626 .flex_1()
2627 .relative()
2628 .group("")
2629 .on_drag_move::<DraggedTab>(cx.listener(Self::handle_drag_move))
2630 .on_drag_move::<DraggedSelection>(cx.listener(Self::handle_drag_move))
2631 .on_drag_move::<ExternalPaths>(cx.listener(Self::handle_drag_move))
2632 .map(|div| {
2633 if let Some(item) = self.active_item() {
2634 div.v_flex()
2635 .child(self.toolbar.clone())
2636 .child(item.to_any())
2637 } else {
2638 let placeholder = div.h_flex().size_full().justify_center();
2639 if has_worktrees {
2640 placeholder
2641 } else {
2642 placeholder.child(
2643 Label::new("Open a file or project to get started.")
2644 .color(Color::Muted),
2645 )
2646 }
2647 }
2648 })
2649 .child(
2650 // drag target
2651 div()
2652 .invisible()
2653 .absolute()
2654 .bg(cx.theme().colors().drop_target_background)
2655 .group_drag_over::<DraggedTab>("", |style| style.visible())
2656 .group_drag_over::<DraggedSelection>("", |style| style.visible())
2657 .group_drag_over::<ExternalPaths>("", |style| style.visible())
2658 .when_some(self.can_drop_predicate.clone(), |this, p| {
2659 this.can_drop(move |a, cx| p(a, cx))
2660 })
2661 .on_drop(cx.listener(move |this, dragged_tab, cx| {
2662 this.handle_tab_drop(dragged_tab, this.active_item_index(), cx)
2663 }))
2664 .on_drop(cx.listener(move |this, selection: &DraggedSelection, cx| {
2665 this.handle_dragged_selection_drop(selection, cx)
2666 }))
2667 .on_drop(cx.listener(move |this, paths, cx| {
2668 this.handle_external_paths_drop(paths, cx)
2669 }))
2670 .map(|div| {
2671 let size = DefiniteLength::Fraction(0.5);
2672 match self.drag_split_direction {
2673 None => div.top_0().right_0().bottom_0().left_0(),
2674 Some(SplitDirection::Up) => {
2675 div.top_0().left_0().right_0().h(size)
2676 }
2677 Some(SplitDirection::Down) => {
2678 div.left_0().bottom_0().right_0().h(size)
2679 }
2680 Some(SplitDirection::Left) => {
2681 div.top_0().left_0().bottom_0().w(size)
2682 }
2683 Some(SplitDirection::Right) => {
2684 div.top_0().bottom_0().right_0().w(size)
2685 }
2686 }
2687 }),
2688 )
2689 })
2690 .on_mouse_down(
2691 MouseButton::Navigate(NavigationDirection::Back),
2692 cx.listener(|pane, _, cx| {
2693 if let Some(workspace) = pane.workspace.upgrade() {
2694 let pane = cx.view().downgrade();
2695 cx.window_context().defer(move |cx| {
2696 workspace.update(cx, |workspace, cx| {
2697 workspace.go_back(pane, cx).detach_and_log_err(cx)
2698 })
2699 })
2700 }
2701 }),
2702 )
2703 .on_mouse_down(
2704 MouseButton::Navigate(NavigationDirection::Forward),
2705 cx.listener(|pane, _, cx| {
2706 if let Some(workspace) = pane.workspace.upgrade() {
2707 let pane = cx.view().downgrade();
2708 cx.window_context().defer(move |cx| {
2709 workspace.update(cx, |workspace, cx| {
2710 workspace.go_forward(pane, cx).detach_and_log_err(cx)
2711 })
2712 })
2713 }
2714 }),
2715 )
2716 }
2717}
2718
2719impl ItemNavHistory {
2720 pub fn push<D: 'static + Send + Any>(&mut self, data: Option<D>, cx: &mut WindowContext) {
2721 self.history
2722 .push(data, self.item.clone(), self.is_preview, cx);
2723 }
2724
2725 pub fn pop_backward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
2726 self.history.pop(NavigationMode::GoingBack, cx)
2727 }
2728
2729 pub fn pop_forward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
2730 self.history.pop(NavigationMode::GoingForward, cx)
2731 }
2732}
2733
2734impl NavHistory {
2735 pub fn for_each_entry(
2736 &self,
2737 cx: &AppContext,
2738 mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
2739 ) {
2740 let borrowed_history = self.0.lock();
2741 borrowed_history
2742 .forward_stack
2743 .iter()
2744 .chain(borrowed_history.backward_stack.iter())
2745 .chain(borrowed_history.closed_stack.iter())
2746 .for_each(|entry| {
2747 if let Some(project_and_abs_path) =
2748 borrowed_history.paths_by_item.get(&entry.item.id())
2749 {
2750 f(entry, project_and_abs_path.clone());
2751 } else if let Some(item) = entry.item.upgrade() {
2752 if let Some(path) = item.project_path(cx) {
2753 f(entry, (path, None));
2754 }
2755 }
2756 })
2757 }
2758
2759 pub fn set_mode(&mut self, mode: NavigationMode) {
2760 self.0.lock().mode = mode;
2761 }
2762
2763 pub fn mode(&self) -> NavigationMode {
2764 self.0.lock().mode
2765 }
2766
2767 pub fn disable(&mut self) {
2768 self.0.lock().mode = NavigationMode::Disabled;
2769 }
2770
2771 pub fn enable(&mut self) {
2772 self.0.lock().mode = NavigationMode::Normal;
2773 }
2774
2775 pub fn pop(&mut self, mode: NavigationMode, cx: &mut WindowContext) -> Option<NavigationEntry> {
2776 let mut state = self.0.lock();
2777 let entry = match mode {
2778 NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
2779 return None
2780 }
2781 NavigationMode::GoingBack => &mut state.backward_stack,
2782 NavigationMode::GoingForward => &mut state.forward_stack,
2783 NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
2784 }
2785 .pop_back();
2786 if entry.is_some() {
2787 state.did_update(cx);
2788 }
2789 entry
2790 }
2791
2792 pub fn push<D: 'static + Send + Any>(
2793 &mut self,
2794 data: Option<D>,
2795 item: Arc<dyn WeakItemHandle>,
2796 is_preview: bool,
2797 cx: &mut WindowContext,
2798 ) {
2799 let state = &mut *self.0.lock();
2800 match state.mode {
2801 NavigationMode::Disabled => {}
2802 NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
2803 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2804 state.backward_stack.pop_front();
2805 }
2806 state.backward_stack.push_back(NavigationEntry {
2807 item,
2808 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2809 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2810 is_preview,
2811 });
2812 state.forward_stack.clear();
2813 }
2814 NavigationMode::GoingBack => {
2815 if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2816 state.forward_stack.pop_front();
2817 }
2818 state.forward_stack.push_back(NavigationEntry {
2819 item,
2820 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2821 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2822 is_preview,
2823 });
2824 }
2825 NavigationMode::GoingForward => {
2826 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2827 state.backward_stack.pop_front();
2828 }
2829 state.backward_stack.push_back(NavigationEntry {
2830 item,
2831 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2832 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2833 is_preview,
2834 });
2835 }
2836 NavigationMode::ClosingItem => {
2837 if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2838 state.closed_stack.pop_front();
2839 }
2840 state.closed_stack.push_back(NavigationEntry {
2841 item,
2842 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2843 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2844 is_preview,
2845 });
2846 }
2847 }
2848 state.did_update(cx);
2849 }
2850
2851 pub fn remove_item(&mut self, item_id: EntityId) {
2852 let mut state = self.0.lock();
2853 state.paths_by_item.remove(&item_id);
2854 state
2855 .backward_stack
2856 .retain(|entry| entry.item.id() != item_id);
2857 state
2858 .forward_stack
2859 .retain(|entry| entry.item.id() != item_id);
2860 state
2861 .closed_stack
2862 .retain(|entry| entry.item.id() != item_id);
2863 }
2864
2865 pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option<PathBuf>)> {
2866 self.0.lock().paths_by_item.get(&item_id).cloned()
2867 }
2868}
2869
2870impl NavHistoryState {
2871 pub fn did_update(&self, cx: &mut WindowContext) {
2872 if let Some(pane) = self.pane.upgrade() {
2873 cx.defer(move |cx| {
2874 pane.update(cx, |pane, cx| pane.history_updated(cx));
2875 });
2876 }
2877 }
2878}
2879
2880fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
2881 let path = buffer_path
2882 .as_ref()
2883 .and_then(|p| {
2884 p.path
2885 .to_str()
2886 .and_then(|s| if s == "" { None } else { Some(s) })
2887 })
2888 .unwrap_or("This buffer");
2889 let path = truncate_and_remove_front(path, 80);
2890 format!("{path} contains unsaved edits. Do you want to save it?")
2891}
2892
2893pub fn tab_details(items: &Vec<Box<dyn ItemHandle>>, cx: &AppContext) -> Vec<usize> {
2894 let mut tab_details = items.iter().map(|_| 0).collect::<Vec<_>>();
2895 let mut tab_descriptions = HashMap::default();
2896 let mut done = false;
2897 while !done {
2898 done = true;
2899
2900 // Store item indices by their tab description.
2901 for (ix, (item, detail)) in items.iter().zip(&tab_details).enumerate() {
2902 if let Some(description) = item.tab_description(*detail, cx) {
2903 if *detail == 0
2904 || Some(&description) != item.tab_description(detail - 1, cx).as_ref()
2905 {
2906 tab_descriptions
2907 .entry(description)
2908 .or_insert(Vec::new())
2909 .push(ix);
2910 }
2911 }
2912 }
2913
2914 // If two or more items have the same tab description, increase their level
2915 // of detail and try again.
2916 for (_, item_ixs) in tab_descriptions.drain() {
2917 if item_ixs.len() > 1 {
2918 done = false;
2919 for ix in item_ixs {
2920 tab_details[ix] += 1;
2921 }
2922 }
2923 }
2924 }
2925
2926 tab_details
2927}
2928
2929pub fn render_item_indicator(item: Box<dyn ItemHandle>, cx: &WindowContext) -> Option<Indicator> {
2930 maybe!({
2931 let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
2932 (true, _) => Color::Warning,
2933 (_, true) => Color::Accent,
2934 (false, false) => return None,
2935 };
2936
2937 Some(Indicator::dot().color(indicator_color))
2938 })
2939}
2940
2941#[cfg(test)]
2942mod tests {
2943 use super::*;
2944 use crate::item::test::{TestItem, TestProjectItem};
2945 use gpui::{TestAppContext, VisualTestContext};
2946 use project::FakeFs;
2947 use settings::SettingsStore;
2948 use theme::LoadThemes;
2949
2950 #[gpui::test]
2951 async fn test_remove_active_empty(cx: &mut TestAppContext) {
2952 init_test(cx);
2953 let fs = FakeFs::new(cx.executor());
2954
2955 let project = Project::test(fs, None, cx).await;
2956 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2957 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2958
2959 pane.update(cx, |pane, cx| {
2960 assert!(pane
2961 .close_active_item(&CloseActiveItem { save_intent: None }, cx)
2962 .is_none())
2963 });
2964 }
2965
2966 #[gpui::test]
2967 async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
2968 init_test(cx);
2969 let fs = FakeFs::new(cx.executor());
2970
2971 let project = Project::test(fs, None, cx).await;
2972 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2973 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2974
2975 // 1. Add with a destination index
2976 // a. Add before the active item
2977 set_labeled_items(&pane, ["A", "B*", "C"], cx);
2978 pane.update(cx, |pane, cx| {
2979 pane.add_item(
2980 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2981 false,
2982 false,
2983 Some(0),
2984 cx,
2985 );
2986 });
2987 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
2988
2989 // b. Add after the active item
2990 set_labeled_items(&pane, ["A", "B*", "C"], cx);
2991 pane.update(cx, |pane, cx| {
2992 pane.add_item(
2993 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2994 false,
2995 false,
2996 Some(2),
2997 cx,
2998 );
2999 });
3000 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3001
3002 // c. Add at the end of the item list (including off the length)
3003 set_labeled_items(&pane, ["A", "B*", "C"], cx);
3004 pane.update(cx, |pane, cx| {
3005 pane.add_item(
3006 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3007 false,
3008 false,
3009 Some(5),
3010 cx,
3011 );
3012 });
3013 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3014
3015 // 2. Add without a destination index
3016 // a. Add with active item at the start of the item list
3017 set_labeled_items(&pane, ["A*", "B", "C"], cx);
3018 pane.update(cx, |pane, cx| {
3019 pane.add_item(
3020 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3021 false,
3022 false,
3023 None,
3024 cx,
3025 );
3026 });
3027 set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
3028
3029 // b. Add with active item at the end of the item list
3030 set_labeled_items(&pane, ["A", "B", "C*"], cx);
3031 pane.update(cx, |pane, cx| {
3032 pane.add_item(
3033 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3034 false,
3035 false,
3036 None,
3037 cx,
3038 );
3039 });
3040 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3041 }
3042
3043 #[gpui::test]
3044 async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
3045 init_test(cx);
3046 let fs = FakeFs::new(cx.executor());
3047
3048 let project = Project::test(fs, None, cx).await;
3049 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3050 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3051
3052 // 1. Add with a destination index
3053 // 1a. Add before the active item
3054 let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3055 pane.update(cx, |pane, cx| {
3056 pane.add_item(d, false, false, Some(0), cx);
3057 });
3058 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
3059
3060 // 1b. Add after the active item
3061 let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3062 pane.update(cx, |pane, cx| {
3063 pane.add_item(d, false, false, Some(2), cx);
3064 });
3065 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3066
3067 // 1c. Add at the end of the item list (including off the length)
3068 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3069 pane.update(cx, |pane, cx| {
3070 pane.add_item(a, false, false, Some(5), cx);
3071 });
3072 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3073
3074 // 1d. Add same item to active index
3075 let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3076 pane.update(cx, |pane, cx| {
3077 pane.add_item(b, false, false, Some(1), cx);
3078 });
3079 assert_item_labels(&pane, ["A", "B*", "C"], cx);
3080
3081 // 1e. Add item to index after same item in last position
3082 let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3083 pane.update(cx, |pane, cx| {
3084 pane.add_item(c, false, false, Some(2), cx);
3085 });
3086 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3087
3088 // 2. Add without a destination index
3089 // 2a. Add with active item at the start of the item list
3090 let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
3091 pane.update(cx, |pane, cx| {
3092 pane.add_item(d, false, false, None, cx);
3093 });
3094 assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
3095
3096 // 2b. Add with active item at the end of the item list
3097 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
3098 pane.update(cx, |pane, cx| {
3099 pane.add_item(a, false, false, None, cx);
3100 });
3101 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3102
3103 // 2c. Add active item to active item at end of list
3104 let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
3105 pane.update(cx, |pane, cx| {
3106 pane.add_item(c, false, false, None, cx);
3107 });
3108 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3109
3110 // 2d. Add active item to active item at start of list
3111 let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
3112 pane.update(cx, |pane, cx| {
3113 pane.add_item(a, false, false, None, cx);
3114 });
3115 assert_item_labels(&pane, ["A*", "B", "C"], cx);
3116 }
3117
3118 #[gpui::test]
3119 async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
3120 init_test(cx);
3121 let fs = FakeFs::new(cx.executor());
3122
3123 let project = Project::test(fs, None, cx).await;
3124 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3125 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3126
3127 // singleton view
3128 pane.update(cx, |pane, cx| {
3129 pane.add_item(
3130 Box::new(cx.new_view(|cx| {
3131 TestItem::new(cx)
3132 .with_singleton(true)
3133 .with_label("buffer 1")
3134 .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
3135 })),
3136 false,
3137 false,
3138 None,
3139 cx,
3140 );
3141 });
3142 assert_item_labels(&pane, ["buffer 1*"], cx);
3143
3144 // new singleton view with the same project entry
3145 pane.update(cx, |pane, cx| {
3146 pane.add_item(
3147 Box::new(cx.new_view(|cx| {
3148 TestItem::new(cx)
3149 .with_singleton(true)
3150 .with_label("buffer 1")
3151 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3152 })),
3153 false,
3154 false,
3155 None,
3156 cx,
3157 );
3158 });
3159 assert_item_labels(&pane, ["buffer 1*"], cx);
3160
3161 // new singleton view with different project entry
3162 pane.update(cx, |pane, cx| {
3163 pane.add_item(
3164 Box::new(cx.new_view(|cx| {
3165 TestItem::new(cx)
3166 .with_singleton(true)
3167 .with_label("buffer 2")
3168 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
3169 })),
3170 false,
3171 false,
3172 None,
3173 cx,
3174 );
3175 });
3176 assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
3177
3178 // new multibuffer view with the same project entry
3179 pane.update(cx, |pane, cx| {
3180 pane.add_item(
3181 Box::new(cx.new_view(|cx| {
3182 TestItem::new(cx)
3183 .with_singleton(false)
3184 .with_label("multibuffer 1")
3185 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3186 })),
3187 false,
3188 false,
3189 None,
3190 cx,
3191 );
3192 });
3193 assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
3194
3195 // another multibuffer view with the same project entry
3196 pane.update(cx, |pane, cx| {
3197 pane.add_item(
3198 Box::new(cx.new_view(|cx| {
3199 TestItem::new(cx)
3200 .with_singleton(false)
3201 .with_label("multibuffer 1b")
3202 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3203 })),
3204 false,
3205 false,
3206 None,
3207 cx,
3208 );
3209 });
3210 assert_item_labels(
3211 &pane,
3212 ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
3213 cx,
3214 );
3215 }
3216
3217 #[gpui::test]
3218 async fn test_remove_item_ordering(cx: &mut TestAppContext) {
3219 init_test(cx);
3220 let fs = FakeFs::new(cx.executor());
3221
3222 let project = Project::test(fs, None, cx).await;
3223 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3224 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3225
3226 add_labeled_item(&pane, "A", false, cx);
3227 add_labeled_item(&pane, "B", false, cx);
3228 add_labeled_item(&pane, "C", false, cx);
3229 add_labeled_item(&pane, "D", false, cx);
3230 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3231
3232 pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
3233 add_labeled_item(&pane, "1", false, cx);
3234 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
3235
3236 pane.update(cx, |pane, cx| {
3237 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3238 })
3239 .unwrap()
3240 .await
3241 .unwrap();
3242 assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
3243
3244 pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
3245 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3246
3247 pane.update(cx, |pane, cx| {
3248 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3249 })
3250 .unwrap()
3251 .await
3252 .unwrap();
3253 assert_item_labels(&pane, ["A", "B*", "C"], cx);
3254
3255 pane.update(cx, |pane, cx| {
3256 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3257 })
3258 .unwrap()
3259 .await
3260 .unwrap();
3261 assert_item_labels(&pane, ["A", "C*"], cx);
3262
3263 pane.update(cx, |pane, cx| {
3264 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3265 })
3266 .unwrap()
3267 .await
3268 .unwrap();
3269 assert_item_labels(&pane, ["A*"], cx);
3270 }
3271
3272 #[gpui::test]
3273 async fn test_close_inactive_items(cx: &mut TestAppContext) {
3274 init_test(cx);
3275 let fs = FakeFs::new(cx.executor());
3276
3277 let project = Project::test(fs, None, cx).await;
3278 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3279 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3280
3281 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
3282
3283 pane.update(cx, |pane, cx| {
3284 pane.close_inactive_items(&CloseInactiveItems { save_intent: None }, cx)
3285 })
3286 .unwrap()
3287 .await
3288 .unwrap();
3289 assert_item_labels(&pane, ["C*"], cx);
3290 }
3291
3292 #[gpui::test]
3293 async fn test_close_clean_items(cx: &mut TestAppContext) {
3294 init_test(cx);
3295 let fs = FakeFs::new(cx.executor());
3296
3297 let project = Project::test(fs, None, cx).await;
3298 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3299 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3300
3301 add_labeled_item(&pane, "A", true, cx);
3302 add_labeled_item(&pane, "B", false, cx);
3303 add_labeled_item(&pane, "C", true, cx);
3304 add_labeled_item(&pane, "D", false, cx);
3305 add_labeled_item(&pane, "E", false, cx);
3306 assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
3307
3308 pane.update(cx, |pane, cx| pane.close_clean_items(&CloseCleanItems, cx))
3309 .unwrap()
3310 .await
3311 .unwrap();
3312 assert_item_labels(&pane, ["A^", "C*^"], cx);
3313 }
3314
3315 #[gpui::test]
3316 async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
3317 init_test(cx);
3318 let fs = FakeFs::new(cx.executor());
3319
3320 let project = Project::test(fs, None, cx).await;
3321 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3322 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3323
3324 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
3325
3326 pane.update(cx, |pane, cx| {
3327 pane.close_items_to_the_left(&CloseItemsToTheLeft, cx)
3328 })
3329 .unwrap()
3330 .await
3331 .unwrap();
3332 assert_item_labels(&pane, ["C*", "D", "E"], cx);
3333 }
3334
3335 #[gpui::test]
3336 async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
3337 init_test(cx);
3338 let fs = FakeFs::new(cx.executor());
3339
3340 let project = Project::test(fs, None, cx).await;
3341 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3342 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3343
3344 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
3345
3346 pane.update(cx, |pane, cx| {
3347 pane.close_items_to_the_right(&CloseItemsToTheRight, cx)
3348 })
3349 .unwrap()
3350 .await
3351 .unwrap();
3352 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3353 }
3354
3355 #[gpui::test]
3356 async fn test_close_all_items(cx: &mut TestAppContext) {
3357 init_test(cx);
3358 let fs = FakeFs::new(cx.executor());
3359
3360 let project = Project::test(fs, None, cx).await;
3361 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3362 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3363
3364 add_labeled_item(&pane, "A", false, cx);
3365 add_labeled_item(&pane, "B", false, cx);
3366 add_labeled_item(&pane, "C", false, cx);
3367 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3368
3369 pane.update(cx, |pane, cx| {
3370 pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
3371 })
3372 .unwrap()
3373 .await
3374 .unwrap();
3375 assert_item_labels(&pane, [], cx);
3376
3377 add_labeled_item(&pane, "A", true, cx);
3378 add_labeled_item(&pane, "B", true, cx);
3379 add_labeled_item(&pane, "C", true, cx);
3380 assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
3381
3382 let save = pane
3383 .update(cx, |pane, cx| {
3384 pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
3385 })
3386 .unwrap();
3387
3388 cx.executor().run_until_parked();
3389 cx.simulate_prompt_answer(2);
3390 save.await.unwrap();
3391 assert_item_labels(&pane, [], cx);
3392 }
3393
3394 fn init_test(cx: &mut TestAppContext) {
3395 cx.update(|cx| {
3396 let settings_store = SettingsStore::test(cx);
3397 cx.set_global(settings_store);
3398 theme::init(LoadThemes::JustBase, cx);
3399 crate::init_settings(cx);
3400 Project::init_settings(cx);
3401 });
3402 }
3403
3404 fn add_labeled_item(
3405 pane: &View<Pane>,
3406 label: &str,
3407 is_dirty: bool,
3408 cx: &mut VisualTestContext,
3409 ) -> Box<View<TestItem>> {
3410 pane.update(cx, |pane, cx| {
3411 let labeled_item = Box::new(
3412 cx.new_view(|cx| TestItem::new(cx).with_label(label).with_dirty(is_dirty)),
3413 );
3414 pane.add_item(labeled_item.clone(), false, false, None, cx);
3415 labeled_item
3416 })
3417 }
3418
3419 fn set_labeled_items<const COUNT: usize>(
3420 pane: &View<Pane>,
3421 labels: [&str; COUNT],
3422 cx: &mut VisualTestContext,
3423 ) -> [Box<View<TestItem>>; COUNT] {
3424 pane.update(cx, |pane, cx| {
3425 pane.items.clear();
3426 let mut active_item_index = 0;
3427
3428 let mut index = 0;
3429 let items = labels.map(|mut label| {
3430 if label.ends_with('*') {
3431 label = label.trim_end_matches('*');
3432 active_item_index = index;
3433 }
3434
3435 let labeled_item = Box::new(cx.new_view(|cx| TestItem::new(cx).with_label(label)));
3436 pane.add_item(labeled_item.clone(), false, false, None, cx);
3437 index += 1;
3438 labeled_item
3439 });
3440
3441 pane.activate_item(active_item_index, false, false, cx);
3442
3443 items
3444 })
3445 }
3446
3447 // Assert the item label, with the active item label suffixed with a '*'
3448 fn assert_item_labels<const COUNT: usize>(
3449 pane: &View<Pane>,
3450 expected_states: [&str; COUNT],
3451 cx: &mut VisualTestContext,
3452 ) {
3453 pane.update(cx, |pane, cx| {
3454 let actual_states = pane
3455 .items
3456 .iter()
3457 .enumerate()
3458 .map(|(ix, item)| {
3459 let mut state = item
3460 .to_any()
3461 .downcast::<TestItem>()
3462 .unwrap()
3463 .read(cx)
3464 .label
3465 .clone();
3466 if ix == pane.active_item_index {
3467 state.push('*');
3468 }
3469 if item.is_dirty(cx) {
3470 state.push('^');
3471 }
3472 state
3473 })
3474 .collect::<Vec<_>>();
3475
3476 assert_eq!(
3477 actual_states, expected_states,
3478 "pane items do not match expectation"
3479 );
3480 })
3481 }
3482}
3483
3484impl Render for DraggedTab {
3485 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3486 let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
3487 let label = self.item.tab_content(
3488 TabContentParams {
3489 detail: Some(self.detail),
3490 selected: false,
3491 preview: false,
3492 },
3493 cx,
3494 );
3495 Tab::new("")
3496 .selected(self.is_active)
3497 .child(label)
3498 .render(cx)
3499 .font(ui_font)
3500 }
3501}