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