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