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