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