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