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