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