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