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