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