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