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