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