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 if let Some(upgraded) = alternative.upgrade() {
488 self.add_item(upgraded, true, true, None, cx);
489 }
490 }
491 }
492
493 pub fn track_alternate_file_items(&mut self) {
494 if let Some(item) = self.active_item().map(|item| item.downgrade_item()) {
495 let (current, _) = &self.alternate_file_items;
496 match current {
497 Some(current) => {
498 if current.id() != item.id() {
499 self.alternate_file_items =
500 (Some(item), self.alternate_file_items.0.take());
501 }
502 }
503 None => {
504 self.alternate_file_items = (Some(item), None);
505 }
506 }
507 }
508 }
509
510 pub fn has_focus(&self, cx: &WindowContext) -> bool {
511 // We not only check whether our focus handle contains focus, but also
512 // whether the active item might have focus, because we might have just activated an item
513 // that hasn't rendered yet.
514 // Before the next render, we might transfer focus
515 // to the item, and `focus_handle.contains_focus` returns false because the `active_item`
516 // is not hooked up to us in the dispatch tree.
517 self.focus_handle.contains_focused(cx)
518 || self
519 .active_item()
520 .map_or(false, |item| item.focus_handle(cx).contains_focused(cx))
521 }
522
523 fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
524 if !self.was_focused {
525 self.was_focused = true;
526 cx.emit(Event::Focus);
527 cx.notify();
528 }
529
530 self.toolbar.update(cx, |toolbar, cx| {
531 toolbar.focus_changed(true, cx);
532 });
533
534 if let Some(active_item) = self.active_item() {
535 if self.focus_handle.is_focused(cx) {
536 // Pane was focused directly. We need to either focus a view inside the active item,
537 // or focus the active item itself
538 if let Some(weak_last_focus_handle) =
539 self.last_focus_handle_by_item.get(&active_item.item_id())
540 {
541 if let Some(focus_handle) = weak_last_focus_handle.upgrade() {
542 focus_handle.focus(cx);
543 return;
544 }
545 }
546
547 active_item.focus_handle(cx).focus(cx);
548 } else if let Some(focused) = cx.focused() {
549 if !self.context_menu_focused(cx) {
550 self.last_focus_handle_by_item
551 .insert(active_item.item_id(), focused.downgrade());
552 }
553 }
554 }
555 }
556
557 pub fn context_menu_focused(&self, cx: &mut ViewContext<Self>) -> bool {
558 self.new_item_context_menu_handle.is_focused(cx)
559 || self.split_item_context_menu_handle.is_focused(cx)
560 }
561
562 fn focus_out(&mut self, _event: FocusOutEvent, cx: &mut ViewContext<Self>) {
563 self.was_focused = false;
564 self.toolbar.update(cx, |toolbar, cx| {
565 toolbar.focus_changed(false, cx);
566 });
567 cx.notify();
568 }
569
570 fn settings_changed(&mut self, cx: &mut ViewContext<Self>) {
571 if let Some(display_nav_history_buttons) = self.display_nav_history_buttons.as_mut() {
572 *display_nav_history_buttons = TabBarSettings::get_global(cx).show_nav_history_buttons;
573 }
574 if !PreviewTabsSettings::get_global(cx).enabled {
575 self.preview_item_id = None;
576 }
577 cx.notify();
578 }
579
580 pub fn active_item_index(&self) -> usize {
581 self.active_item_index
582 }
583
584 pub fn activation_history(&self) -> &[ActivationHistoryEntry] {
585 &self.activation_history
586 }
587
588 pub fn set_should_display_tab_bar<F>(&mut self, should_display_tab_bar: F)
589 where
590 F: 'static + Fn(&ViewContext<Pane>) -> bool,
591 {
592 self.should_display_tab_bar = Rc::new(should_display_tab_bar);
593 }
594
595 pub fn set_can_split(&mut self, can_split: bool, cx: &mut ViewContext<Self>) {
596 self.can_split = can_split;
597 cx.notify();
598 }
599
600 pub fn set_can_navigate(&mut self, can_navigate: bool, cx: &mut ViewContext<Self>) {
601 self.toolbar.update(cx, |toolbar, cx| {
602 toolbar.set_can_navigate(can_navigate, cx);
603 });
604 cx.notify();
605 }
606
607 pub fn set_render_tab_bar_buttons<F>(&mut self, cx: &mut ViewContext<Self>, render: F)
608 where
609 F: 'static
610 + Fn(&mut Pane, &mut ViewContext<Pane>) -> (Option<AnyElement>, Option<AnyElement>),
611 {
612 self.render_tab_bar_buttons = Rc::new(render);
613 cx.notify();
614 }
615
616 pub fn set_custom_drop_handle<F>(&mut self, cx: &mut ViewContext<Self>, handle: F)
617 where
618 F: 'static + Fn(&mut Pane, &dyn Any, &mut ViewContext<Pane>) -> ControlFlow<(), ()>,
619 {
620 self.custom_drop_handle = Some(Arc::new(handle));
621 cx.notify();
622 }
623
624 pub fn nav_history_for_item<T: Item>(&self, item: &View<T>) -> ItemNavHistory {
625 ItemNavHistory {
626 history: self.nav_history.clone(),
627 item: Arc::new(item.downgrade()),
628 is_preview: self.preview_item_id == Some(item.item_id()),
629 }
630 }
631
632 pub fn nav_history(&self) -> &NavHistory {
633 &self.nav_history
634 }
635
636 pub fn nav_history_mut(&mut self) -> &mut NavHistory {
637 &mut self.nav_history
638 }
639
640 pub fn disable_history(&mut self) {
641 self.nav_history.disable();
642 }
643
644 pub fn enable_history(&mut self) {
645 self.nav_history.enable();
646 }
647
648 pub fn can_navigate_backward(&self) -> bool {
649 !self.nav_history.0.lock().backward_stack.is_empty()
650 }
651
652 pub fn can_navigate_forward(&self) -> bool {
653 !self.nav_history.0.lock().forward_stack.is_empty()
654 }
655
656 fn navigate_backward(&mut self, cx: &mut ViewContext<Self>) {
657 if let Some(workspace) = self.workspace.upgrade() {
658 let pane = cx.view().downgrade();
659 cx.window_context().defer(move |cx| {
660 workspace.update(cx, |workspace, cx| {
661 workspace.go_back(pane, cx).detach_and_log_err(cx)
662 })
663 })
664 }
665 }
666
667 fn navigate_forward(&mut self, cx: &mut ViewContext<Self>) {
668 if let Some(workspace) = self.workspace.upgrade() {
669 let pane = cx.view().downgrade();
670 cx.window_context().defer(move |cx| {
671 workspace.update(cx, |workspace, cx| {
672 workspace.go_forward(pane, cx).detach_and_log_err(cx)
673 })
674 })
675 }
676 }
677
678 fn join_into_next(&mut self, cx: &mut ViewContext<Self>) {
679 cx.emit(Event::JoinIntoNext);
680 }
681
682 fn history_updated(&mut self, cx: &mut ViewContext<Self>) {
683 self.toolbar.update(cx, |_, cx| cx.notify());
684 }
685
686 pub fn preview_item_id(&self) -> Option<EntityId> {
687 self.preview_item_id
688 }
689
690 pub fn preview_item(&self) -> Option<Box<dyn ItemHandle>> {
691 self.preview_item_id
692 .and_then(|id| self.items.iter().find(|item| item.item_id() == id))
693 .cloned()
694 }
695
696 fn preview_item_idx(&self) -> Option<usize> {
697 if let Some(preview_item_id) = self.preview_item_id {
698 self.items
699 .iter()
700 .position(|item| item.item_id() == preview_item_id)
701 } else {
702 None
703 }
704 }
705
706 pub fn is_active_preview_item(&self, item_id: EntityId) -> bool {
707 self.preview_item_id == Some(item_id)
708 }
709
710 /// Marks the item with the given ID as the preview item.
711 /// This will be ignored if the global setting `preview_tabs` is disabled.
712 pub fn set_preview_item_id(&mut self, item_id: Option<EntityId>, cx: &AppContext) {
713 if PreviewTabsSettings::get_global(cx).enabled {
714 self.preview_item_id = item_id;
715 }
716 }
717
718 pub fn handle_item_edit(&mut self, item_id: EntityId, cx: &AppContext) {
719 if let Some(preview_item) = self.preview_item() {
720 if preview_item.item_id() == item_id && !preview_item.preserve_preview(cx) {
721 self.set_preview_item_id(None, cx);
722 }
723 }
724 }
725
726 pub(crate) fn open_item(
727 &mut self,
728 project_entry_id: Option<ProjectEntryId>,
729 focus_item: bool,
730 allow_preview: bool,
731 cx: &mut ViewContext<Self>,
732 build_item: impl FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
733 ) -> Box<dyn ItemHandle> {
734 let mut existing_item = None;
735 if let Some(project_entry_id) = project_entry_id {
736 for (index, item) in self.items.iter().enumerate() {
737 if item.is_singleton(cx)
738 && item.project_entry_ids(cx).as_slice() == [project_entry_id]
739 {
740 let item = item.boxed_clone();
741 existing_item = Some((index, item));
742 break;
743 }
744 }
745 }
746
747 if let Some((index, existing_item)) = existing_item {
748 // If the item is already open, and the item is a preview item
749 // and we are not allowing items to open as preview, mark the item as persistent.
750 if let Some(preview_item_id) = self.preview_item_id {
751 if let Some(tab) = self.items.get(index) {
752 if tab.item_id() == preview_item_id && !allow_preview {
753 self.set_preview_item_id(None, cx);
754 }
755 }
756 }
757
758 self.activate_item(index, focus_item, focus_item, cx);
759 existing_item
760 } else {
761 // If the item is being opened as preview and we have an existing preview tab,
762 // open the new item in the position of the existing preview tab.
763 let destination_index = if allow_preview {
764 self.close_current_preview_item(cx)
765 } else {
766 None
767 };
768
769 let new_item = build_item(cx);
770
771 if allow_preview {
772 self.set_preview_item_id(Some(new_item.item_id()), cx);
773 }
774
775 self.add_item(new_item.clone(), true, focus_item, destination_index, cx);
776
777 new_item
778 }
779 }
780
781 pub fn close_current_preview_item(&mut self, cx: &mut ViewContext<Self>) -> Option<usize> {
782 let item_idx = self.preview_item_idx()?;
783
784 let prev_active_item_index = self.active_item_index;
785 self.remove_item(item_idx, false, false, cx);
786 self.active_item_index = prev_active_item_index;
787
788 if item_idx < self.items.len() {
789 Some(item_idx)
790 } else {
791 None
792 }
793 }
794
795 pub fn add_item(
796 &mut self,
797 item: Box<dyn ItemHandle>,
798 activate_pane: bool,
799 focus_item: bool,
800 destination_index: Option<usize>,
801 cx: &mut ViewContext<Self>,
802 ) {
803 if item.is_singleton(cx) {
804 if let Some(&entry_id) = item.project_entry_ids(cx).first() {
805 let project = self.project.read(cx);
806 if let Some(project_path) = project.path_for_entry(entry_id, cx) {
807 let abs_path = project.absolute_path(&project_path, cx);
808 self.nav_history
809 .0
810 .lock()
811 .paths_by_item
812 .insert(item.item_id(), (project_path, abs_path));
813 }
814 }
815 }
816 // If no destination index is specified, add or move the item after the active item.
817 let mut insertion_index = {
818 cmp::min(
819 if let Some(destination_index) = destination_index {
820 destination_index
821 } else {
822 self.active_item_index + 1
823 },
824 self.items.len(),
825 )
826 };
827
828 // Does the item already exist?
829 let project_entry_id = if item.is_singleton(cx) {
830 item.project_entry_ids(cx).first().copied()
831 } else {
832 None
833 };
834
835 let existing_item_index = self.items.iter().position(|existing_item| {
836 if existing_item.item_id() == item.item_id() {
837 true
838 } else if existing_item.is_singleton(cx) {
839 existing_item
840 .project_entry_ids(cx)
841 .first()
842 .map_or(false, |existing_entry_id| {
843 Some(existing_entry_id) == project_entry_id.as_ref()
844 })
845 } else {
846 false
847 }
848 });
849
850 if let Some(existing_item_index) = existing_item_index {
851 // If the item already exists, move it to the desired destination and activate it
852
853 if existing_item_index != insertion_index {
854 let existing_item_is_active = existing_item_index == self.active_item_index;
855
856 // If the caller didn't specify a destination and the added item is already
857 // the active one, don't move it
858 if existing_item_is_active && destination_index.is_none() {
859 insertion_index = existing_item_index;
860 } else {
861 self.items.remove(existing_item_index);
862 if existing_item_index < self.active_item_index {
863 self.active_item_index -= 1;
864 }
865 insertion_index = insertion_index.min(self.items.len());
866
867 self.items.insert(insertion_index, item.clone());
868
869 if existing_item_is_active {
870 self.active_item_index = insertion_index;
871 } else if insertion_index <= self.active_item_index {
872 self.active_item_index += 1;
873 }
874 }
875
876 cx.notify();
877 }
878
879 self.activate_item(insertion_index, activate_pane, focus_item, cx);
880 } else {
881 self.items.insert(insertion_index, item.clone());
882
883 if insertion_index <= self.active_item_index
884 && self.preview_item_idx() != Some(self.active_item_index)
885 {
886 self.active_item_index += 1;
887 }
888
889 self.activate_item(insertion_index, activate_pane, focus_item, cx);
890 cx.notify();
891 }
892
893 cx.emit(Event::AddItem { item });
894 }
895
896 pub fn items_len(&self) -> usize {
897 self.items.len()
898 }
899
900 pub fn items(&self) -> impl DoubleEndedIterator<Item = &Box<dyn ItemHandle>> {
901 self.items.iter()
902 }
903
904 pub fn items_of_type<T: Render>(&self) -> impl '_ + Iterator<Item = View<T>> {
905 self.items
906 .iter()
907 .filter_map(|item| item.to_any().downcast().ok())
908 }
909
910 pub fn active_item(&self) -> Option<Box<dyn ItemHandle>> {
911 self.items.get(self.active_item_index).cloned()
912 }
913
914 pub fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Point<Pixels>> {
915 self.items
916 .get(self.active_item_index)?
917 .pixel_position_of_cursor(cx)
918 }
919
920 pub fn item_for_entry(
921 &self,
922 entry_id: ProjectEntryId,
923 cx: &AppContext,
924 ) -> Option<Box<dyn ItemHandle>> {
925 self.items.iter().find_map(|item| {
926 if item.is_singleton(cx) && (item.project_entry_ids(cx).as_slice() == [entry_id]) {
927 Some(item.boxed_clone())
928 } else {
929 None
930 }
931 })
932 }
933
934 pub fn item_for_path(
935 &self,
936 project_path: ProjectPath,
937 cx: &AppContext,
938 ) -> Option<Box<dyn ItemHandle>> {
939 self.items.iter().find_map(move |item| {
940 if item.is_singleton(cx) && (item.project_path(cx).as_slice() == [project_path.clone()])
941 {
942 Some(item.boxed_clone())
943 } else {
944 None
945 }
946 })
947 }
948
949 pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option<usize> {
950 self.index_for_item_id(item.item_id())
951 }
952
953 fn index_for_item_id(&self, item_id: EntityId) -> Option<usize> {
954 self.items.iter().position(|i| i.item_id() == item_id)
955 }
956
957 pub fn item_for_index(&self, ix: usize) -> Option<&dyn ItemHandle> {
958 self.items.get(ix).map(|i| i.as_ref())
959 }
960
961 pub fn toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext<Self>) {
962 if self.zoomed {
963 cx.emit(Event::ZoomOut);
964 } else if !self.items.is_empty() {
965 if !self.focus_handle.contains_focused(cx) {
966 cx.focus_self();
967 }
968 cx.emit(Event::ZoomIn);
969 }
970 }
971
972 pub fn activate_item(
973 &mut self,
974 index: usize,
975 activate_pane: bool,
976 focus_item: bool,
977 cx: &mut ViewContext<Self>,
978 ) {
979 use NavigationMode::{GoingBack, GoingForward};
980
981 if index < self.items.len() {
982 let prev_active_item_ix = mem::replace(&mut self.active_item_index, index);
983 if prev_active_item_ix != self.active_item_index
984 || matches!(self.nav_history.mode(), GoingBack | GoingForward)
985 {
986 if let Some(prev_item) = self.items.get(prev_active_item_ix) {
987 prev_item.deactivated(cx);
988 }
989 }
990 cx.emit(Event::ActivateItem {
991 local: activate_pane,
992 });
993
994 if let Some(newly_active_item) = self.items.get(index) {
995 self.activation_history
996 .retain(|entry| entry.entity_id != newly_active_item.item_id());
997 self.activation_history.push(ActivationHistoryEntry {
998 entity_id: newly_active_item.item_id(),
999 timestamp: self
1000 .next_activation_timestamp
1001 .fetch_add(1, Ordering::SeqCst),
1002 });
1003 }
1004
1005 self.update_toolbar(cx);
1006 self.update_status_bar(cx);
1007
1008 if focus_item {
1009 self.focus_active_item(cx);
1010 }
1011
1012 if !self.is_tab_pinned(index) {
1013 self.tab_bar_scroll_handle
1014 .scroll_to_item(index - self.pinned_tab_count);
1015 }
1016
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 = !matches!(
1600 item.workspace_settings(cx).autosave,
1601 AutosaveSetting::AfterDelay { .. }
1602 );
1603 if Self::can_autosave_item(item, cx) {
1604 item.save(format, project, cx)
1605 } else {
1606 Task::ready(Ok(()))
1607 }
1608 }
1609
1610 pub fn focus(&mut self, cx: &mut ViewContext<Pane>) {
1611 cx.focus(&self.focus_handle);
1612 }
1613
1614 pub fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
1615 if let Some(active_item) = self.active_item() {
1616 let focus_handle = active_item.focus_handle(cx);
1617 cx.focus(&focus_handle);
1618 }
1619 }
1620
1621 pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext<Self>) {
1622 cx.emit(Event::Split(direction));
1623 }
1624
1625 pub fn toolbar(&self) -> &View<Toolbar> {
1626 &self.toolbar
1627 }
1628
1629 pub fn handle_deleted_project_item(
1630 &mut self,
1631 entry_id: ProjectEntryId,
1632 cx: &mut ViewContext<Pane>,
1633 ) -> Option<()> {
1634 let (item_index_to_delete, item_id) = self.items().enumerate().find_map(|(i, item)| {
1635 if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
1636 Some((i, item.item_id()))
1637 } else {
1638 None
1639 }
1640 })?;
1641
1642 self.remove_item(item_index_to_delete, false, true, cx);
1643 self.nav_history.remove_item(item_id);
1644
1645 Some(())
1646 }
1647
1648 fn update_toolbar(&mut self, cx: &mut ViewContext<Self>) {
1649 let active_item = self
1650 .items
1651 .get(self.active_item_index)
1652 .map(|item| item.as_ref());
1653 self.toolbar.update(cx, |toolbar, cx| {
1654 toolbar.set_active_item(active_item, cx);
1655 });
1656 }
1657
1658 fn update_status_bar(&mut self, cx: &mut ViewContext<Self>) {
1659 let workspace = self.workspace.clone();
1660 let pane = cx.view().clone();
1661
1662 cx.window_context().defer(move |cx| {
1663 let Ok(status_bar) = workspace.update(cx, |workspace, _| workspace.status_bar.clone())
1664 else {
1665 return;
1666 };
1667
1668 status_bar.update(cx, move |status_bar, cx| {
1669 status_bar.set_active_pane(&pane, cx);
1670 });
1671 });
1672 }
1673
1674 fn entry_abs_path(&self, entry: ProjectEntryId, cx: &WindowContext) -> Option<PathBuf> {
1675 let worktree = self
1676 .workspace
1677 .upgrade()?
1678 .read(cx)
1679 .project()
1680 .read(cx)
1681 .worktree_for_entry(entry, cx)?
1682 .read(cx);
1683 let entry = worktree.entry_for_id(entry)?;
1684 let abs_path = worktree.absolutize(&entry.path).ok()?;
1685 if entry.is_symlink {
1686 abs_path.canonicalize().ok()
1687 } else {
1688 Some(abs_path)
1689 }
1690 }
1691
1692 fn copy_relative_path(&mut self, _: &CopyRelativePath, cx: &mut ViewContext<Self>) {
1693 if let Some(clipboard_text) = self
1694 .active_item()
1695 .as_ref()
1696 .and_then(|entry| entry.project_path(cx))
1697 .map(|p| p.path.to_string_lossy().to_string())
1698 {
1699 cx.write_to_clipboard(ClipboardItem::new_string(clipboard_text));
1700 }
1701 }
1702
1703 pub fn icon_color(selected: bool) -> Color {
1704 if selected {
1705 Color::Default
1706 } else {
1707 Color::Muted
1708 }
1709 }
1710
1711 pub fn git_aware_icon_color(
1712 git_status: Option<GitFileStatus>,
1713 ignored: bool,
1714 selected: bool,
1715 ) -> Color {
1716 if ignored {
1717 Color::Ignored
1718 } else {
1719 match git_status {
1720 Some(GitFileStatus::Added) => Color::Created,
1721 Some(GitFileStatus::Modified) => Color::Modified,
1722 Some(GitFileStatus::Conflict) => Color::Conflict,
1723 None => Self::icon_color(selected),
1724 }
1725 }
1726 }
1727
1728 fn toggle_pin_tab(&mut self, _: &TogglePinTab, cx: &mut ViewContext<'_, Self>) {
1729 if self.items.is_empty() {
1730 return;
1731 }
1732 let active_tab_ix = self.active_item_index();
1733 if self.is_tab_pinned(active_tab_ix) {
1734 self.unpin_tab_at(active_tab_ix, cx);
1735 } else {
1736 self.pin_tab_at(active_tab_ix, cx);
1737 }
1738 }
1739
1740 fn pin_tab_at(&mut self, ix: usize, cx: &mut ViewContext<'_, Self>) {
1741 maybe!({
1742 let pane = cx.view().clone();
1743 let destination_index = self.pinned_tab_count;
1744 self.pinned_tab_count += 1;
1745 let id = self.item_for_index(ix)?.item_id();
1746
1747 self.workspace
1748 .update(cx, |_, cx| {
1749 cx.defer(move |this, cx| {
1750 this.move_item(pane.clone(), pane, id, destination_index, cx)
1751 });
1752 })
1753 .ok()?;
1754
1755 Some(())
1756 });
1757 }
1758
1759 fn unpin_tab_at(&mut self, ix: usize, cx: &mut ViewContext<'_, Self>) {
1760 maybe!({
1761 let pane = cx.view().clone();
1762 self.pinned_tab_count = self.pinned_tab_count.checked_sub(1).unwrap();
1763 let destination_index = self.pinned_tab_count;
1764
1765 let id = self.item_for_index(ix)?.item_id();
1766
1767 self.workspace
1768 .update(cx, |_, cx| {
1769 cx.defer(move |this, cx| {
1770 this.move_item(pane.clone(), pane, id, destination_index, cx)
1771 });
1772 })
1773 .ok()?;
1774
1775 Some(())
1776 });
1777 }
1778
1779 fn is_tab_pinned(&self, ix: usize) -> bool {
1780 self.pinned_tab_count > ix
1781 }
1782
1783 fn has_pinned_tabs(&self) -> bool {
1784 self.pinned_tab_count != 0
1785 }
1786
1787 fn render_tab(
1788 &self,
1789 ix: usize,
1790 item: &dyn ItemHandle,
1791 detail: usize,
1792 cx: &mut ViewContext<'_, Pane>,
1793 ) -> impl IntoElement {
1794 let project_path = item.project_path(cx);
1795
1796 let is_active = ix == self.active_item_index;
1797 let is_preview = self
1798 .preview_item_id
1799 .map(|id| id == item.item_id())
1800 .unwrap_or(false);
1801
1802 let label = item.tab_content(
1803 TabContentParams {
1804 detail: Some(detail),
1805 selected: is_active,
1806 preview: is_preview,
1807 },
1808 cx,
1809 );
1810
1811 let icon_color = if ItemSettings::get_global(cx).git_status {
1812 project_path
1813 .as_ref()
1814 .and_then(|path| self.project.read(cx).entry_for_path(path, cx))
1815 .map(|entry| {
1816 Self::git_aware_icon_color(entry.git_status, entry.is_ignored, is_active)
1817 })
1818 .unwrap_or_else(|| Self::icon_color(is_active))
1819 } else {
1820 Self::icon_color(is_active)
1821 };
1822
1823 let icon = item.tab_icon(cx);
1824 let close_side = &ItemSettings::get_global(cx).close_position;
1825 let indicator = render_item_indicator(item.boxed_clone(), cx);
1826 let item_id = item.item_id();
1827 let is_first_item = ix == 0;
1828 let is_last_item = ix == self.items.len() - 1;
1829 let is_pinned = self.is_tab_pinned(ix);
1830 let position_relative_to_active_item = ix.cmp(&self.active_item_index);
1831
1832 let tab = Tab::new(ix)
1833 .position(if is_first_item {
1834 TabPosition::First
1835 } else if is_last_item {
1836 TabPosition::Last
1837 } else {
1838 TabPosition::Middle(position_relative_to_active_item)
1839 })
1840 .close_side(match close_side {
1841 ClosePosition::Left => ui::TabCloseSide::Start,
1842 ClosePosition::Right => ui::TabCloseSide::End,
1843 })
1844 .selected(is_active)
1845 .on_click(
1846 cx.listener(move |pane: &mut Self, _, cx| pane.activate_item(ix, true, true, cx)),
1847 )
1848 // TODO: This should be a click listener with the middle mouse button instead of a mouse down listener.
1849 .on_mouse_down(
1850 MouseButton::Middle,
1851 cx.listener(move |pane, _event, cx| {
1852 pane.close_item_by_id(item_id, SaveIntent::Close, cx)
1853 .detach_and_log_err(cx);
1854 }),
1855 )
1856 .on_mouse_down(
1857 MouseButton::Left,
1858 cx.listener(move |pane, event: &MouseDownEvent, cx| {
1859 if let Some(id) = pane.preview_item_id {
1860 if id == item_id && event.click_count > 1 {
1861 pane.set_preview_item_id(None, cx);
1862 }
1863 }
1864 }),
1865 )
1866 .on_drag(
1867 DraggedTab {
1868 item: item.boxed_clone(),
1869 pane: cx.view().clone(),
1870 detail,
1871 is_active,
1872 ix,
1873 },
1874 |tab, cx| cx.new_view(|_| tab.clone()),
1875 )
1876 .drag_over::<DraggedTab>(|tab, _, cx| {
1877 tab.bg(cx.theme().colors().drop_target_background)
1878 })
1879 .drag_over::<DraggedSelection>(|tab, _, cx| {
1880 tab.bg(cx.theme().colors().drop_target_background)
1881 })
1882 .when_some(self.can_drop_predicate.clone(), |this, p| {
1883 this.can_drop(move |a, cx| p(a, cx))
1884 })
1885 .on_drop(cx.listener(move |this, dragged_tab: &DraggedTab, cx| {
1886 this.drag_split_direction = None;
1887 this.handle_tab_drop(dragged_tab, ix, cx)
1888 }))
1889 .on_drop(cx.listener(move |this, selection: &DraggedSelection, cx| {
1890 this.drag_split_direction = None;
1891 this.handle_dragged_selection_drop(selection, cx)
1892 }))
1893 .on_drop(cx.listener(move |this, paths, cx| {
1894 this.drag_split_direction = None;
1895 this.handle_external_paths_drop(paths, cx)
1896 }))
1897 .when_some(item.tab_tooltip_text(cx), |tab, text| {
1898 tab.tooltip(move |cx| Tooltip::text(text.clone(), cx))
1899 })
1900 .start_slot::<Indicator>(indicator)
1901 .map(|this| {
1902 let end_slot = if is_pinned {
1903 IconButton::new("unpin tab", IconName::Pin)
1904 .shape(IconButtonShape::Square)
1905 .icon_color(Color::Muted)
1906 .size(ButtonSize::None)
1907 .icon_size(IconSize::XSmall)
1908 .on_click(cx.listener(move |pane, _, cx| {
1909 pane.unpin_tab_at(ix, cx);
1910 }))
1911 .tooltip(|cx| Tooltip::text("Unpin Tab", cx))
1912 } else {
1913 IconButton::new("close tab", IconName::Close)
1914 .visible_on_hover("")
1915 .shape(IconButtonShape::Square)
1916 .icon_color(Color::Muted)
1917 .size(ButtonSize::None)
1918 .icon_size(IconSize::XSmall)
1919 .on_click(cx.listener(move |pane, _, cx| {
1920 pane.close_item_by_id(item_id, SaveIntent::Close, cx)
1921 .detach_and_log_err(cx);
1922 }))
1923 };
1924 this.end_slot(end_slot)
1925 })
1926 .child(
1927 h_flex()
1928 .gap_1()
1929 .children(icon.map(|icon| icon.size(IconSize::Small).color(icon_color)))
1930 .child(label),
1931 );
1932
1933 let single_entry_to_resolve = {
1934 let item_entries = self.items[ix].project_entry_ids(cx);
1935 if item_entries.len() == 1 {
1936 Some(item_entries[0])
1937 } else {
1938 None
1939 }
1940 };
1941
1942 let is_pinned = self.is_tab_pinned(ix);
1943 let pane = cx.view().downgrade();
1944 right_click_menu(ix).trigger(tab).menu(move |cx| {
1945 let pane = pane.clone();
1946 ContextMenu::build(cx, move |mut menu, cx| {
1947 if let Some(pane) = pane.upgrade() {
1948 menu = menu
1949 .entry(
1950 "Close",
1951 Some(Box::new(CloseActiveItem { save_intent: None })),
1952 cx.handler_for(&pane, move |pane, cx| {
1953 pane.close_item_by_id(item_id, SaveIntent::Close, cx)
1954 .detach_and_log_err(cx);
1955 }),
1956 )
1957 .entry(
1958 "Close Others",
1959 Some(Box::new(CloseInactiveItems { save_intent: None })),
1960 cx.handler_for(&pane, move |pane, cx| {
1961 pane.close_items(cx, SaveIntent::Close, |id| id != item_id)
1962 .detach_and_log_err(cx);
1963 }),
1964 )
1965 .separator()
1966 .entry(
1967 "Close Left",
1968 Some(Box::new(CloseItemsToTheLeft)),
1969 cx.handler_for(&pane, move |pane, cx| {
1970 pane.close_items_to_the_left_by_id(item_id, cx)
1971 .detach_and_log_err(cx);
1972 }),
1973 )
1974 .entry(
1975 "Close Right",
1976 Some(Box::new(CloseItemsToTheRight)),
1977 cx.handler_for(&pane, move |pane, cx| {
1978 pane.close_items_to_the_right_by_id(item_id, cx)
1979 .detach_and_log_err(cx);
1980 }),
1981 )
1982 .separator()
1983 .entry(
1984 "Close Clean",
1985 Some(Box::new(CloseCleanItems)),
1986 cx.handler_for(&pane, move |pane, cx| {
1987 if let Some(task) = pane.close_clean_items(&CloseCleanItems, cx) {
1988 task.detach_and_log_err(cx)
1989 }
1990 }),
1991 )
1992 .entry(
1993 "Close All",
1994 Some(Box::new(CloseAllItems { save_intent: None })),
1995 cx.handler_for(&pane, |pane, cx| {
1996 if let Some(task) =
1997 pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
1998 {
1999 task.detach_and_log_err(cx)
2000 }
2001 }),
2002 );
2003
2004 let pin_tab_entries = |menu: ContextMenu| {
2005 menu.separator().map(|this| {
2006 if is_pinned {
2007 this.entry(
2008 "Unpin Tab",
2009 Some(TogglePinTab.boxed_clone()),
2010 cx.handler_for(&pane, move |pane, cx| {
2011 pane.unpin_tab_at(ix, cx);
2012 }),
2013 )
2014 } else {
2015 this.entry(
2016 "Pin Tab",
2017 Some(TogglePinTab.boxed_clone()),
2018 cx.handler_for(&pane, move |pane, cx| {
2019 pane.pin_tab_at(ix, cx);
2020 }),
2021 )
2022 }
2023 })
2024 };
2025 if let Some(entry) = single_entry_to_resolve {
2026 let entry_abs_path = pane.read(cx).entry_abs_path(entry, cx);
2027 let parent_abs_path = entry_abs_path
2028 .as_deref()
2029 .and_then(|abs_path| Some(abs_path.parent()?.to_path_buf()));
2030
2031 let entry_id = entry.to_proto();
2032 menu = menu
2033 .separator()
2034 .when_some(entry_abs_path, |menu, abs_path| {
2035 menu.entry(
2036 "Copy Path",
2037 Some(Box::new(CopyPath)),
2038 cx.handler_for(&pane, move |_, cx| {
2039 cx.write_to_clipboard(ClipboardItem::new_string(
2040 abs_path.to_string_lossy().to_string(),
2041 ));
2042 }),
2043 )
2044 })
2045 .entry(
2046 "Copy Relative Path",
2047 Some(Box::new(CopyRelativePath)),
2048 cx.handler_for(&pane, move |pane, cx| {
2049 pane.copy_relative_path(&CopyRelativePath, cx);
2050 }),
2051 )
2052 .map(pin_tab_entries)
2053 .separator()
2054 .entry(
2055 "Reveal In Project Panel",
2056 Some(Box::new(RevealInProjectPanel {
2057 entry_id: Some(entry_id),
2058 })),
2059 cx.handler_for(&pane, move |pane, cx| {
2060 pane.project.update(cx, |_, cx| {
2061 cx.emit(project::Event::RevealInProjectPanel(
2062 ProjectEntryId::from_proto(entry_id),
2063 ))
2064 });
2065 }),
2066 )
2067 .when_some(parent_abs_path, |menu, parent_abs_path| {
2068 menu.entry(
2069 "Open in Terminal",
2070 Some(Box::new(OpenInTerminal)),
2071 cx.handler_for(&pane, move |_, cx| {
2072 cx.dispatch_action(
2073 OpenTerminal {
2074 working_directory: parent_abs_path.clone(),
2075 }
2076 .boxed_clone(),
2077 );
2078 }),
2079 )
2080 });
2081 } else {
2082 menu = menu.map(pin_tab_entries);
2083 }
2084 }
2085
2086 menu
2087 })
2088 })
2089 }
2090
2091 fn render_tab_bar(&mut self, cx: &mut ViewContext<'_, Pane>) -> impl IntoElement {
2092 let focus_handle = self.focus_handle.clone();
2093 let navigate_backward = IconButton::new("navigate_backward", IconName::ArrowLeft)
2094 .shape(IconButtonShape::Square)
2095 .icon_size(IconSize::Small)
2096 .on_click({
2097 let view = cx.view().clone();
2098 move |_, cx| view.update(cx, Self::navigate_backward)
2099 })
2100 .disabled(!self.can_navigate_backward())
2101 .tooltip({
2102 let focus_handle = focus_handle.clone();
2103 move |cx| Tooltip::for_action_in("Go Back", &GoBack, &focus_handle, cx)
2104 });
2105
2106 let navigate_forward = IconButton::new("navigate_forward", IconName::ArrowRight)
2107 .shape(IconButtonShape::Square)
2108 .icon_size(IconSize::Small)
2109 .on_click({
2110 let view = cx.view().clone();
2111 move |_, cx| view.update(cx, Self::navigate_forward)
2112 })
2113 .disabled(!self.can_navigate_forward())
2114 .tooltip({
2115 let focus_handle = focus_handle.clone();
2116 move |cx| Tooltip::for_action_in("Go Forward", &GoForward, &focus_handle, cx)
2117 });
2118
2119 let mut tab_items = self
2120 .items
2121 .iter()
2122 .enumerate()
2123 .zip(tab_details(&self.items, cx))
2124 .map(|((ix, item), detail)| self.render_tab(ix, &**item, detail, cx))
2125 .collect::<Vec<_>>();
2126
2127 let unpinned_tabs = tab_items.split_off(self.pinned_tab_count);
2128 let pinned_tabs = tab_items;
2129 TabBar::new("tab_bar")
2130 .when(
2131 self.display_nav_history_buttons.unwrap_or_default(),
2132 |tab_bar| {
2133 tab_bar
2134 .start_child(navigate_backward)
2135 .start_child(navigate_forward)
2136 },
2137 )
2138 .map(|tab_bar| {
2139 let render_tab_buttons = self.render_tab_bar_buttons.clone();
2140 let (left_children, right_children) = render_tab_buttons(self, cx);
2141
2142 tab_bar
2143 .start_children(left_children)
2144 .end_children(right_children)
2145 })
2146 .children(pinned_tabs.len().ne(&0).then(|| {
2147 h_flex()
2148 .children(pinned_tabs)
2149 .border_r_2()
2150 .border_color(cx.theme().colors().border)
2151 }))
2152 .child(
2153 h_flex()
2154 .id("unpinned tabs")
2155 .overflow_x_scroll()
2156 .w_full()
2157 .track_scroll(&self.tab_bar_scroll_handle)
2158 .children(unpinned_tabs)
2159 .child(
2160 div()
2161 .id("tab_bar_drop_target")
2162 .min_w_6()
2163 // HACK: This empty child is currently necessary to force the drop target to appear
2164 // despite us setting a min width above.
2165 .child("")
2166 .h_full()
2167 .flex_grow()
2168 .drag_over::<DraggedTab>(|bar, _, cx| {
2169 bar.bg(cx.theme().colors().drop_target_background)
2170 })
2171 .drag_over::<DraggedSelection>(|bar, _, cx| {
2172 bar.bg(cx.theme().colors().drop_target_background)
2173 })
2174 .on_drop(cx.listener(move |this, dragged_tab: &DraggedTab, cx| {
2175 this.drag_split_direction = None;
2176 this.handle_tab_drop(dragged_tab, this.items.len(), cx)
2177 }))
2178 .on_drop(cx.listener(move |this, selection: &DraggedSelection, cx| {
2179 this.drag_split_direction = None;
2180 this.handle_project_entry_drop(
2181 &selection.active_selection.entry_id,
2182 cx,
2183 )
2184 }))
2185 .on_drop(cx.listener(move |this, paths, cx| {
2186 this.drag_split_direction = None;
2187 this.handle_external_paths_drop(paths, cx)
2188 }))
2189 .on_click(cx.listener(move |this, event: &ClickEvent, cx| {
2190 if event.up.click_count == 2 {
2191 cx.dispatch_action(
2192 this.double_click_dispatch_action.boxed_clone(),
2193 )
2194 }
2195 })),
2196 ),
2197 )
2198 }
2199
2200 pub fn render_menu_overlay(menu: &View<ContextMenu>) -> Div {
2201 div().absolute().bottom_0().right_0().size_0().child(
2202 deferred(
2203 anchored()
2204 .anchor(AnchorCorner::TopRight)
2205 .child(menu.clone()),
2206 )
2207 .with_priority(1),
2208 )
2209 }
2210
2211 pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
2212 self.zoomed = zoomed;
2213 cx.notify();
2214 }
2215
2216 pub fn is_zoomed(&self) -> bool {
2217 self.zoomed
2218 }
2219
2220 fn handle_drag_move<T>(&mut self, event: &DragMoveEvent<T>, cx: &mut ViewContext<Self>) {
2221 if !self.can_split {
2222 return;
2223 }
2224
2225 let rect = event.bounds.size;
2226
2227 let size = event.bounds.size.width.min(event.bounds.size.height)
2228 * WorkspaceSettings::get_global(cx).drop_target_size;
2229
2230 let relative_cursor = Point::new(
2231 event.event.position.x - event.bounds.left(),
2232 event.event.position.y - event.bounds.top(),
2233 );
2234
2235 let direction = if relative_cursor.x < size
2236 || relative_cursor.x > rect.width - size
2237 || relative_cursor.y < size
2238 || relative_cursor.y > rect.height - size
2239 {
2240 [
2241 SplitDirection::Up,
2242 SplitDirection::Right,
2243 SplitDirection::Down,
2244 SplitDirection::Left,
2245 ]
2246 .iter()
2247 .min_by_key(|side| match side {
2248 SplitDirection::Up => relative_cursor.y,
2249 SplitDirection::Right => rect.width - relative_cursor.x,
2250 SplitDirection::Down => rect.height - relative_cursor.y,
2251 SplitDirection::Left => relative_cursor.x,
2252 })
2253 .cloned()
2254 } else {
2255 None
2256 };
2257
2258 if direction != self.drag_split_direction {
2259 self.drag_split_direction = direction;
2260 }
2261 }
2262
2263 fn handle_tab_drop(
2264 &mut self,
2265 dragged_tab: &DraggedTab,
2266 ix: usize,
2267 cx: &mut ViewContext<'_, Self>,
2268 ) {
2269 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2270 if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_tab, cx) {
2271 return;
2272 }
2273 }
2274 let mut to_pane = cx.view().clone();
2275 let split_direction = self.drag_split_direction;
2276 let item_id = dragged_tab.item.item_id();
2277 if let Some(preview_item_id) = self.preview_item_id {
2278 if item_id == preview_item_id {
2279 self.set_preview_item_id(None, cx);
2280 }
2281 }
2282
2283 let from_pane = dragged_tab.pane.clone();
2284 self.workspace
2285 .update(cx, |_, cx| {
2286 cx.defer(move |workspace, cx| {
2287 if let Some(split_direction) = split_direction {
2288 to_pane = workspace.split_pane(to_pane, split_direction, cx);
2289 }
2290 let old_ix = from_pane.read(cx).index_for_item_id(item_id);
2291 if to_pane == from_pane {
2292 if let Some(old_index) = old_ix {
2293 to_pane.update(cx, |this, _| {
2294 if old_index < this.pinned_tab_count
2295 && (ix == this.items.len() || ix > this.pinned_tab_count)
2296 {
2297 this.pinned_tab_count -= 1;
2298 } else if this.has_pinned_tabs()
2299 && old_index >= this.pinned_tab_count
2300 && ix < this.pinned_tab_count
2301 {
2302 this.pinned_tab_count += 1;
2303 }
2304 });
2305 }
2306 } else {
2307 to_pane.update(cx, |this, _| {
2308 if this.has_pinned_tabs() && ix < this.pinned_tab_count {
2309 this.pinned_tab_count += 1;
2310 }
2311 });
2312 from_pane.update(cx, |this, _| {
2313 if let Some(index) = old_ix {
2314 if this.pinned_tab_count > index {
2315 this.pinned_tab_count -= 1;
2316 }
2317 }
2318 })
2319 }
2320 workspace.move_item(from_pane.clone(), to_pane.clone(), item_id, ix, cx);
2321 });
2322 })
2323 .log_err();
2324 }
2325
2326 fn handle_dragged_selection_drop(
2327 &mut self,
2328 dragged_selection: &DraggedSelection,
2329 cx: &mut ViewContext<'_, Self>,
2330 ) {
2331 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2332 if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_selection, cx) {
2333 return;
2334 }
2335 }
2336 self.handle_project_entry_drop(&dragged_selection.active_selection.entry_id, cx);
2337 }
2338
2339 fn handle_project_entry_drop(
2340 &mut self,
2341 project_entry_id: &ProjectEntryId,
2342 cx: &mut ViewContext<'_, Self>,
2343 ) {
2344 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2345 if let ControlFlow::Break(()) = custom_drop_handle(self, project_entry_id, cx) {
2346 return;
2347 }
2348 }
2349 let mut to_pane = cx.view().clone();
2350 let split_direction = self.drag_split_direction;
2351 let project_entry_id = *project_entry_id;
2352 self.workspace
2353 .update(cx, |_, cx| {
2354 cx.defer(move |workspace, cx| {
2355 if let Some(path) = workspace
2356 .project()
2357 .read(cx)
2358 .path_for_entry(project_entry_id, cx)
2359 {
2360 let load_path_task = workspace.load_path(path, cx);
2361 cx.spawn(|workspace, mut cx| async move {
2362 if let Some((project_entry_id, build_item)) =
2363 load_path_task.await.notify_async_err(&mut cx)
2364 {
2365 let (to_pane, new_item_handle) = workspace
2366 .update(&mut cx, |workspace, cx| {
2367 if let Some(split_direction) = split_direction {
2368 to_pane =
2369 workspace.split_pane(to_pane, split_direction, cx);
2370 }
2371 let new_item_handle = to_pane.update(cx, |pane, cx| {
2372 pane.open_item(
2373 project_entry_id,
2374 true,
2375 false,
2376 cx,
2377 build_item,
2378 )
2379 });
2380 (to_pane, new_item_handle)
2381 })
2382 .log_err()?;
2383 to_pane
2384 .update(&mut cx, |this, cx| {
2385 let Some(index) = this.index_for_item(&*new_item_handle)
2386 else {
2387 return;
2388 };
2389 if !this.is_tab_pinned(index) {
2390 this.pin_tab_at(index, cx);
2391 }
2392 })
2393 .ok()?
2394 }
2395 Some(())
2396 })
2397 .detach();
2398 };
2399 });
2400 })
2401 .log_err();
2402 }
2403
2404 fn handle_external_paths_drop(
2405 &mut self,
2406 paths: &ExternalPaths,
2407 cx: &mut ViewContext<'_, Self>,
2408 ) {
2409 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2410 if let ControlFlow::Break(()) = custom_drop_handle(self, paths, cx) {
2411 return;
2412 }
2413 }
2414 let mut to_pane = cx.view().clone();
2415 let mut split_direction = self.drag_split_direction;
2416 let paths = paths.paths().to_vec();
2417 let is_remote = self
2418 .workspace
2419 .update(cx, |workspace, cx| {
2420 if workspace.project().read(cx).is_via_collab() {
2421 workspace.show_error(
2422 &anyhow::anyhow!("Cannot drop files on a remote project"),
2423 cx,
2424 );
2425 true
2426 } else {
2427 false
2428 }
2429 })
2430 .unwrap_or(true);
2431 if is_remote {
2432 return;
2433 }
2434
2435 self.workspace
2436 .update(cx, |workspace, cx| {
2437 let fs = Arc::clone(workspace.project().read(cx).fs());
2438 cx.spawn(|workspace, mut cx| async move {
2439 let mut is_file_checks = FuturesUnordered::new();
2440 for path in &paths {
2441 is_file_checks.push(fs.is_file(path))
2442 }
2443 let mut has_files_to_open = false;
2444 while let Some(is_file) = is_file_checks.next().await {
2445 if is_file {
2446 has_files_to_open = true;
2447 break;
2448 }
2449 }
2450 drop(is_file_checks);
2451 if !has_files_to_open {
2452 split_direction = None;
2453 }
2454
2455 if let Ok(open_task) = workspace.update(&mut cx, |workspace, cx| {
2456 if let Some(split_direction) = split_direction {
2457 to_pane = workspace.split_pane(to_pane, split_direction, cx);
2458 }
2459 workspace.open_paths(
2460 paths,
2461 OpenVisible::OnlyDirectories,
2462 Some(to_pane.downgrade()),
2463 cx,
2464 )
2465 }) {
2466 let opened_items: Vec<_> = open_task.await;
2467 _ = workspace.update(&mut cx, |workspace, cx| {
2468 for item in opened_items.into_iter().flatten() {
2469 if let Err(e) = item {
2470 workspace.show_error(&e, cx);
2471 }
2472 }
2473 });
2474 }
2475 })
2476 .detach();
2477 })
2478 .log_err();
2479 }
2480
2481 pub fn display_nav_history_buttons(&mut self, display: Option<bool>) {
2482 self.display_nav_history_buttons = display;
2483 }
2484}
2485
2486impl FocusableView for Pane {
2487 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
2488 self.focus_handle.clone()
2489 }
2490}
2491
2492impl Render for Pane {
2493 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2494 let mut key_context = KeyContext::new_with_defaults();
2495 key_context.add("Pane");
2496 if self.active_item().is_none() {
2497 key_context.add("EmptyPane");
2498 }
2499
2500 let should_display_tab_bar = self.should_display_tab_bar.clone();
2501 let display_tab_bar = should_display_tab_bar(cx);
2502
2503 v_flex()
2504 .key_context(key_context)
2505 .track_focus(&self.focus_handle)
2506 .size_full()
2507 .flex_none()
2508 .overflow_hidden()
2509 .on_action(cx.listener(|pane, _: &AlternateFile, cx| {
2510 pane.alternate_file(cx);
2511 }))
2512 .on_action(cx.listener(|pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx)))
2513 .on_action(cx.listener(|pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx)))
2514 .on_action(cx.listener(|pane, _: &SplitHorizontal, cx| {
2515 pane.split(SplitDirection::horizontal(cx), cx)
2516 }))
2517 .on_action(cx.listener(|pane, _: &SplitVertical, cx| {
2518 pane.split(SplitDirection::vertical(cx), cx)
2519 }))
2520 .on_action(
2521 cx.listener(|pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx)),
2522 )
2523 .on_action(cx.listener(|pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx)))
2524 .on_action(cx.listener(|pane, _: &GoBack, cx| pane.navigate_backward(cx)))
2525 .on_action(cx.listener(|pane, _: &GoForward, cx| pane.navigate_forward(cx)))
2526 .on_action(cx.listener(|pane, _: &JoinIntoNext, cx| pane.join_into_next(cx)))
2527 .on_action(cx.listener(Pane::toggle_zoom))
2528 .on_action(cx.listener(|pane: &mut Pane, action: &ActivateItem, cx| {
2529 pane.activate_item(action.0, true, true, cx);
2530 }))
2531 .on_action(cx.listener(|pane: &mut Pane, _: &ActivateLastItem, cx| {
2532 pane.activate_item(pane.items.len() - 1, true, true, cx);
2533 }))
2534 .on_action(cx.listener(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
2535 pane.activate_prev_item(true, cx);
2536 }))
2537 .on_action(cx.listener(|pane: &mut Pane, _: &ActivateNextItem, cx| {
2538 pane.activate_next_item(true, cx);
2539 }))
2540 .on_action(cx.listener(|pane, action, cx| {
2541 pane.toggle_pin_tab(action, cx);
2542 }))
2543 .when(PreviewTabsSettings::get_global(cx).enabled, |this| {
2544 this.on_action(cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, cx| {
2545 if let Some(active_item_id) = pane.active_item().map(|i| i.item_id()) {
2546 if pane.is_active_preview_item(active_item_id) {
2547 pane.set_preview_item_id(None, cx);
2548 } else {
2549 pane.set_preview_item_id(Some(active_item_id), cx);
2550 }
2551 }
2552 }))
2553 })
2554 .on_action(
2555 cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
2556 if let Some(task) = pane.close_active_item(action, cx) {
2557 task.detach_and_log_err(cx)
2558 }
2559 }),
2560 )
2561 .on_action(
2562 cx.listener(|pane: &mut Self, action: &CloseInactiveItems, cx| {
2563 if let Some(task) = pane.close_inactive_items(action, cx) {
2564 task.detach_and_log_err(cx)
2565 }
2566 }),
2567 )
2568 .on_action(
2569 cx.listener(|pane: &mut Self, action: &CloseCleanItems, cx| {
2570 if let Some(task) = pane.close_clean_items(action, cx) {
2571 task.detach_and_log_err(cx)
2572 }
2573 }),
2574 )
2575 .on_action(
2576 cx.listener(|pane: &mut Self, action: &CloseItemsToTheLeft, cx| {
2577 if let Some(task) = pane.close_items_to_the_left(action, cx) {
2578 task.detach_and_log_err(cx)
2579 }
2580 }),
2581 )
2582 .on_action(
2583 cx.listener(|pane: &mut Self, action: &CloseItemsToTheRight, cx| {
2584 if let Some(task) = pane.close_items_to_the_right(action, cx) {
2585 task.detach_and_log_err(cx)
2586 }
2587 }),
2588 )
2589 .on_action(cx.listener(|pane: &mut Self, action: &CloseAllItems, cx| {
2590 if let Some(task) = pane.close_all_items(action, cx) {
2591 task.detach_and_log_err(cx)
2592 }
2593 }))
2594 .on_action(
2595 cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
2596 if let Some(task) = pane.close_active_item(action, cx) {
2597 task.detach_and_log_err(cx)
2598 }
2599 }),
2600 )
2601 .on_action(
2602 cx.listener(|pane: &mut Self, action: &RevealInProjectPanel, cx| {
2603 let entry_id = action
2604 .entry_id
2605 .map(ProjectEntryId::from_proto)
2606 .or_else(|| pane.active_item()?.project_entry_ids(cx).first().copied());
2607 if let Some(entry_id) = entry_id {
2608 pane.project.update(cx, |_, cx| {
2609 cx.emit(project::Event::RevealInProjectPanel(entry_id))
2610 });
2611 }
2612 }),
2613 )
2614 .when(self.active_item().is_some() && display_tab_bar, |pane| {
2615 pane.child(self.render_tab_bar(cx))
2616 })
2617 .child({
2618 let has_worktrees = self.project.read(cx).worktrees(cx).next().is_some();
2619 // main content
2620 div()
2621 .flex_1()
2622 .relative()
2623 .group("")
2624 .on_drag_move::<DraggedTab>(cx.listener(Self::handle_drag_move))
2625 .on_drag_move::<DraggedSelection>(cx.listener(Self::handle_drag_move))
2626 .on_drag_move::<ExternalPaths>(cx.listener(Self::handle_drag_move))
2627 .map(|div| {
2628 if let Some(item) = self.active_item() {
2629 div.v_flex()
2630 .child(self.toolbar.clone())
2631 .child(item.to_any())
2632 } else {
2633 let placeholder = div.h_flex().size_full().justify_center();
2634 if has_worktrees {
2635 placeholder
2636 } else {
2637 placeholder.child(
2638 Label::new("Open a file or project to get started.")
2639 .color(Color::Muted),
2640 )
2641 }
2642 }
2643 })
2644 .child(
2645 // drag target
2646 div()
2647 .invisible()
2648 .absolute()
2649 .bg(cx.theme().colors().drop_target_background)
2650 .group_drag_over::<DraggedTab>("", |style| style.visible())
2651 .group_drag_over::<DraggedSelection>("", |style| style.visible())
2652 .group_drag_over::<ExternalPaths>("", |style| style.visible())
2653 .when_some(self.can_drop_predicate.clone(), |this, p| {
2654 this.can_drop(move |a, cx| p(a, cx))
2655 })
2656 .on_drop(cx.listener(move |this, dragged_tab, cx| {
2657 this.handle_tab_drop(dragged_tab, this.active_item_index(), cx)
2658 }))
2659 .on_drop(cx.listener(move |this, selection: &DraggedSelection, cx| {
2660 this.handle_dragged_selection_drop(selection, cx)
2661 }))
2662 .on_drop(cx.listener(move |this, paths, cx| {
2663 this.handle_external_paths_drop(paths, cx)
2664 }))
2665 .map(|div| {
2666 let size = DefiniteLength::Fraction(0.5);
2667 match self.drag_split_direction {
2668 None => div.top_0().right_0().bottom_0().left_0(),
2669 Some(SplitDirection::Up) => {
2670 div.top_0().left_0().right_0().h(size)
2671 }
2672 Some(SplitDirection::Down) => {
2673 div.left_0().bottom_0().right_0().h(size)
2674 }
2675 Some(SplitDirection::Left) => {
2676 div.top_0().left_0().bottom_0().w(size)
2677 }
2678 Some(SplitDirection::Right) => {
2679 div.top_0().bottom_0().right_0().w(size)
2680 }
2681 }
2682 }),
2683 )
2684 })
2685 .on_mouse_down(
2686 MouseButton::Navigate(NavigationDirection::Back),
2687 cx.listener(|pane, _, cx| {
2688 if let Some(workspace) = pane.workspace.upgrade() {
2689 let pane = cx.view().downgrade();
2690 cx.window_context().defer(move |cx| {
2691 workspace.update(cx, |workspace, cx| {
2692 workspace.go_back(pane, cx).detach_and_log_err(cx)
2693 })
2694 })
2695 }
2696 }),
2697 )
2698 .on_mouse_down(
2699 MouseButton::Navigate(NavigationDirection::Forward),
2700 cx.listener(|pane, _, cx| {
2701 if let Some(workspace) = pane.workspace.upgrade() {
2702 let pane = cx.view().downgrade();
2703 cx.window_context().defer(move |cx| {
2704 workspace.update(cx, |workspace, cx| {
2705 workspace.go_forward(pane, cx).detach_and_log_err(cx)
2706 })
2707 })
2708 }
2709 }),
2710 )
2711 }
2712}
2713
2714impl ItemNavHistory {
2715 pub fn push<D: 'static + Send + Any>(&mut self, data: Option<D>, cx: &mut WindowContext) {
2716 self.history
2717 .push(data, self.item.clone(), self.is_preview, cx);
2718 }
2719
2720 pub fn pop_backward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
2721 self.history.pop(NavigationMode::GoingBack, cx)
2722 }
2723
2724 pub fn pop_forward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
2725 self.history.pop(NavigationMode::GoingForward, cx)
2726 }
2727}
2728
2729impl NavHistory {
2730 pub fn for_each_entry(
2731 &self,
2732 cx: &AppContext,
2733 mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
2734 ) {
2735 let borrowed_history = self.0.lock();
2736 borrowed_history
2737 .forward_stack
2738 .iter()
2739 .chain(borrowed_history.backward_stack.iter())
2740 .chain(borrowed_history.closed_stack.iter())
2741 .for_each(|entry| {
2742 if let Some(project_and_abs_path) =
2743 borrowed_history.paths_by_item.get(&entry.item.id())
2744 {
2745 f(entry, project_and_abs_path.clone());
2746 } else if let Some(item) = entry.item.upgrade() {
2747 if let Some(path) = item.project_path(cx) {
2748 f(entry, (path, None));
2749 }
2750 }
2751 })
2752 }
2753
2754 pub fn set_mode(&mut self, mode: NavigationMode) {
2755 self.0.lock().mode = mode;
2756 }
2757
2758 pub fn mode(&self) -> NavigationMode {
2759 self.0.lock().mode
2760 }
2761
2762 pub fn disable(&mut self) {
2763 self.0.lock().mode = NavigationMode::Disabled;
2764 }
2765
2766 pub fn enable(&mut self) {
2767 self.0.lock().mode = NavigationMode::Normal;
2768 }
2769
2770 pub fn pop(&mut self, mode: NavigationMode, cx: &mut WindowContext) -> Option<NavigationEntry> {
2771 let mut state = self.0.lock();
2772 let entry = match mode {
2773 NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
2774 return None
2775 }
2776 NavigationMode::GoingBack => &mut state.backward_stack,
2777 NavigationMode::GoingForward => &mut state.forward_stack,
2778 NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
2779 }
2780 .pop_back();
2781 if entry.is_some() {
2782 state.did_update(cx);
2783 }
2784 entry
2785 }
2786
2787 pub fn push<D: 'static + Send + Any>(
2788 &mut self,
2789 data: Option<D>,
2790 item: Arc<dyn WeakItemHandle>,
2791 is_preview: bool,
2792 cx: &mut WindowContext,
2793 ) {
2794 let state = &mut *self.0.lock();
2795 match state.mode {
2796 NavigationMode::Disabled => {}
2797 NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
2798 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2799 state.backward_stack.pop_front();
2800 }
2801 state.backward_stack.push_back(NavigationEntry {
2802 item,
2803 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2804 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2805 is_preview,
2806 });
2807 state.forward_stack.clear();
2808 }
2809 NavigationMode::GoingBack => {
2810 if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2811 state.forward_stack.pop_front();
2812 }
2813 state.forward_stack.push_back(NavigationEntry {
2814 item,
2815 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2816 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2817 is_preview,
2818 });
2819 }
2820 NavigationMode::GoingForward => {
2821 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2822 state.backward_stack.pop_front();
2823 }
2824 state.backward_stack.push_back(NavigationEntry {
2825 item,
2826 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2827 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2828 is_preview,
2829 });
2830 }
2831 NavigationMode::ClosingItem => {
2832 if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2833 state.closed_stack.pop_front();
2834 }
2835 state.closed_stack.push_back(NavigationEntry {
2836 item,
2837 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2838 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2839 is_preview,
2840 });
2841 }
2842 }
2843 state.did_update(cx);
2844 }
2845
2846 pub fn remove_item(&mut self, item_id: EntityId) {
2847 let mut state = self.0.lock();
2848 state.paths_by_item.remove(&item_id);
2849 state
2850 .backward_stack
2851 .retain(|entry| entry.item.id() != item_id);
2852 state
2853 .forward_stack
2854 .retain(|entry| entry.item.id() != item_id);
2855 state
2856 .closed_stack
2857 .retain(|entry| entry.item.id() != item_id);
2858 }
2859
2860 pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option<PathBuf>)> {
2861 self.0.lock().paths_by_item.get(&item_id).cloned()
2862 }
2863}
2864
2865impl NavHistoryState {
2866 pub fn did_update(&self, cx: &mut WindowContext) {
2867 if let Some(pane) = self.pane.upgrade() {
2868 cx.defer(move |cx| {
2869 pane.update(cx, |pane, cx| pane.history_updated(cx));
2870 });
2871 }
2872 }
2873}
2874
2875fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
2876 let path = buffer_path
2877 .as_ref()
2878 .and_then(|p| {
2879 p.path
2880 .to_str()
2881 .and_then(|s| if s.is_empty() { None } else { Some(s) })
2882 })
2883 .unwrap_or("This buffer");
2884 let path = truncate_and_remove_front(path, 80);
2885 format!("{path} contains unsaved edits. Do you want to save it?")
2886}
2887
2888pub fn tab_details(items: &[Box<dyn ItemHandle>], cx: &AppContext) -> Vec<usize> {
2889 let mut tab_details = items.iter().map(|_| 0).collect::<Vec<_>>();
2890 let mut tab_descriptions = HashMap::default();
2891 let mut done = false;
2892 while !done {
2893 done = true;
2894
2895 // Store item indices by their tab description.
2896 for (ix, (item, detail)) in items.iter().zip(&tab_details).enumerate() {
2897 if let Some(description) = item.tab_description(*detail, cx) {
2898 if *detail == 0
2899 || Some(&description) != item.tab_description(detail - 1, cx).as_ref()
2900 {
2901 tab_descriptions
2902 .entry(description)
2903 .or_insert(Vec::new())
2904 .push(ix);
2905 }
2906 }
2907 }
2908
2909 // If two or more items have the same tab description, increase their level
2910 // of detail and try again.
2911 for (_, item_ixs) in tab_descriptions.drain() {
2912 if item_ixs.len() > 1 {
2913 done = false;
2914 for ix in item_ixs {
2915 tab_details[ix] += 1;
2916 }
2917 }
2918 }
2919 }
2920
2921 tab_details
2922}
2923
2924pub fn render_item_indicator(item: Box<dyn ItemHandle>, cx: &WindowContext) -> Option<Indicator> {
2925 maybe!({
2926 let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
2927 (true, _) => Color::Warning,
2928 (_, true) => Color::Accent,
2929 (false, false) => return None,
2930 };
2931
2932 Some(Indicator::dot().color(indicator_color))
2933 })
2934}
2935
2936impl Render for DraggedTab {
2937 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2938 let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
2939 let label = self.item.tab_content(
2940 TabContentParams {
2941 detail: Some(self.detail),
2942 selected: false,
2943 preview: false,
2944 },
2945 cx,
2946 );
2947 Tab::new("")
2948 .selected(self.is_active)
2949 .child(label)
2950 .render(cx)
2951 .font(ui_font)
2952 }
2953}
2954
2955#[cfg(test)]
2956mod tests {
2957 use super::*;
2958 use crate::item::test::{TestItem, TestProjectItem};
2959 use gpui::{TestAppContext, VisualTestContext};
2960 use project::FakeFs;
2961 use settings::SettingsStore;
2962 use theme::LoadThemes;
2963
2964 #[gpui::test]
2965 async fn test_remove_active_empty(cx: &mut TestAppContext) {
2966 init_test(cx);
2967 let fs = FakeFs::new(cx.executor());
2968
2969 let project = Project::test(fs, None, cx).await;
2970 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2971 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2972
2973 pane.update(cx, |pane, cx| {
2974 assert!(pane
2975 .close_active_item(&CloseActiveItem { save_intent: None }, cx)
2976 .is_none())
2977 });
2978 }
2979
2980 #[gpui::test]
2981 async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
2982 init_test(cx);
2983 let fs = FakeFs::new(cx.executor());
2984
2985 let project = Project::test(fs, None, cx).await;
2986 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2987 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2988
2989 // 1. Add with a destination index
2990 // a. Add before the active item
2991 set_labeled_items(&pane, ["A", "B*", "C"], cx);
2992 pane.update(cx, |pane, cx| {
2993 pane.add_item(
2994 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2995 false,
2996 false,
2997 Some(0),
2998 cx,
2999 );
3000 });
3001 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
3002
3003 // b. Add after the active item
3004 set_labeled_items(&pane, ["A", "B*", "C"], cx);
3005 pane.update(cx, |pane, cx| {
3006 pane.add_item(
3007 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3008 false,
3009 false,
3010 Some(2),
3011 cx,
3012 );
3013 });
3014 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3015
3016 // c. Add at the end of the item list (including off the length)
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 Some(5),
3024 cx,
3025 );
3026 });
3027 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3028
3029 // 2. Add without a destination index
3030 // a. Add with active item at the start of the item list
3031 set_labeled_items(&pane, ["A*", "B", "C"], cx);
3032 pane.update(cx, |pane, cx| {
3033 pane.add_item(
3034 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3035 false,
3036 false,
3037 None,
3038 cx,
3039 );
3040 });
3041 set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
3042
3043 // b. Add with active item at the end of the item list
3044 set_labeled_items(&pane, ["A", "B", "C*"], cx);
3045 pane.update(cx, |pane, cx| {
3046 pane.add_item(
3047 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3048 false,
3049 false,
3050 None,
3051 cx,
3052 );
3053 });
3054 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3055 }
3056
3057 #[gpui::test]
3058 async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
3059 init_test(cx);
3060 let fs = FakeFs::new(cx.executor());
3061
3062 let project = Project::test(fs, None, cx).await;
3063 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3064 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3065
3066 // 1. Add with a destination index
3067 // 1a. Add before the active item
3068 let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3069 pane.update(cx, |pane, cx| {
3070 pane.add_item(d, false, false, Some(0), cx);
3071 });
3072 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
3073
3074 // 1b. Add after the active item
3075 let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3076 pane.update(cx, |pane, cx| {
3077 pane.add_item(d, false, false, Some(2), cx);
3078 });
3079 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3080
3081 // 1c. Add at the end of the item list (including off the length)
3082 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3083 pane.update(cx, |pane, cx| {
3084 pane.add_item(a, false, false, Some(5), cx);
3085 });
3086 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3087
3088 // 1d. Add same item to active index
3089 let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3090 pane.update(cx, |pane, cx| {
3091 pane.add_item(b, false, false, Some(1), cx);
3092 });
3093 assert_item_labels(&pane, ["A", "B*", "C"], cx);
3094
3095 // 1e. Add item to index after same item in last position
3096 let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3097 pane.update(cx, |pane, cx| {
3098 pane.add_item(c, false, false, Some(2), cx);
3099 });
3100 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3101
3102 // 2. Add without a destination index
3103 // 2a. Add with active item at the start of the item list
3104 let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
3105 pane.update(cx, |pane, cx| {
3106 pane.add_item(d, false, false, None, cx);
3107 });
3108 assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
3109
3110 // 2b. Add with active item at the end of the item list
3111 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
3112 pane.update(cx, |pane, cx| {
3113 pane.add_item(a, false, false, None, cx);
3114 });
3115 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3116
3117 // 2c. Add active item to active item at end of list
3118 let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
3119 pane.update(cx, |pane, cx| {
3120 pane.add_item(c, false, false, None, cx);
3121 });
3122 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3123
3124 // 2d. Add active item to active item at start of list
3125 let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
3126 pane.update(cx, |pane, cx| {
3127 pane.add_item(a, false, false, None, cx);
3128 });
3129 assert_item_labels(&pane, ["A*", "B", "C"], cx);
3130 }
3131
3132 #[gpui::test]
3133 async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
3134 init_test(cx);
3135 let fs = FakeFs::new(cx.executor());
3136
3137 let project = Project::test(fs, None, cx).await;
3138 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3139 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3140
3141 // singleton view
3142 pane.update(cx, |pane, cx| {
3143 pane.add_item(
3144 Box::new(cx.new_view(|cx| {
3145 TestItem::new(cx)
3146 .with_singleton(true)
3147 .with_label("buffer 1")
3148 .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
3149 })),
3150 false,
3151 false,
3152 None,
3153 cx,
3154 );
3155 });
3156 assert_item_labels(&pane, ["buffer 1*"], cx);
3157
3158 // new singleton view with the same project entry
3159 pane.update(cx, |pane, cx| {
3160 pane.add_item(
3161 Box::new(cx.new_view(|cx| {
3162 TestItem::new(cx)
3163 .with_singleton(true)
3164 .with_label("buffer 1")
3165 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3166 })),
3167 false,
3168 false,
3169 None,
3170 cx,
3171 );
3172 });
3173 assert_item_labels(&pane, ["buffer 1*"], cx);
3174
3175 // new singleton view with different project entry
3176 pane.update(cx, |pane, cx| {
3177 pane.add_item(
3178 Box::new(cx.new_view(|cx| {
3179 TestItem::new(cx)
3180 .with_singleton(true)
3181 .with_label("buffer 2")
3182 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
3183 })),
3184 false,
3185 false,
3186 None,
3187 cx,
3188 );
3189 });
3190 assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
3191
3192 // new multibuffer view with the same project entry
3193 pane.update(cx, |pane, cx| {
3194 pane.add_item(
3195 Box::new(cx.new_view(|cx| {
3196 TestItem::new(cx)
3197 .with_singleton(false)
3198 .with_label("multibuffer 1")
3199 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3200 })),
3201 false,
3202 false,
3203 None,
3204 cx,
3205 );
3206 });
3207 assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
3208
3209 // another multibuffer view with the same project entry
3210 pane.update(cx, |pane, cx| {
3211 pane.add_item(
3212 Box::new(cx.new_view(|cx| {
3213 TestItem::new(cx)
3214 .with_singleton(false)
3215 .with_label("multibuffer 1b")
3216 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3217 })),
3218 false,
3219 false,
3220 None,
3221 cx,
3222 );
3223 });
3224 assert_item_labels(
3225 &pane,
3226 ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
3227 cx,
3228 );
3229 }
3230
3231 #[gpui::test]
3232 async fn test_remove_item_ordering(cx: &mut TestAppContext) {
3233 init_test(cx);
3234 let fs = FakeFs::new(cx.executor());
3235
3236 let project = Project::test(fs, None, cx).await;
3237 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3238 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3239
3240 add_labeled_item(&pane, "A", false, cx);
3241 add_labeled_item(&pane, "B", false, cx);
3242 add_labeled_item(&pane, "C", false, cx);
3243 add_labeled_item(&pane, "D", false, cx);
3244 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3245
3246 pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
3247 add_labeled_item(&pane, "1", false, cx);
3248 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
3249
3250 pane.update(cx, |pane, cx| {
3251 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3252 })
3253 .unwrap()
3254 .await
3255 .unwrap();
3256 assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
3257
3258 pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
3259 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3260
3261 pane.update(cx, |pane, cx| {
3262 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3263 })
3264 .unwrap()
3265 .await
3266 .unwrap();
3267 assert_item_labels(&pane, ["A", "B*", "C"], cx);
3268
3269 pane.update(cx, |pane, cx| {
3270 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3271 })
3272 .unwrap()
3273 .await
3274 .unwrap();
3275 assert_item_labels(&pane, ["A", "C*"], cx);
3276
3277 pane.update(cx, |pane, cx| {
3278 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3279 })
3280 .unwrap()
3281 .await
3282 .unwrap();
3283 assert_item_labels(&pane, ["A*"], cx);
3284 }
3285
3286 #[gpui::test]
3287 async fn test_close_inactive_items(cx: &mut TestAppContext) {
3288 init_test(cx);
3289 let fs = FakeFs::new(cx.executor());
3290
3291 let project = Project::test(fs, None, cx).await;
3292 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3293 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3294
3295 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
3296
3297 pane.update(cx, |pane, cx| {
3298 pane.close_inactive_items(&CloseInactiveItems { save_intent: None }, cx)
3299 })
3300 .unwrap()
3301 .await
3302 .unwrap();
3303 assert_item_labels(&pane, ["C*"], cx);
3304 }
3305
3306 #[gpui::test]
3307 async fn test_close_clean_items(cx: &mut TestAppContext) {
3308 init_test(cx);
3309 let fs = FakeFs::new(cx.executor());
3310
3311 let project = Project::test(fs, None, cx).await;
3312 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3313 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3314
3315 add_labeled_item(&pane, "A", true, cx);
3316 add_labeled_item(&pane, "B", false, cx);
3317 add_labeled_item(&pane, "C", true, cx);
3318 add_labeled_item(&pane, "D", false, cx);
3319 add_labeled_item(&pane, "E", false, cx);
3320 assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
3321
3322 pane.update(cx, |pane, cx| pane.close_clean_items(&CloseCleanItems, cx))
3323 .unwrap()
3324 .await
3325 .unwrap();
3326 assert_item_labels(&pane, ["A^", "C*^"], cx);
3327 }
3328
3329 #[gpui::test]
3330 async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
3331 init_test(cx);
3332 let fs = FakeFs::new(cx.executor());
3333
3334 let project = Project::test(fs, None, cx).await;
3335 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3336 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3337
3338 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
3339
3340 pane.update(cx, |pane, cx| {
3341 pane.close_items_to_the_left(&CloseItemsToTheLeft, cx)
3342 })
3343 .unwrap()
3344 .await
3345 .unwrap();
3346 assert_item_labels(&pane, ["C*", "D", "E"], cx);
3347 }
3348
3349 #[gpui::test]
3350 async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
3351 init_test(cx);
3352 let fs = FakeFs::new(cx.executor());
3353
3354 let project = Project::test(fs, None, cx).await;
3355 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3356 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3357
3358 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
3359
3360 pane.update(cx, |pane, cx| {
3361 pane.close_items_to_the_right(&CloseItemsToTheRight, cx)
3362 })
3363 .unwrap()
3364 .await
3365 .unwrap();
3366 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3367 }
3368
3369 #[gpui::test]
3370 async fn test_close_all_items(cx: &mut TestAppContext) {
3371 init_test(cx);
3372 let fs = FakeFs::new(cx.executor());
3373
3374 let project = Project::test(fs, None, cx).await;
3375 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3376 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3377
3378 add_labeled_item(&pane, "A", false, cx);
3379 add_labeled_item(&pane, "B", false, cx);
3380 add_labeled_item(&pane, "C", false, cx);
3381 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3382
3383 pane.update(cx, |pane, cx| {
3384 pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
3385 })
3386 .unwrap()
3387 .await
3388 .unwrap();
3389 assert_item_labels(&pane, [], cx);
3390
3391 add_labeled_item(&pane, "A", true, cx);
3392 add_labeled_item(&pane, "B", true, cx);
3393 add_labeled_item(&pane, "C", true, cx);
3394 assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
3395
3396 let save = pane
3397 .update(cx, |pane, cx| {
3398 pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
3399 })
3400 .unwrap();
3401
3402 cx.executor().run_until_parked();
3403 cx.simulate_prompt_answer(2);
3404 save.await.unwrap();
3405 assert_item_labels(&pane, [], cx);
3406 }
3407
3408 fn init_test(cx: &mut TestAppContext) {
3409 cx.update(|cx| {
3410 let settings_store = SettingsStore::test(cx);
3411 cx.set_global(settings_store);
3412 theme::init(LoadThemes::JustBase, cx);
3413 crate::init_settings(cx);
3414 Project::init_settings(cx);
3415 });
3416 }
3417
3418 fn add_labeled_item(
3419 pane: &View<Pane>,
3420 label: &str,
3421 is_dirty: bool,
3422 cx: &mut VisualTestContext,
3423 ) -> Box<View<TestItem>> {
3424 pane.update(cx, |pane, cx| {
3425 let labeled_item = Box::new(
3426 cx.new_view(|cx| TestItem::new(cx).with_label(label).with_dirty(is_dirty)),
3427 );
3428 pane.add_item(labeled_item.clone(), false, false, None, cx);
3429 labeled_item
3430 })
3431 }
3432
3433 fn set_labeled_items<const COUNT: usize>(
3434 pane: &View<Pane>,
3435 labels: [&str; COUNT],
3436 cx: &mut VisualTestContext,
3437 ) -> [Box<View<TestItem>>; COUNT] {
3438 pane.update(cx, |pane, cx| {
3439 pane.items.clear();
3440 let mut active_item_index = 0;
3441
3442 let mut index = 0;
3443 let items = labels.map(|mut label| {
3444 if label.ends_with('*') {
3445 label = label.trim_end_matches('*');
3446 active_item_index = index;
3447 }
3448
3449 let labeled_item = Box::new(cx.new_view(|cx| TestItem::new(cx).with_label(label)));
3450 pane.add_item(labeled_item.clone(), false, false, None, cx);
3451 index += 1;
3452 labeled_item
3453 });
3454
3455 pane.activate_item(active_item_index, false, false, cx);
3456
3457 items
3458 })
3459 }
3460
3461 // Assert the item label, with the active item label suffixed with a '*'
3462 fn assert_item_labels<const COUNT: usize>(
3463 pane: &View<Pane>,
3464 expected_states: [&str; COUNT],
3465 cx: &mut VisualTestContext,
3466 ) {
3467 pane.update(cx, |pane, cx| {
3468 let actual_states = pane
3469 .items
3470 .iter()
3471 .enumerate()
3472 .map(|(ix, item)| {
3473 let mut state = item
3474 .to_any()
3475 .downcast::<TestItem>()
3476 .unwrap()
3477 .read(cx)
3478 .label
3479 .clone();
3480 if ix == pane.active_item_index {
3481 state.push('*');
3482 }
3483 if item.is_dirty(cx) {
3484 state.push('^');
3485 }
3486 state
3487 })
3488 .collect::<Vec<_>>();
3489
3490 assert_eq!(
3491 actual_states, expected_states,
3492 "pane items do not match expectation"
3493 );
3494 })
3495 }
3496}