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