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 let old_len = to_pane.read(cx).items.len();
2459 move_item(&from_pane, &to_pane, item_id, ix, cx);
2460 if to_pane == from_pane {
2461 if let Some(old_index) = old_ix {
2462 to_pane.update(cx, |this, _| {
2463 if old_index < this.pinned_tab_count
2464 && (ix == this.items.len() || ix > this.pinned_tab_count)
2465 {
2466 this.pinned_tab_count -= 1;
2467 } else if this.has_pinned_tabs()
2468 && old_index >= this.pinned_tab_count
2469 && ix < this.pinned_tab_count
2470 {
2471 this.pinned_tab_count += 1;
2472 }
2473 });
2474 }
2475 } else {
2476 to_pane.update(cx, |this, _| {
2477 if this.items.len() > old_len // Did we not deduplicate on drag?
2478 && this.has_pinned_tabs()
2479 && ix < this.pinned_tab_count
2480 {
2481 this.pinned_tab_count += 1;
2482 }
2483 });
2484 from_pane.update(cx, |this, _| {
2485 if let Some(index) = old_ix {
2486 if this.pinned_tab_count > index {
2487 this.pinned_tab_count -= 1;
2488 }
2489 }
2490 })
2491 }
2492 });
2493 })
2494 .log_err();
2495 }
2496
2497 fn handle_dragged_selection_drop(
2498 &mut self,
2499 dragged_selection: &DraggedSelection,
2500 dragged_onto: Option<usize>,
2501 cx: &mut ViewContext<'_, Self>,
2502 ) {
2503 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2504 if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_selection, cx) {
2505 return;
2506 }
2507 }
2508 self.handle_project_entry_drop(
2509 &dragged_selection.active_selection.entry_id,
2510 dragged_onto,
2511 cx,
2512 );
2513 }
2514
2515 fn handle_project_entry_drop(
2516 &mut self,
2517 project_entry_id: &ProjectEntryId,
2518 target: Option<usize>,
2519 cx: &mut ViewContext<'_, Self>,
2520 ) {
2521 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2522 if let ControlFlow::Break(()) = custom_drop_handle(self, project_entry_id, cx) {
2523 return;
2524 }
2525 }
2526 let mut to_pane = cx.view().clone();
2527 let split_direction = self.drag_split_direction;
2528 let project_entry_id = *project_entry_id;
2529 self.workspace
2530 .update(cx, |_, cx| {
2531 cx.defer(move |workspace, cx| {
2532 if let Some(path) = workspace
2533 .project()
2534 .read(cx)
2535 .path_for_entry(project_entry_id, cx)
2536 {
2537 let load_path_task = workspace.load_path(path, cx);
2538 cx.spawn(|workspace, mut cx| async move {
2539 if let Some((project_entry_id, build_item)) =
2540 load_path_task.await.notify_async_err(&mut cx)
2541 {
2542 let (to_pane, new_item_handle) = workspace
2543 .update(&mut cx, |workspace, cx| {
2544 if let Some(split_direction) = split_direction {
2545 to_pane =
2546 workspace.split_pane(to_pane, split_direction, cx);
2547 }
2548 let new_item_handle = to_pane.update(cx, |pane, cx| {
2549 pane.open_item(
2550 project_entry_id,
2551 true,
2552 false,
2553 target,
2554 cx,
2555 build_item,
2556 )
2557 });
2558 (to_pane, new_item_handle)
2559 })
2560 .log_err()?;
2561 to_pane
2562 .update(&mut cx, |this, cx| {
2563 let Some(index) = this.index_for_item(&*new_item_handle)
2564 else {
2565 return;
2566 };
2567
2568 if target.map_or(false, |target| this.is_tab_pinned(target))
2569 {
2570 this.pin_tab_at(index, cx);
2571 }
2572 })
2573 .ok()?
2574 }
2575 Some(())
2576 })
2577 .detach();
2578 };
2579 });
2580 })
2581 .log_err();
2582 }
2583
2584 fn handle_external_paths_drop(
2585 &mut self,
2586 paths: &ExternalPaths,
2587 cx: &mut ViewContext<'_, Self>,
2588 ) {
2589 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2590 if let ControlFlow::Break(()) = custom_drop_handle(self, paths, cx) {
2591 return;
2592 }
2593 }
2594 let mut to_pane = cx.view().clone();
2595 let mut split_direction = self.drag_split_direction;
2596 let paths = paths.paths().to_vec();
2597 let is_remote = self
2598 .workspace
2599 .update(cx, |workspace, cx| {
2600 if workspace.project().read(cx).is_via_collab() {
2601 workspace.show_error(
2602 &anyhow::anyhow!("Cannot drop files on a remote project"),
2603 cx,
2604 );
2605 true
2606 } else {
2607 false
2608 }
2609 })
2610 .unwrap_or(true);
2611 if is_remote {
2612 return;
2613 }
2614
2615 self.workspace
2616 .update(cx, |workspace, cx| {
2617 let fs = Arc::clone(workspace.project().read(cx).fs());
2618 cx.spawn(|workspace, mut cx| async move {
2619 let mut is_file_checks = FuturesUnordered::new();
2620 for path in &paths {
2621 is_file_checks.push(fs.is_file(path))
2622 }
2623 let mut has_files_to_open = false;
2624 while let Some(is_file) = is_file_checks.next().await {
2625 if is_file {
2626 has_files_to_open = true;
2627 break;
2628 }
2629 }
2630 drop(is_file_checks);
2631 if !has_files_to_open {
2632 split_direction = None;
2633 }
2634
2635 if let Ok(open_task) = workspace.update(&mut cx, |workspace, cx| {
2636 if let Some(split_direction) = split_direction {
2637 to_pane = workspace.split_pane(to_pane, split_direction, cx);
2638 }
2639 workspace.open_paths(
2640 paths,
2641 OpenVisible::OnlyDirectories,
2642 Some(to_pane.downgrade()),
2643 cx,
2644 )
2645 }) {
2646 let opened_items: Vec<_> = open_task.await;
2647 _ = workspace.update(&mut cx, |workspace, cx| {
2648 for item in opened_items.into_iter().flatten() {
2649 if let Err(e) = item {
2650 workspace.show_error(&e, cx);
2651 }
2652 }
2653 });
2654 }
2655 })
2656 .detach();
2657 })
2658 .log_err();
2659 }
2660
2661 pub fn display_nav_history_buttons(&mut self, display: Option<bool>) {
2662 self.display_nav_history_buttons = display;
2663 }
2664
2665 fn get_non_closeable_item_ids(&self, close_pinned: bool) -> Vec<EntityId> {
2666 if close_pinned {
2667 return vec![];
2668 }
2669
2670 self.items
2671 .iter()
2672 .map(|item| item.item_id())
2673 .filter(|item_id| {
2674 if let Some(ix) = self.index_for_item_id(*item_id) {
2675 self.is_tab_pinned(ix)
2676 } else {
2677 true
2678 }
2679 })
2680 .collect()
2681 }
2682}
2683
2684impl FocusableView for Pane {
2685 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
2686 self.focus_handle.clone()
2687 }
2688}
2689
2690impl Render for Pane {
2691 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2692 let mut key_context = KeyContext::new_with_defaults();
2693 key_context.add("Pane");
2694 if self.active_item().is_none() {
2695 key_context.add("EmptyPane");
2696 }
2697
2698 let should_display_tab_bar = self.should_display_tab_bar.clone();
2699 let display_tab_bar = should_display_tab_bar(cx);
2700 let is_local = self.project.read(cx).is_local();
2701
2702 v_flex()
2703 .key_context(key_context)
2704 .track_focus(&self.focus_handle(cx))
2705 .size_full()
2706 .flex_none()
2707 .overflow_hidden()
2708 .on_action(cx.listener(|pane, _: &AlternateFile, cx| {
2709 pane.alternate_file(cx);
2710 }))
2711 .on_action(cx.listener(|pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx)))
2712 .on_action(cx.listener(|pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx)))
2713 .on_action(cx.listener(|pane, _: &SplitHorizontal, cx| {
2714 pane.split(SplitDirection::horizontal(cx), cx)
2715 }))
2716 .on_action(cx.listener(|pane, _: &SplitVertical, cx| {
2717 pane.split(SplitDirection::vertical(cx), cx)
2718 }))
2719 .on_action(
2720 cx.listener(|pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx)),
2721 )
2722 .on_action(cx.listener(|pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx)))
2723 .on_action(cx.listener(|pane, _: &GoBack, cx| pane.navigate_backward(cx)))
2724 .on_action(cx.listener(|pane, _: &GoForward, cx| pane.navigate_forward(cx)))
2725 .on_action(cx.listener(|pane, _: &JoinIntoNext, cx| pane.join_into_next(cx)))
2726 .on_action(cx.listener(|pane, _: &JoinAll, cx| pane.join_all(cx)))
2727 .on_action(cx.listener(Pane::toggle_zoom))
2728 .on_action(cx.listener(|pane: &mut Pane, action: &ActivateItem, cx| {
2729 pane.activate_item(action.0, true, true, cx);
2730 }))
2731 .on_action(cx.listener(|pane: &mut Pane, _: &ActivateLastItem, cx| {
2732 pane.activate_item(pane.items.len() - 1, true, true, cx);
2733 }))
2734 .on_action(cx.listener(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
2735 pane.activate_prev_item(true, cx);
2736 }))
2737 .on_action(cx.listener(|pane: &mut Pane, _: &ActivateNextItem, cx| {
2738 pane.activate_next_item(true, cx);
2739 }))
2740 .on_action(cx.listener(|pane, _: &SwapItemLeft, cx| pane.swap_item_left(cx)))
2741 .on_action(cx.listener(|pane, _: &SwapItemRight, cx| pane.swap_item_right(cx)))
2742 .on_action(cx.listener(|pane, action, cx| {
2743 pane.toggle_pin_tab(action, cx);
2744 }))
2745 .when(PreviewTabsSettings::get_global(cx).enabled, |this| {
2746 this.on_action(cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, cx| {
2747 if let Some(active_item_id) = pane.active_item().map(|i| i.item_id()) {
2748 if pane.is_active_preview_item(active_item_id) {
2749 pane.set_preview_item_id(None, cx);
2750 } else {
2751 pane.set_preview_item_id(Some(active_item_id), cx);
2752 }
2753 }
2754 }))
2755 })
2756 .on_action(
2757 cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
2758 if let Some(task) = pane.close_active_item(action, cx) {
2759 task.detach_and_log_err(cx)
2760 }
2761 }),
2762 )
2763 .on_action(
2764 cx.listener(|pane: &mut Self, action: &CloseInactiveItems, cx| {
2765 if let Some(task) = pane.close_inactive_items(action, cx) {
2766 task.detach_and_log_err(cx)
2767 }
2768 }),
2769 )
2770 .on_action(
2771 cx.listener(|pane: &mut Self, action: &CloseCleanItems, cx| {
2772 if let Some(task) = pane.close_clean_items(action, cx) {
2773 task.detach_and_log_err(cx)
2774 }
2775 }),
2776 )
2777 .on_action(
2778 cx.listener(|pane: &mut Self, action: &CloseItemsToTheLeft, cx| {
2779 if let Some(task) = pane.close_items_to_the_left(action, cx) {
2780 task.detach_and_log_err(cx)
2781 }
2782 }),
2783 )
2784 .on_action(
2785 cx.listener(|pane: &mut Self, action: &CloseItemsToTheRight, cx| {
2786 if let Some(task) = pane.close_items_to_the_right(action, cx) {
2787 task.detach_and_log_err(cx)
2788 }
2789 }),
2790 )
2791 .on_action(cx.listener(|pane: &mut Self, action: &CloseAllItems, cx| {
2792 if let Some(task) = pane.close_all_items(action, cx) {
2793 task.detach_and_log_err(cx)
2794 }
2795 }))
2796 .on_action(
2797 cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
2798 if let Some(task) = pane.close_active_item(action, cx) {
2799 task.detach_and_log_err(cx)
2800 }
2801 }),
2802 )
2803 .on_action(
2804 cx.listener(|pane: &mut Self, action: &RevealInProjectPanel, cx| {
2805 let entry_id = action
2806 .entry_id
2807 .map(ProjectEntryId::from_proto)
2808 .or_else(|| pane.active_item()?.project_entry_ids(cx).first().copied());
2809 if let Some(entry_id) = entry_id {
2810 pane.project.update(cx, |_, cx| {
2811 cx.emit(project::Event::RevealInProjectPanel(entry_id))
2812 });
2813 }
2814 }),
2815 )
2816 .when(self.active_item().is_some() && display_tab_bar, |pane| {
2817 pane.child(self.render_tab_bar(cx))
2818 })
2819 .child({
2820 let has_worktrees = self.project.read(cx).worktrees(cx).next().is_some();
2821 // main content
2822 div()
2823 .flex_1()
2824 .relative()
2825 .group("")
2826 .overflow_hidden()
2827 .on_drag_move::<DraggedTab>(cx.listener(Self::handle_drag_move))
2828 .on_drag_move::<DraggedSelection>(cx.listener(Self::handle_drag_move))
2829 .when(is_local, |div| {
2830 div.on_drag_move::<ExternalPaths>(cx.listener(Self::handle_drag_move))
2831 })
2832 .map(|div| {
2833 if let Some(item) = self.active_item() {
2834 div.v_flex()
2835 .size_full()
2836 .overflow_hidden()
2837 .child(self.toolbar.clone())
2838 .child(item.to_any())
2839 } else {
2840 let placeholder = div.h_flex().size_full().justify_center();
2841 if has_worktrees {
2842 placeholder
2843 } else {
2844 placeholder.child(
2845 Label::new("Open a file or project to get started.")
2846 .color(Color::Muted),
2847 )
2848 }
2849 }
2850 })
2851 .child(
2852 // drag target
2853 div()
2854 .invisible()
2855 .absolute()
2856 .bg(cx.theme().colors().drop_target_background)
2857 .group_drag_over::<DraggedTab>("", |style| style.visible())
2858 .group_drag_over::<DraggedSelection>("", |style| style.visible())
2859 .when(is_local, |div| {
2860 div.group_drag_over::<ExternalPaths>("", |style| style.visible())
2861 })
2862 .when_some(self.can_drop_predicate.clone(), |this, p| {
2863 this.can_drop(move |a, cx| p(a, cx))
2864 })
2865 .on_drop(cx.listener(move |this, dragged_tab, cx| {
2866 this.handle_tab_drop(dragged_tab, this.active_item_index(), cx)
2867 }))
2868 .on_drop(cx.listener(move |this, selection: &DraggedSelection, cx| {
2869 this.handle_dragged_selection_drop(selection, None, cx)
2870 }))
2871 .on_drop(cx.listener(move |this, paths, cx| {
2872 this.handle_external_paths_drop(paths, cx)
2873 }))
2874 .map(|div| {
2875 let size = DefiniteLength::Fraction(0.5);
2876 match self.drag_split_direction {
2877 None => div.top_0().right_0().bottom_0().left_0(),
2878 Some(SplitDirection::Up) => {
2879 div.top_0().left_0().right_0().h(size)
2880 }
2881 Some(SplitDirection::Down) => {
2882 div.left_0().bottom_0().right_0().h(size)
2883 }
2884 Some(SplitDirection::Left) => {
2885 div.top_0().left_0().bottom_0().w(size)
2886 }
2887 Some(SplitDirection::Right) => {
2888 div.top_0().bottom_0().right_0().w(size)
2889 }
2890 }
2891 }),
2892 )
2893 })
2894 .on_mouse_down(
2895 MouseButton::Navigate(NavigationDirection::Back),
2896 cx.listener(|pane, _, cx| {
2897 if let Some(workspace) = pane.workspace.upgrade() {
2898 let pane = cx.view().downgrade();
2899 cx.window_context().defer(move |cx| {
2900 workspace.update(cx, |workspace, cx| {
2901 workspace.go_back(pane, cx).detach_and_log_err(cx)
2902 })
2903 })
2904 }
2905 }),
2906 )
2907 .on_mouse_down(
2908 MouseButton::Navigate(NavigationDirection::Forward),
2909 cx.listener(|pane, _, cx| {
2910 if let Some(workspace) = pane.workspace.upgrade() {
2911 let pane = cx.view().downgrade();
2912 cx.window_context().defer(move |cx| {
2913 workspace.update(cx, |workspace, cx| {
2914 workspace.go_forward(pane, cx).detach_and_log_err(cx)
2915 })
2916 })
2917 }
2918 }),
2919 )
2920 }
2921}
2922
2923impl ItemNavHistory {
2924 pub fn push<D: 'static + Send + Any>(&mut self, data: Option<D>, cx: &mut WindowContext) {
2925 self.history
2926 .push(data, self.item.clone(), self.is_preview, cx);
2927 }
2928
2929 pub fn pop_backward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
2930 self.history.pop(NavigationMode::GoingBack, cx)
2931 }
2932
2933 pub fn pop_forward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
2934 self.history.pop(NavigationMode::GoingForward, cx)
2935 }
2936}
2937
2938impl NavHistory {
2939 pub fn for_each_entry(
2940 &self,
2941 cx: &AppContext,
2942 mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
2943 ) {
2944 let borrowed_history = self.0.lock();
2945 borrowed_history
2946 .forward_stack
2947 .iter()
2948 .chain(borrowed_history.backward_stack.iter())
2949 .chain(borrowed_history.closed_stack.iter())
2950 .for_each(|entry| {
2951 if let Some(project_and_abs_path) =
2952 borrowed_history.paths_by_item.get(&entry.item.id())
2953 {
2954 f(entry, project_and_abs_path.clone());
2955 } else if let Some(item) = entry.item.upgrade() {
2956 if let Some(path) = item.project_path(cx) {
2957 f(entry, (path, None));
2958 }
2959 }
2960 })
2961 }
2962
2963 pub fn set_mode(&mut self, mode: NavigationMode) {
2964 self.0.lock().mode = mode;
2965 }
2966
2967 pub fn mode(&self) -> NavigationMode {
2968 self.0.lock().mode
2969 }
2970
2971 pub fn disable(&mut self) {
2972 self.0.lock().mode = NavigationMode::Disabled;
2973 }
2974
2975 pub fn enable(&mut self) {
2976 self.0.lock().mode = NavigationMode::Normal;
2977 }
2978
2979 pub fn pop(&mut self, mode: NavigationMode, cx: &mut WindowContext) -> Option<NavigationEntry> {
2980 let mut state = self.0.lock();
2981 let entry = match mode {
2982 NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
2983 return None
2984 }
2985 NavigationMode::GoingBack => &mut state.backward_stack,
2986 NavigationMode::GoingForward => &mut state.forward_stack,
2987 NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
2988 }
2989 .pop_back();
2990 if entry.is_some() {
2991 state.did_update(cx);
2992 }
2993 entry
2994 }
2995
2996 pub fn push<D: 'static + Send + Any>(
2997 &mut self,
2998 data: Option<D>,
2999 item: Arc<dyn WeakItemHandle>,
3000 is_preview: bool,
3001 cx: &mut WindowContext,
3002 ) {
3003 let state = &mut *self.0.lock();
3004 match state.mode {
3005 NavigationMode::Disabled => {}
3006 NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
3007 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3008 state.backward_stack.pop_front();
3009 }
3010 state.backward_stack.push_back(NavigationEntry {
3011 item,
3012 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3013 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3014 is_preview,
3015 });
3016 state.forward_stack.clear();
3017 }
3018 NavigationMode::GoingBack => {
3019 if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3020 state.forward_stack.pop_front();
3021 }
3022 state.forward_stack.push_back(NavigationEntry {
3023 item,
3024 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3025 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3026 is_preview,
3027 });
3028 }
3029 NavigationMode::GoingForward => {
3030 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3031 state.backward_stack.pop_front();
3032 }
3033 state.backward_stack.push_back(NavigationEntry {
3034 item,
3035 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3036 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3037 is_preview,
3038 });
3039 }
3040 NavigationMode::ClosingItem => {
3041 if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3042 state.closed_stack.pop_front();
3043 }
3044 state.closed_stack.push_back(NavigationEntry {
3045 item,
3046 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3047 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3048 is_preview,
3049 });
3050 }
3051 }
3052 state.did_update(cx);
3053 }
3054
3055 pub fn remove_item(&mut self, item_id: EntityId) {
3056 let mut state = self.0.lock();
3057 state.paths_by_item.remove(&item_id);
3058 state
3059 .backward_stack
3060 .retain(|entry| entry.item.id() != item_id);
3061 state
3062 .forward_stack
3063 .retain(|entry| entry.item.id() != item_id);
3064 state
3065 .closed_stack
3066 .retain(|entry| entry.item.id() != item_id);
3067 }
3068
3069 pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option<PathBuf>)> {
3070 self.0.lock().paths_by_item.get(&item_id).cloned()
3071 }
3072}
3073
3074impl NavHistoryState {
3075 pub fn did_update(&self, cx: &mut WindowContext) {
3076 if let Some(pane) = self.pane.upgrade() {
3077 cx.defer(move |cx| {
3078 pane.update(cx, |pane, cx| pane.history_updated(cx));
3079 });
3080 }
3081 }
3082}
3083
3084fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
3085 let path = buffer_path
3086 .as_ref()
3087 .and_then(|p| {
3088 p.path
3089 .to_str()
3090 .and_then(|s| if s.is_empty() { None } else { Some(s) })
3091 })
3092 .unwrap_or("This buffer");
3093 let path = truncate_and_remove_front(path, 80);
3094 format!("{path} contains unsaved edits. Do you want to save it?")
3095}
3096
3097pub fn tab_details(items: &[Box<dyn ItemHandle>], cx: &AppContext) -> Vec<usize> {
3098 let mut tab_details = items.iter().map(|_| 0).collect::<Vec<_>>();
3099 let mut tab_descriptions = HashMap::default();
3100 let mut done = false;
3101 while !done {
3102 done = true;
3103
3104 // Store item indices by their tab description.
3105 for (ix, (item, detail)) in items.iter().zip(&tab_details).enumerate() {
3106 if let Some(description) = item.tab_description(*detail, cx) {
3107 if *detail == 0
3108 || Some(&description) != item.tab_description(detail - 1, cx).as_ref()
3109 {
3110 tab_descriptions
3111 .entry(description)
3112 .or_insert(Vec::new())
3113 .push(ix);
3114 }
3115 }
3116 }
3117
3118 // If two or more items have the same tab description, increase their level
3119 // of detail and try again.
3120 for (_, item_ixs) in tab_descriptions.drain() {
3121 if item_ixs.len() > 1 {
3122 done = false;
3123 for ix in item_ixs {
3124 tab_details[ix] += 1;
3125 }
3126 }
3127 }
3128 }
3129
3130 tab_details
3131}
3132
3133pub fn render_item_indicator(item: Box<dyn ItemHandle>, cx: &WindowContext) -> Option<Indicator> {
3134 maybe!({
3135 let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
3136 (true, _) => Color::Warning,
3137 (_, true) => Color::Accent,
3138 (false, false) => return None,
3139 };
3140
3141 Some(Indicator::dot().color(indicator_color))
3142 })
3143}
3144
3145impl Render for DraggedTab {
3146 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3147 let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
3148 let label = self.item.tab_content(
3149 TabContentParams {
3150 detail: Some(self.detail),
3151 selected: false,
3152 preview: false,
3153 },
3154 cx,
3155 );
3156 Tab::new("")
3157 .selected(self.is_active)
3158 .child(label)
3159 .render(cx)
3160 .font(ui_font)
3161 }
3162}
3163
3164#[cfg(test)]
3165mod tests {
3166 use super::*;
3167 use crate::item::test::{TestItem, TestProjectItem};
3168 use gpui::{TestAppContext, VisualTestContext};
3169 use project::FakeFs;
3170 use settings::SettingsStore;
3171 use theme::LoadThemes;
3172
3173 #[gpui::test]
3174 async fn test_remove_active_empty(cx: &mut TestAppContext) {
3175 init_test(cx);
3176 let fs = FakeFs::new(cx.executor());
3177
3178 let project = Project::test(fs, None, cx).await;
3179 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3180 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3181
3182 pane.update(cx, |pane, cx| {
3183 assert!(pane
3184 .close_active_item(&CloseActiveItem { save_intent: None }, cx)
3185 .is_none())
3186 });
3187 }
3188
3189 #[gpui::test]
3190 async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
3191 init_test(cx);
3192 let fs = FakeFs::new(cx.executor());
3193
3194 let project = Project::test(fs, None, cx).await;
3195 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3196 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3197
3198 // 1. Add with a destination index
3199 // a. Add before the active item
3200 set_labeled_items(&pane, ["A", "B*", "C"], cx);
3201 pane.update(cx, |pane, cx| {
3202 pane.add_item(
3203 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3204 false,
3205 false,
3206 Some(0),
3207 cx,
3208 );
3209 });
3210 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
3211
3212 // b. Add after the active item
3213 set_labeled_items(&pane, ["A", "B*", "C"], cx);
3214 pane.update(cx, |pane, cx| {
3215 pane.add_item(
3216 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3217 false,
3218 false,
3219 Some(2),
3220 cx,
3221 );
3222 });
3223 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3224
3225 // c. Add at the end of the item list (including off the length)
3226 set_labeled_items(&pane, ["A", "B*", "C"], cx);
3227 pane.update(cx, |pane, cx| {
3228 pane.add_item(
3229 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3230 false,
3231 false,
3232 Some(5),
3233 cx,
3234 );
3235 });
3236 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3237
3238 // 2. Add without a destination index
3239 // a. Add with active item at the start of the item list
3240 set_labeled_items(&pane, ["A*", "B", "C"], cx);
3241 pane.update(cx, |pane, cx| {
3242 pane.add_item(
3243 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3244 false,
3245 false,
3246 None,
3247 cx,
3248 );
3249 });
3250 set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
3251
3252 // b. Add with active item at the end of the item list
3253 set_labeled_items(&pane, ["A", "B", "C*"], cx);
3254 pane.update(cx, |pane, cx| {
3255 pane.add_item(
3256 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3257 false,
3258 false,
3259 None,
3260 cx,
3261 );
3262 });
3263 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3264 }
3265
3266 #[gpui::test]
3267 async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
3268 init_test(cx);
3269 let fs = FakeFs::new(cx.executor());
3270
3271 let project = Project::test(fs, None, cx).await;
3272 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3273 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3274
3275 // 1. Add with a destination index
3276 // 1a. Add before the active item
3277 let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3278 pane.update(cx, |pane, cx| {
3279 pane.add_item(d, false, false, Some(0), cx);
3280 });
3281 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
3282
3283 // 1b. Add after the active item
3284 let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3285 pane.update(cx, |pane, cx| {
3286 pane.add_item(d, false, false, Some(2), cx);
3287 });
3288 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3289
3290 // 1c. Add at the end of the item list (including off the length)
3291 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3292 pane.update(cx, |pane, cx| {
3293 pane.add_item(a, false, false, Some(5), cx);
3294 });
3295 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3296
3297 // 1d. Add same item to active index
3298 let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3299 pane.update(cx, |pane, cx| {
3300 pane.add_item(b, false, false, Some(1), cx);
3301 });
3302 assert_item_labels(&pane, ["A", "B*", "C"], cx);
3303
3304 // 1e. Add item to index after same item in last position
3305 let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3306 pane.update(cx, |pane, cx| {
3307 pane.add_item(c, false, false, Some(2), cx);
3308 });
3309 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3310
3311 // 2. Add without a destination index
3312 // 2a. Add with active item at the start of the item list
3313 let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
3314 pane.update(cx, |pane, cx| {
3315 pane.add_item(d, false, false, None, cx);
3316 });
3317 assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
3318
3319 // 2b. Add with active item at the end of the item list
3320 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
3321 pane.update(cx, |pane, cx| {
3322 pane.add_item(a, false, false, None, cx);
3323 });
3324 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3325
3326 // 2c. Add active item to active item at end of list
3327 let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
3328 pane.update(cx, |pane, cx| {
3329 pane.add_item(c, false, false, None, cx);
3330 });
3331 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3332
3333 // 2d. Add active item to active item at start of list
3334 let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
3335 pane.update(cx, |pane, cx| {
3336 pane.add_item(a, false, false, None, cx);
3337 });
3338 assert_item_labels(&pane, ["A*", "B", "C"], cx);
3339 }
3340
3341 #[gpui::test]
3342 async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
3343 init_test(cx);
3344 let fs = FakeFs::new(cx.executor());
3345
3346 let project = Project::test(fs, None, cx).await;
3347 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3348 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3349
3350 // singleton view
3351 pane.update(cx, |pane, cx| {
3352 pane.add_item(
3353 Box::new(cx.new_view(|cx| {
3354 TestItem::new(cx)
3355 .with_singleton(true)
3356 .with_label("buffer 1")
3357 .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
3358 })),
3359 false,
3360 false,
3361 None,
3362 cx,
3363 );
3364 });
3365 assert_item_labels(&pane, ["buffer 1*"], cx);
3366
3367 // new singleton view with the same project entry
3368 pane.update(cx, |pane, cx| {
3369 pane.add_item(
3370 Box::new(cx.new_view(|cx| {
3371 TestItem::new(cx)
3372 .with_singleton(true)
3373 .with_label("buffer 1")
3374 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3375 })),
3376 false,
3377 false,
3378 None,
3379 cx,
3380 );
3381 });
3382 assert_item_labels(&pane, ["buffer 1*"], cx);
3383
3384 // new singleton view with different project entry
3385 pane.update(cx, |pane, cx| {
3386 pane.add_item(
3387 Box::new(cx.new_view(|cx| {
3388 TestItem::new(cx)
3389 .with_singleton(true)
3390 .with_label("buffer 2")
3391 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
3392 })),
3393 false,
3394 false,
3395 None,
3396 cx,
3397 );
3398 });
3399 assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
3400
3401 // new multibuffer view with the same project entry
3402 pane.update(cx, |pane, cx| {
3403 pane.add_item(
3404 Box::new(cx.new_view(|cx| {
3405 TestItem::new(cx)
3406 .with_singleton(false)
3407 .with_label("multibuffer 1")
3408 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3409 })),
3410 false,
3411 false,
3412 None,
3413 cx,
3414 );
3415 });
3416 assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
3417
3418 // another multibuffer view with the same project entry
3419 pane.update(cx, |pane, cx| {
3420 pane.add_item(
3421 Box::new(cx.new_view(|cx| {
3422 TestItem::new(cx)
3423 .with_singleton(false)
3424 .with_label("multibuffer 1b")
3425 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3426 })),
3427 false,
3428 false,
3429 None,
3430 cx,
3431 );
3432 });
3433 assert_item_labels(
3434 &pane,
3435 ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
3436 cx,
3437 );
3438 }
3439
3440 #[gpui::test]
3441 async fn test_remove_item_ordering_history(cx: &mut TestAppContext) {
3442 init_test(cx);
3443 let fs = FakeFs::new(cx.executor());
3444
3445 let project = Project::test(fs, None, cx).await;
3446 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3447 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3448
3449 add_labeled_item(&pane, "A", false, cx);
3450 add_labeled_item(&pane, "B", false, cx);
3451 add_labeled_item(&pane, "C", false, cx);
3452 add_labeled_item(&pane, "D", false, cx);
3453 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3454
3455 pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
3456 add_labeled_item(&pane, "1", false, cx);
3457 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
3458
3459 pane.update(cx, |pane, cx| {
3460 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3461 })
3462 .unwrap()
3463 .await
3464 .unwrap();
3465 assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
3466
3467 pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
3468 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3469
3470 pane.update(cx, |pane, cx| {
3471 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3472 })
3473 .unwrap()
3474 .await
3475 .unwrap();
3476 assert_item_labels(&pane, ["A", "B*", "C"], cx);
3477
3478 pane.update(cx, |pane, cx| {
3479 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3480 })
3481 .unwrap()
3482 .await
3483 .unwrap();
3484 assert_item_labels(&pane, ["A", "C*"], cx);
3485
3486 pane.update(cx, |pane, cx| {
3487 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3488 })
3489 .unwrap()
3490 .await
3491 .unwrap();
3492 assert_item_labels(&pane, ["A*"], cx);
3493 }
3494
3495 #[gpui::test]
3496 async fn test_remove_item_ordering_neighbour(cx: &mut TestAppContext) {
3497 init_test(cx);
3498 cx.update_global::<SettingsStore, ()>(|s, cx| {
3499 s.update_user_settings::<ItemSettings>(cx, |s| {
3500 s.activate_on_close = Some(ActivateOnClose::Neighbour);
3501 });
3502 });
3503 let fs = FakeFs::new(cx.executor());
3504
3505 let project = Project::test(fs, None, cx).await;
3506 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3507 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3508
3509 add_labeled_item(&pane, "A", false, cx);
3510 add_labeled_item(&pane, "B", false, cx);
3511 add_labeled_item(&pane, "C", false, cx);
3512 add_labeled_item(&pane, "D", false, cx);
3513 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3514
3515 pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
3516 add_labeled_item(&pane, "1", false, cx);
3517 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
3518
3519 pane.update(cx, |pane, cx| {
3520 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3521 })
3522 .unwrap()
3523 .await
3524 .unwrap();
3525 assert_item_labels(&pane, ["A", "B", "C*", "D"], cx);
3526
3527 pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
3528 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3529
3530 pane.update(cx, |pane, cx| {
3531 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3532 })
3533 .unwrap()
3534 .await
3535 .unwrap();
3536 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3537
3538 pane.update(cx, |pane, cx| {
3539 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3540 })
3541 .unwrap()
3542 .await
3543 .unwrap();
3544 assert_item_labels(&pane, ["A", "B*"], cx);
3545
3546 pane.update(cx, |pane, cx| {
3547 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3548 })
3549 .unwrap()
3550 .await
3551 .unwrap();
3552 assert_item_labels(&pane, ["A*"], cx);
3553 }
3554
3555 #[gpui::test]
3556 async fn test_close_inactive_items(cx: &mut TestAppContext) {
3557 init_test(cx);
3558 let fs = FakeFs::new(cx.executor());
3559
3560 let project = Project::test(fs, None, cx).await;
3561 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3562 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3563
3564 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
3565
3566 pane.update(cx, |pane, cx| {
3567 pane.close_inactive_items(
3568 &CloseInactiveItems {
3569 save_intent: None,
3570 close_pinned: false,
3571 },
3572 cx,
3573 )
3574 })
3575 .unwrap()
3576 .await
3577 .unwrap();
3578 assert_item_labels(&pane, ["C*"], cx);
3579 }
3580
3581 #[gpui::test]
3582 async fn test_close_clean_items(cx: &mut TestAppContext) {
3583 init_test(cx);
3584 let fs = FakeFs::new(cx.executor());
3585
3586 let project = Project::test(fs, None, cx).await;
3587 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3588 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3589
3590 add_labeled_item(&pane, "A", true, cx);
3591 add_labeled_item(&pane, "B", false, cx);
3592 add_labeled_item(&pane, "C", true, cx);
3593 add_labeled_item(&pane, "D", false, cx);
3594 add_labeled_item(&pane, "E", false, cx);
3595 assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
3596
3597 pane.update(cx, |pane, cx| {
3598 pane.close_clean_items(
3599 &CloseCleanItems {
3600 close_pinned: false,
3601 },
3602 cx,
3603 )
3604 })
3605 .unwrap()
3606 .await
3607 .unwrap();
3608 assert_item_labels(&pane, ["A^", "C*^"], cx);
3609 }
3610
3611 #[gpui::test]
3612 async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
3613 init_test(cx);
3614 let fs = FakeFs::new(cx.executor());
3615
3616 let project = Project::test(fs, None, cx).await;
3617 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3618 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3619
3620 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
3621
3622 pane.update(cx, |pane, cx| {
3623 pane.close_items_to_the_left(
3624 &CloseItemsToTheLeft {
3625 close_pinned: false,
3626 },
3627 cx,
3628 )
3629 })
3630 .unwrap()
3631 .await
3632 .unwrap();
3633 assert_item_labels(&pane, ["C*", "D", "E"], cx);
3634 }
3635
3636 #[gpui::test]
3637 async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
3638 init_test(cx);
3639 let fs = FakeFs::new(cx.executor());
3640
3641 let project = Project::test(fs, None, cx).await;
3642 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3643 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3644
3645 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
3646
3647 pane.update(cx, |pane, cx| {
3648 pane.close_items_to_the_right(
3649 &CloseItemsToTheRight {
3650 close_pinned: false,
3651 },
3652 cx,
3653 )
3654 })
3655 .unwrap()
3656 .await
3657 .unwrap();
3658 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3659 }
3660
3661 #[gpui::test]
3662 async fn test_close_all_items(cx: &mut TestAppContext) {
3663 init_test(cx);
3664 let fs = FakeFs::new(cx.executor());
3665
3666 let project = Project::test(fs, None, cx).await;
3667 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3668 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3669
3670 let item_a = add_labeled_item(&pane, "A", false, cx);
3671 add_labeled_item(&pane, "B", false, cx);
3672 add_labeled_item(&pane, "C", false, cx);
3673 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3674
3675 pane.update(cx, |pane, cx| {
3676 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
3677 pane.pin_tab_at(ix, cx);
3678 pane.close_all_items(
3679 &CloseAllItems {
3680 save_intent: None,
3681 close_pinned: false,
3682 },
3683 cx,
3684 )
3685 })
3686 .unwrap()
3687 .await
3688 .unwrap();
3689 assert_item_labels(&pane, ["A*"], cx);
3690
3691 pane.update(cx, |pane, cx| {
3692 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
3693 pane.unpin_tab_at(ix, cx);
3694 pane.close_all_items(
3695 &CloseAllItems {
3696 save_intent: None,
3697 close_pinned: false,
3698 },
3699 cx,
3700 )
3701 })
3702 .unwrap()
3703 .await
3704 .unwrap();
3705
3706 assert_item_labels(&pane, [], cx);
3707
3708 add_labeled_item(&pane, "A", true, cx);
3709 add_labeled_item(&pane, "B", true, cx);
3710 add_labeled_item(&pane, "C", true, cx);
3711 assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
3712
3713 let save = pane
3714 .update(cx, |pane, cx| {
3715 pane.close_all_items(
3716 &CloseAllItems {
3717 save_intent: None,
3718 close_pinned: false,
3719 },
3720 cx,
3721 )
3722 })
3723 .unwrap();
3724
3725 cx.executor().run_until_parked();
3726 cx.simulate_prompt_answer(2);
3727 save.await.unwrap();
3728 assert_item_labels(&pane, [], cx);
3729 }
3730
3731 #[gpui::test]
3732 async fn test_close_all_items_including_pinned(cx: &mut TestAppContext) {
3733 init_test(cx);
3734 let fs = FakeFs::new(cx.executor());
3735
3736 let project = Project::test(fs, None, cx).await;
3737 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3738 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3739
3740 let item_a = add_labeled_item(&pane, "A", false, cx);
3741 add_labeled_item(&pane, "B", false, cx);
3742 add_labeled_item(&pane, "C", false, cx);
3743 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3744
3745 pane.update(cx, |pane, cx| {
3746 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
3747 pane.pin_tab_at(ix, cx);
3748 pane.close_all_items(
3749 &CloseAllItems {
3750 save_intent: None,
3751 close_pinned: true,
3752 },
3753 cx,
3754 )
3755 })
3756 .unwrap()
3757 .await
3758 .unwrap();
3759 assert_item_labels(&pane, [], cx);
3760 }
3761
3762 fn init_test(cx: &mut TestAppContext) {
3763 cx.update(|cx| {
3764 let settings_store = SettingsStore::test(cx);
3765 cx.set_global(settings_store);
3766 theme::init(LoadThemes::JustBase, cx);
3767 crate::init_settings(cx);
3768 Project::init_settings(cx);
3769 });
3770 }
3771
3772 fn add_labeled_item(
3773 pane: &View<Pane>,
3774 label: &str,
3775 is_dirty: bool,
3776 cx: &mut VisualTestContext,
3777 ) -> Box<View<TestItem>> {
3778 pane.update(cx, |pane, cx| {
3779 let labeled_item = Box::new(
3780 cx.new_view(|cx| TestItem::new(cx).with_label(label).with_dirty(is_dirty)),
3781 );
3782 pane.add_item(labeled_item.clone(), false, false, None, cx);
3783 labeled_item
3784 })
3785 }
3786
3787 fn set_labeled_items<const COUNT: usize>(
3788 pane: &View<Pane>,
3789 labels: [&str; COUNT],
3790 cx: &mut VisualTestContext,
3791 ) -> [Box<View<TestItem>>; COUNT] {
3792 pane.update(cx, |pane, cx| {
3793 pane.items.clear();
3794 let mut active_item_index = 0;
3795
3796 let mut index = 0;
3797 let items = labels.map(|mut label| {
3798 if label.ends_with('*') {
3799 label = label.trim_end_matches('*');
3800 active_item_index = index;
3801 }
3802
3803 let labeled_item = Box::new(cx.new_view(|cx| TestItem::new(cx).with_label(label)));
3804 pane.add_item(labeled_item.clone(), false, false, None, cx);
3805 index += 1;
3806 labeled_item
3807 });
3808
3809 pane.activate_item(active_item_index, false, false, cx);
3810
3811 items
3812 })
3813 }
3814
3815 // Assert the item label, with the active item label suffixed with a '*'
3816 fn assert_item_labels<const COUNT: usize>(
3817 pane: &View<Pane>,
3818 expected_states: [&str; COUNT],
3819 cx: &mut VisualTestContext,
3820 ) {
3821 pane.update(cx, |pane, cx| {
3822 let actual_states = pane
3823 .items
3824 .iter()
3825 .enumerate()
3826 .map(|(ix, item)| {
3827 let mut state = item
3828 .to_any()
3829 .downcast::<TestItem>()
3830 .unwrap()
3831 .read(cx)
3832 .label
3833 .clone();
3834 if ix == pane.active_item_index {
3835 state.push('*');
3836 }
3837 if item.is_dirty(cx) {
3838 state.push('^');
3839 }
3840 state
3841 })
3842 .collect::<Vec<_>>();
3843
3844 assert_eq!(
3845 actual_states, expected_states,
3846 "pane items do not match expectation"
3847 );
3848 })
3849 }
3850}