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