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