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