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