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