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 entity = cx.entity().clone();
2544 move |_, window, cx| {
2545 entity.update(cx, |pane, cx| pane.navigate_backward(window, cx))
2546 }
2547 })
2548 .disabled(!self.can_navigate_backward())
2549 .tooltip({
2550 let focus_handle = focus_handle.clone();
2551 move |window, cx| {
2552 Tooltip::for_action_in("Go Back", &GoBack, &focus_handle, window, cx)
2553 }
2554 });
2555
2556 let navigate_forward = IconButton::new("navigate_forward", IconName::ArrowRight)
2557 .icon_size(IconSize::Small)
2558 .on_click({
2559 let entity = cx.entity().clone();
2560 move |_, window, cx| entity.update(cx, |pane, cx| pane.navigate_forward(window, cx))
2561 })
2562 .disabled(!self.can_navigate_forward())
2563 .tooltip({
2564 let focus_handle = focus_handle.clone();
2565 move |window, cx| {
2566 Tooltip::for_action_in("Go Forward", &GoForward, &focus_handle, window, cx)
2567 }
2568 });
2569
2570 let mut tab_items = self
2571 .items
2572 .iter()
2573 .enumerate()
2574 .zip(tab_details(&self.items, cx))
2575 .map(|((ix, item), detail)| {
2576 self.render_tab(ix, &**item, detail, &focus_handle, window, cx)
2577 })
2578 .collect::<Vec<_>>();
2579 let tab_count = tab_items.len();
2580 let unpinned_tabs = tab_items.split_off(self.pinned_tab_count);
2581 let pinned_tabs = tab_items;
2582 TabBar::new("tab_bar")
2583 .when(
2584 self.display_nav_history_buttons.unwrap_or_default(),
2585 |tab_bar| {
2586 tab_bar
2587 .start_child(navigate_backward)
2588 .start_child(navigate_forward)
2589 },
2590 )
2591 .map(|tab_bar| {
2592 if self.show_tab_bar_buttons {
2593 let render_tab_buttons = self.render_tab_bar_buttons.clone();
2594 let (left_children, right_children) = render_tab_buttons(self, window, cx);
2595 tab_bar
2596 .start_children(left_children)
2597 .end_children(right_children)
2598 } else {
2599 tab_bar
2600 }
2601 })
2602 .children(pinned_tabs.len().ne(&0).then(|| {
2603 h_flex()
2604 .children(pinned_tabs)
2605 .border_r_2()
2606 .border_color(cx.theme().colors().border)
2607 }))
2608 .child(
2609 h_flex()
2610 .id("unpinned tabs")
2611 .overflow_x_scroll()
2612 .w_full()
2613 .track_scroll(&self.tab_bar_scroll_handle)
2614 .children(unpinned_tabs)
2615 .child(
2616 div()
2617 .id("tab_bar_drop_target")
2618 .min_w_6()
2619 // HACK: This empty child is currently necessary to force the drop target to appear
2620 // despite us setting a min width above.
2621 .child("")
2622 .h_full()
2623 .flex_grow()
2624 .drag_over::<DraggedTab>(|bar, _, _, cx| {
2625 bar.bg(cx.theme().colors().drop_target_background)
2626 })
2627 .drag_over::<DraggedSelection>(|bar, _, _, cx| {
2628 bar.bg(cx.theme().colors().drop_target_background)
2629 })
2630 .on_drop(cx.listener(
2631 move |this, dragged_tab: &DraggedTab, window, cx| {
2632 this.drag_split_direction = None;
2633 this.handle_tab_drop(dragged_tab, this.items.len(), window, cx)
2634 },
2635 ))
2636 .on_drop(cx.listener(
2637 move |this, selection: &DraggedSelection, window, cx| {
2638 this.drag_split_direction = None;
2639 this.handle_project_entry_drop(
2640 &selection.active_selection.entry_id,
2641 Some(tab_count),
2642 window,
2643 cx,
2644 )
2645 },
2646 ))
2647 .on_drop(cx.listener(move |this, paths, window, cx| {
2648 this.drag_split_direction = None;
2649 this.handle_external_paths_drop(paths, window, cx)
2650 }))
2651 .on_click(cx.listener(move |this, event: &ClickEvent, window, cx| {
2652 if event.up.click_count == 2 {
2653 window.dispatch_action(
2654 this.double_click_dispatch_action.boxed_clone(),
2655 cx,
2656 )
2657 }
2658 })),
2659 ),
2660 )
2661 }
2662
2663 pub fn render_menu_overlay(menu: &Entity<ContextMenu>) -> Div {
2664 div().absolute().bottom_0().right_0().size_0().child(
2665 deferred(anchored().anchor(Corner::TopRight).child(menu.clone())).with_priority(1),
2666 )
2667 }
2668
2669 pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut Context<Self>) {
2670 self.zoomed = zoomed;
2671 cx.notify();
2672 }
2673
2674 pub fn is_zoomed(&self) -> bool {
2675 self.zoomed
2676 }
2677
2678 fn handle_drag_move<T: 'static>(
2679 &mut self,
2680 event: &DragMoveEvent<T>,
2681 window: &mut Window,
2682 cx: &mut Context<Self>,
2683 ) {
2684 let can_split_predicate = self.can_split_predicate.take();
2685 let can_split = match &can_split_predicate {
2686 Some(can_split_predicate) => {
2687 can_split_predicate(self, event.dragged_item(), window, cx)
2688 }
2689 None => false,
2690 };
2691 self.can_split_predicate = can_split_predicate;
2692 if !can_split {
2693 return;
2694 }
2695
2696 let rect = event.bounds.size;
2697
2698 let size = event.bounds.size.width.min(event.bounds.size.height)
2699 * WorkspaceSettings::get_global(cx).drop_target_size;
2700
2701 let relative_cursor = Point::new(
2702 event.event.position.x - event.bounds.left(),
2703 event.event.position.y - event.bounds.top(),
2704 );
2705
2706 let direction = if relative_cursor.x < size
2707 || relative_cursor.x > rect.width - size
2708 || relative_cursor.y < size
2709 || relative_cursor.y > rect.height - size
2710 {
2711 [
2712 SplitDirection::Up,
2713 SplitDirection::Right,
2714 SplitDirection::Down,
2715 SplitDirection::Left,
2716 ]
2717 .iter()
2718 .min_by_key(|side| match side {
2719 SplitDirection::Up => relative_cursor.y,
2720 SplitDirection::Right => rect.width - relative_cursor.x,
2721 SplitDirection::Down => rect.height - relative_cursor.y,
2722 SplitDirection::Left => relative_cursor.x,
2723 })
2724 .cloned()
2725 } else {
2726 None
2727 };
2728
2729 if direction != self.drag_split_direction {
2730 self.drag_split_direction = direction;
2731 }
2732 }
2733
2734 fn handle_tab_drop(
2735 &mut self,
2736 dragged_tab: &DraggedTab,
2737 ix: usize,
2738 window: &mut Window,
2739 cx: &mut Context<Self>,
2740 ) {
2741 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2742 if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_tab, window, cx) {
2743 return;
2744 }
2745 }
2746 let mut to_pane = cx.entity().clone();
2747 let split_direction = self.drag_split_direction;
2748 let item_id = dragged_tab.item.item_id();
2749 if let Some(preview_item_id) = self.preview_item_id {
2750 if item_id == preview_item_id {
2751 self.set_preview_item_id(None, cx);
2752 }
2753 }
2754
2755 let from_pane = dragged_tab.pane.clone();
2756 self.workspace
2757 .update(cx, |_, cx| {
2758 cx.defer_in(window, move |workspace, window, cx| {
2759 if let Some(split_direction) = split_direction {
2760 to_pane = workspace.split_pane(to_pane, split_direction, window, cx);
2761 }
2762 let old_ix = from_pane.read(cx).index_for_item_id(item_id);
2763 let old_len = to_pane.read(cx).items.len();
2764 move_item(&from_pane, &to_pane, item_id, ix, window, cx);
2765 if to_pane == from_pane {
2766 if let Some(old_index) = old_ix {
2767 to_pane.update(cx, |this, _| {
2768 if old_index < this.pinned_tab_count
2769 && (ix == this.items.len() || ix > this.pinned_tab_count)
2770 {
2771 this.pinned_tab_count -= 1;
2772 } else if this.has_pinned_tabs()
2773 && old_index >= this.pinned_tab_count
2774 && ix < this.pinned_tab_count
2775 {
2776 this.pinned_tab_count += 1;
2777 }
2778 });
2779 }
2780 } else {
2781 to_pane.update(cx, |this, _| {
2782 if this.items.len() > old_len // Did we not deduplicate on drag?
2783 && this.has_pinned_tabs()
2784 && ix < this.pinned_tab_count
2785 {
2786 this.pinned_tab_count += 1;
2787 }
2788 });
2789 from_pane.update(cx, |this, _| {
2790 if let Some(index) = old_ix {
2791 if this.pinned_tab_count > index {
2792 this.pinned_tab_count -= 1;
2793 }
2794 }
2795 })
2796 }
2797 });
2798 })
2799 .log_err();
2800 }
2801
2802 fn handle_dragged_selection_drop(
2803 &mut self,
2804 dragged_selection: &DraggedSelection,
2805 dragged_onto: Option<usize>,
2806 window: &mut Window,
2807 cx: &mut Context<Self>,
2808 ) {
2809 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2810 if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_selection, window, cx)
2811 {
2812 return;
2813 }
2814 }
2815 self.handle_project_entry_drop(
2816 &dragged_selection.active_selection.entry_id,
2817 dragged_onto,
2818 window,
2819 cx,
2820 );
2821 }
2822
2823 fn handle_project_entry_drop(
2824 &mut self,
2825 project_entry_id: &ProjectEntryId,
2826 target: Option<usize>,
2827 window: &mut Window,
2828 cx: &mut Context<Self>,
2829 ) {
2830 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2831 if let ControlFlow::Break(()) = custom_drop_handle(self, project_entry_id, window, cx) {
2832 return;
2833 }
2834 }
2835 let mut to_pane = cx.entity().clone();
2836 let split_direction = self.drag_split_direction;
2837 let project_entry_id = *project_entry_id;
2838 self.workspace
2839 .update(cx, |_, cx| {
2840 cx.defer_in(window, move |workspace, window, cx| {
2841 if let Some(path) = workspace
2842 .project()
2843 .read(cx)
2844 .path_for_entry(project_entry_id, cx)
2845 {
2846 let load_path_task = workspace.load_path(path, window, cx);
2847 cx.spawn_in(window, |workspace, mut cx| async move {
2848 if let Some((project_entry_id, build_item)) =
2849 load_path_task.await.notify_async_err(&mut cx)
2850 {
2851 let (to_pane, new_item_handle) = workspace
2852 .update_in(&mut cx, |workspace, window, cx| {
2853 if let Some(split_direction) = split_direction {
2854 to_pane = workspace.split_pane(
2855 to_pane,
2856 split_direction,
2857 window,
2858 cx,
2859 );
2860 }
2861 let new_item_handle = to_pane.update(cx, |pane, cx| {
2862 pane.open_item(
2863 project_entry_id,
2864 true,
2865 false,
2866 target,
2867 window,
2868 cx,
2869 build_item,
2870 )
2871 });
2872 (to_pane, new_item_handle)
2873 })
2874 .log_err()?;
2875 to_pane
2876 .update_in(&mut cx, |this, window, cx| {
2877 let Some(index) = this.index_for_item(&*new_item_handle)
2878 else {
2879 return;
2880 };
2881
2882 if target.map_or(false, |target| this.is_tab_pinned(target))
2883 {
2884 this.pin_tab_at(index, window, cx);
2885 }
2886 })
2887 .ok()?
2888 }
2889 Some(())
2890 })
2891 .detach();
2892 };
2893 });
2894 })
2895 .log_err();
2896 }
2897
2898 fn handle_external_paths_drop(
2899 &mut self,
2900 paths: &ExternalPaths,
2901 window: &mut Window,
2902 cx: &mut Context<Self>,
2903 ) {
2904 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2905 if let ControlFlow::Break(()) = custom_drop_handle(self, paths, window, cx) {
2906 return;
2907 }
2908 }
2909 let mut to_pane = cx.entity().clone();
2910 let mut split_direction = self.drag_split_direction;
2911 let paths = paths.paths().to_vec();
2912 let is_remote = self
2913 .workspace
2914 .update(cx, |workspace, cx| {
2915 if workspace.project().read(cx).is_via_collab() {
2916 workspace.show_error(
2917 &anyhow::anyhow!("Cannot drop files on a remote project"),
2918 cx,
2919 );
2920 true
2921 } else {
2922 false
2923 }
2924 })
2925 .unwrap_or(true);
2926 if is_remote {
2927 return;
2928 }
2929
2930 self.workspace
2931 .update(cx, |workspace, cx| {
2932 let fs = Arc::clone(workspace.project().read(cx).fs());
2933 cx.spawn_in(window, |workspace, mut cx| async move {
2934 let mut is_file_checks = FuturesUnordered::new();
2935 for path in &paths {
2936 is_file_checks.push(fs.is_file(path))
2937 }
2938 let mut has_files_to_open = false;
2939 while let Some(is_file) = is_file_checks.next().await {
2940 if is_file {
2941 has_files_to_open = true;
2942 break;
2943 }
2944 }
2945 drop(is_file_checks);
2946 if !has_files_to_open {
2947 split_direction = None;
2948 }
2949
2950 if let Ok(open_task) = workspace.update_in(&mut cx, |workspace, window, cx| {
2951 if let Some(split_direction) = split_direction {
2952 to_pane = workspace.split_pane(to_pane, split_direction, window, cx);
2953 }
2954 workspace.open_paths(
2955 paths,
2956 OpenVisible::OnlyDirectories,
2957 Some(to_pane.downgrade()),
2958 window,
2959 cx,
2960 )
2961 }) {
2962 let opened_items: Vec<_> = open_task.await;
2963 _ = workspace.update(&mut cx, |workspace, cx| {
2964 for item in opened_items.into_iter().flatten() {
2965 if let Err(e) = item {
2966 workspace.show_error(&e, cx);
2967 }
2968 }
2969 });
2970 }
2971 })
2972 .detach();
2973 })
2974 .log_err();
2975 }
2976
2977 pub fn display_nav_history_buttons(&mut self, display: Option<bool>) {
2978 self.display_nav_history_buttons = display;
2979 }
2980
2981 fn get_non_closeable_item_ids(&self, close_pinned: bool) -> Vec<EntityId> {
2982 if close_pinned {
2983 return vec![];
2984 }
2985
2986 self.items
2987 .iter()
2988 .map(|item| item.item_id())
2989 .filter(|item_id| {
2990 if let Some(ix) = self.index_for_item_id(*item_id) {
2991 self.is_tab_pinned(ix)
2992 } else {
2993 true
2994 }
2995 })
2996 .collect()
2997 }
2998
2999 pub fn drag_split_direction(&self) -> Option<SplitDirection> {
3000 self.drag_split_direction
3001 }
3002
3003 pub fn set_zoom_out_on_close(&mut self, zoom_out_on_close: bool) {
3004 self.zoom_out_on_close = zoom_out_on_close;
3005 }
3006}
3007
3008impl Focusable for Pane {
3009 fn focus_handle(&self, _cx: &App) -> FocusHandle {
3010 self.focus_handle.clone()
3011 }
3012}
3013
3014impl Render for Pane {
3015 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3016 let mut key_context = KeyContext::new_with_defaults();
3017 key_context.add("Pane");
3018 if self.active_item().is_none() {
3019 key_context.add("EmptyPane");
3020 }
3021
3022 let should_display_tab_bar = self.should_display_tab_bar.clone();
3023 let display_tab_bar = should_display_tab_bar(window, cx);
3024 let Some(project) = self.project.upgrade() else {
3025 return div().track_focus(&self.focus_handle(cx));
3026 };
3027 let is_local = project.read(cx).is_local();
3028
3029 v_flex()
3030 .key_context(key_context)
3031 .track_focus(&self.focus_handle(cx))
3032 .size_full()
3033 .flex_none()
3034 .overflow_hidden()
3035 .on_action(cx.listener(|pane, _: &AlternateFile, window, cx| {
3036 pane.alternate_file(window, cx);
3037 }))
3038 .on_action(
3039 cx.listener(|pane, _: &SplitLeft, _, cx| pane.split(SplitDirection::Left, cx)),
3040 )
3041 .on_action(cx.listener(|pane, _: &SplitUp, _, cx| pane.split(SplitDirection::Up, cx)))
3042 .on_action(cx.listener(|pane, _: &SplitHorizontal, _, cx| {
3043 pane.split(SplitDirection::horizontal(cx), cx)
3044 }))
3045 .on_action(cx.listener(|pane, _: &SplitVertical, _, cx| {
3046 pane.split(SplitDirection::vertical(cx), cx)
3047 }))
3048 .on_action(
3049 cx.listener(|pane, _: &SplitRight, _, cx| pane.split(SplitDirection::Right, cx)),
3050 )
3051 .on_action(
3052 cx.listener(|pane, _: &SplitDown, _, cx| pane.split(SplitDirection::Down, cx)),
3053 )
3054 .on_action(
3055 cx.listener(|pane, _: &GoBack, window, cx| pane.navigate_backward(window, cx)),
3056 )
3057 .on_action(
3058 cx.listener(|pane, _: &GoForward, window, cx| pane.navigate_forward(window, cx)),
3059 )
3060 .on_action(cx.listener(|_, _: &JoinIntoNext, _, cx| {
3061 cx.emit(Event::JoinIntoNext);
3062 }))
3063 .on_action(cx.listener(|_, _: &JoinAll, _, cx| {
3064 cx.emit(Event::JoinAll);
3065 }))
3066 .on_action(cx.listener(Pane::toggle_zoom))
3067 .on_action(
3068 cx.listener(|pane: &mut Pane, action: &ActivateItem, window, cx| {
3069 pane.activate_item(action.0, true, true, window, cx);
3070 }),
3071 )
3072 .on_action(
3073 cx.listener(|pane: &mut Pane, _: &ActivateLastItem, window, cx| {
3074 pane.activate_item(pane.items.len() - 1, true, true, window, cx);
3075 }),
3076 )
3077 .on_action(
3078 cx.listener(|pane: &mut Pane, _: &ActivatePrevItem, window, cx| {
3079 pane.activate_prev_item(true, window, cx);
3080 }),
3081 )
3082 .on_action(
3083 cx.listener(|pane: &mut Pane, _: &ActivateNextItem, window, cx| {
3084 pane.activate_next_item(true, window, cx);
3085 }),
3086 )
3087 .on_action(
3088 cx.listener(|pane, _: &SwapItemLeft, window, cx| pane.swap_item_left(window, cx)),
3089 )
3090 .on_action(
3091 cx.listener(|pane, _: &SwapItemRight, window, cx| pane.swap_item_right(window, cx)),
3092 )
3093 .on_action(cx.listener(|pane, action, window, cx| {
3094 pane.toggle_pin_tab(action, window, cx);
3095 }))
3096 .when(PreviewTabsSettings::get_global(cx).enabled, |this| {
3097 this.on_action(cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, _, cx| {
3098 if let Some(active_item_id) = pane.active_item().map(|i| i.item_id()) {
3099 if pane.is_active_preview_item(active_item_id) {
3100 pane.set_preview_item_id(None, cx);
3101 } else {
3102 pane.set_preview_item_id(Some(active_item_id), cx);
3103 }
3104 }
3105 }))
3106 })
3107 .on_action(
3108 cx.listener(|pane: &mut Self, action: &CloseActiveItem, window, cx| {
3109 if let Some(task) = pane.close_active_item(action, window, cx) {
3110 task.detach_and_log_err(cx)
3111 }
3112 }),
3113 )
3114 .on_action(
3115 cx.listener(|pane: &mut Self, action: &CloseInactiveItems, window, cx| {
3116 if let Some(task) = pane.close_inactive_items(action, window, cx) {
3117 task.detach_and_log_err(cx)
3118 }
3119 }),
3120 )
3121 .on_action(
3122 cx.listener(|pane: &mut Self, action: &CloseCleanItems, window, cx| {
3123 if let Some(task) = pane.close_clean_items(action, window, cx) {
3124 task.detach_and_log_err(cx)
3125 }
3126 }),
3127 )
3128 .on_action(cx.listener(
3129 |pane: &mut Self, action: &CloseItemsToTheLeft, window, cx| {
3130 if let Some(task) = pane.close_items_to_the_left(action, window, cx) {
3131 task.detach_and_log_err(cx)
3132 }
3133 },
3134 ))
3135 .on_action(cx.listener(
3136 |pane: &mut Self, action: &CloseItemsToTheRight, window, cx| {
3137 if let Some(task) = pane.close_items_to_the_right(action, window, cx) {
3138 task.detach_and_log_err(cx)
3139 }
3140 },
3141 ))
3142 .on_action(
3143 cx.listener(|pane: &mut Self, action: &CloseAllItems, window, cx| {
3144 if let Some(task) = pane.close_all_items(action, window, cx) {
3145 task.detach_and_log_err(cx)
3146 }
3147 }),
3148 )
3149 .on_action(
3150 cx.listener(|pane: &mut Self, action: &CloseActiveItem, window, cx| {
3151 if let Some(task) = pane.close_active_item(action, window, cx) {
3152 task.detach_and_log_err(cx)
3153 }
3154 }),
3155 )
3156 .on_action(
3157 cx.listener(|pane: &mut Self, action: &RevealInProjectPanel, _, cx| {
3158 let entry_id = action
3159 .entry_id
3160 .map(ProjectEntryId::from_proto)
3161 .or_else(|| pane.active_item()?.project_entry_ids(cx).first().copied());
3162 if let Some(entry_id) = entry_id {
3163 pane.project
3164 .update(cx, |_, cx| {
3165 cx.emit(project::Event::RevealInProjectPanel(entry_id))
3166 })
3167 .ok();
3168 }
3169 }),
3170 )
3171 .when(self.active_item().is_some() && display_tab_bar, |pane| {
3172 pane.child(self.render_tab_bar(window, cx))
3173 })
3174 .child({
3175 let has_worktrees = project.read(cx).worktrees(cx).next().is_some();
3176 // main content
3177 div()
3178 .flex_1()
3179 .relative()
3180 .group("")
3181 .overflow_hidden()
3182 .on_drag_move::<DraggedTab>(cx.listener(Self::handle_drag_move))
3183 .on_drag_move::<DraggedSelection>(cx.listener(Self::handle_drag_move))
3184 .when(is_local, |div| {
3185 div.on_drag_move::<ExternalPaths>(cx.listener(Self::handle_drag_move))
3186 })
3187 .map(|div| {
3188 if let Some(item) = self.active_item() {
3189 div.v_flex()
3190 .size_full()
3191 .overflow_hidden()
3192 .child(self.toolbar.clone())
3193 .child(item.to_any())
3194 } else {
3195 let placeholder = div.h_flex().size_full().justify_center();
3196 if has_worktrees {
3197 placeholder
3198 } else {
3199 placeholder.child(
3200 Label::new("Open a file or project to get started.")
3201 .color(Color::Muted),
3202 )
3203 }
3204 }
3205 })
3206 .child(
3207 // drag target
3208 div()
3209 .invisible()
3210 .absolute()
3211 .bg(cx.theme().colors().drop_target_background)
3212 .group_drag_over::<DraggedTab>("", |style| style.visible())
3213 .group_drag_over::<DraggedSelection>("", |style| style.visible())
3214 .when(is_local, |div| {
3215 div.group_drag_over::<ExternalPaths>("", |style| style.visible())
3216 })
3217 .when_some(self.can_drop_predicate.clone(), |this, p| {
3218 this.can_drop(move |a, window, cx| p(a, window, cx))
3219 })
3220 .on_drop(cx.listener(move |this, dragged_tab, window, cx| {
3221 this.handle_tab_drop(
3222 dragged_tab,
3223 this.active_item_index(),
3224 window,
3225 cx,
3226 )
3227 }))
3228 .on_drop(cx.listener(
3229 move |this, selection: &DraggedSelection, window, cx| {
3230 this.handle_dragged_selection_drop(selection, None, window, cx)
3231 },
3232 ))
3233 .on_drop(cx.listener(move |this, paths, window, cx| {
3234 this.handle_external_paths_drop(paths, window, cx)
3235 }))
3236 .map(|div| {
3237 let size = DefiniteLength::Fraction(0.5);
3238 match self.drag_split_direction {
3239 None => div.top_0().right_0().bottom_0().left_0(),
3240 Some(SplitDirection::Up) => {
3241 div.top_0().left_0().right_0().h(size)
3242 }
3243 Some(SplitDirection::Down) => {
3244 div.left_0().bottom_0().right_0().h(size)
3245 }
3246 Some(SplitDirection::Left) => {
3247 div.top_0().left_0().bottom_0().w(size)
3248 }
3249 Some(SplitDirection::Right) => {
3250 div.top_0().bottom_0().right_0().w(size)
3251 }
3252 }
3253 }),
3254 )
3255 })
3256 .on_mouse_down(
3257 MouseButton::Navigate(NavigationDirection::Back),
3258 cx.listener(|pane, _, window, cx| {
3259 if let Some(workspace) = pane.workspace.upgrade() {
3260 let pane = cx.entity().downgrade();
3261 window.defer(cx, move |window, cx| {
3262 workspace.update(cx, |workspace, cx| {
3263 workspace.go_back(pane, window, cx).detach_and_log_err(cx)
3264 })
3265 })
3266 }
3267 }),
3268 )
3269 .on_mouse_down(
3270 MouseButton::Navigate(NavigationDirection::Forward),
3271 cx.listener(|pane, _, window, cx| {
3272 if let Some(workspace) = pane.workspace.upgrade() {
3273 let pane = cx.entity().downgrade();
3274 window.defer(cx, move |window, cx| {
3275 workspace.update(cx, |workspace, cx| {
3276 workspace
3277 .go_forward(pane, window, cx)
3278 .detach_and_log_err(cx)
3279 })
3280 })
3281 }
3282 }),
3283 )
3284 }
3285}
3286
3287impl ItemNavHistory {
3288 pub fn push<D: 'static + Send + Any>(&mut self, data: Option<D>, cx: &mut App) {
3289 if self
3290 .item
3291 .upgrade()
3292 .is_some_and(|item| item.include_in_nav_history())
3293 {
3294 self.history
3295 .push(data, self.item.clone(), self.is_preview, cx);
3296 }
3297 }
3298
3299 pub fn pop_backward(&mut self, cx: &mut App) -> Option<NavigationEntry> {
3300 self.history.pop(NavigationMode::GoingBack, cx)
3301 }
3302
3303 pub fn pop_forward(&mut self, cx: &mut App) -> Option<NavigationEntry> {
3304 self.history.pop(NavigationMode::GoingForward, cx)
3305 }
3306}
3307
3308impl NavHistory {
3309 pub fn for_each_entry(
3310 &self,
3311 cx: &App,
3312 mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
3313 ) {
3314 let borrowed_history = self.0.lock();
3315 borrowed_history
3316 .forward_stack
3317 .iter()
3318 .chain(borrowed_history.backward_stack.iter())
3319 .chain(borrowed_history.closed_stack.iter())
3320 .for_each(|entry| {
3321 if let Some(project_and_abs_path) =
3322 borrowed_history.paths_by_item.get(&entry.item.id())
3323 {
3324 f(entry, project_and_abs_path.clone());
3325 } else if let Some(item) = entry.item.upgrade() {
3326 if let Some(path) = item.project_path(cx) {
3327 f(entry, (path, None));
3328 }
3329 }
3330 })
3331 }
3332
3333 pub fn set_mode(&mut self, mode: NavigationMode) {
3334 self.0.lock().mode = mode;
3335 }
3336
3337 pub fn mode(&self) -> NavigationMode {
3338 self.0.lock().mode
3339 }
3340
3341 pub fn disable(&mut self) {
3342 self.0.lock().mode = NavigationMode::Disabled;
3343 }
3344
3345 pub fn enable(&mut self) {
3346 self.0.lock().mode = NavigationMode::Normal;
3347 }
3348
3349 pub fn pop(&mut self, mode: NavigationMode, cx: &mut App) -> Option<NavigationEntry> {
3350 let mut state = self.0.lock();
3351 let entry = match mode {
3352 NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
3353 return None
3354 }
3355 NavigationMode::GoingBack => &mut state.backward_stack,
3356 NavigationMode::GoingForward => &mut state.forward_stack,
3357 NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
3358 }
3359 .pop_back();
3360 if entry.is_some() {
3361 state.did_update(cx);
3362 }
3363 entry
3364 }
3365
3366 pub fn push<D: 'static + Send + Any>(
3367 &mut self,
3368 data: Option<D>,
3369 item: Arc<dyn WeakItemHandle>,
3370 is_preview: bool,
3371 cx: &mut App,
3372 ) {
3373 let state = &mut *self.0.lock();
3374 match state.mode {
3375 NavigationMode::Disabled => {}
3376 NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
3377 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3378 state.backward_stack.pop_front();
3379 }
3380 state.backward_stack.push_back(NavigationEntry {
3381 item,
3382 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3383 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3384 is_preview,
3385 });
3386 state.forward_stack.clear();
3387 }
3388 NavigationMode::GoingBack => {
3389 if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3390 state.forward_stack.pop_front();
3391 }
3392 state.forward_stack.push_back(NavigationEntry {
3393 item,
3394 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3395 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3396 is_preview,
3397 });
3398 }
3399 NavigationMode::GoingForward => {
3400 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3401 state.backward_stack.pop_front();
3402 }
3403 state.backward_stack.push_back(NavigationEntry {
3404 item,
3405 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3406 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3407 is_preview,
3408 });
3409 }
3410 NavigationMode::ClosingItem => {
3411 if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3412 state.closed_stack.pop_front();
3413 }
3414 state.closed_stack.push_back(NavigationEntry {
3415 item,
3416 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3417 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3418 is_preview,
3419 });
3420 }
3421 }
3422 state.did_update(cx);
3423 }
3424
3425 pub fn remove_item(&mut self, item_id: EntityId) {
3426 let mut state = self.0.lock();
3427 state.paths_by_item.remove(&item_id);
3428 state
3429 .backward_stack
3430 .retain(|entry| entry.item.id() != item_id);
3431 state
3432 .forward_stack
3433 .retain(|entry| entry.item.id() != item_id);
3434 state
3435 .closed_stack
3436 .retain(|entry| entry.item.id() != item_id);
3437 }
3438
3439 pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option<PathBuf>)> {
3440 self.0.lock().paths_by_item.get(&item_id).cloned()
3441 }
3442}
3443
3444impl NavHistoryState {
3445 pub fn did_update(&self, cx: &mut App) {
3446 if let Some(pane) = self.pane.upgrade() {
3447 cx.defer(move |cx| {
3448 pane.update(cx, |pane, cx| pane.history_updated(cx));
3449 });
3450 }
3451 }
3452}
3453
3454fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
3455 let path = buffer_path
3456 .as_ref()
3457 .and_then(|p| {
3458 p.path
3459 .to_str()
3460 .and_then(|s| if s.is_empty() { None } else { Some(s) })
3461 })
3462 .unwrap_or("This buffer");
3463 let path = truncate_and_remove_front(path, 80);
3464 format!("{path} contains unsaved edits. Do you want to save it?")
3465}
3466
3467pub fn tab_details(items: &[Box<dyn ItemHandle>], cx: &App) -> Vec<usize> {
3468 let mut tab_details = items.iter().map(|_| 0).collect::<Vec<_>>();
3469 let mut tab_descriptions = HashMap::default();
3470 let mut done = false;
3471 while !done {
3472 done = true;
3473
3474 // Store item indices by their tab description.
3475 for (ix, (item, detail)) in items.iter().zip(&tab_details).enumerate() {
3476 if let Some(description) = item.tab_description(*detail, cx) {
3477 if *detail == 0
3478 || Some(&description) != item.tab_description(detail - 1, cx).as_ref()
3479 {
3480 tab_descriptions
3481 .entry(description)
3482 .or_insert(Vec::new())
3483 .push(ix);
3484 }
3485 }
3486 }
3487
3488 // If two or more items have the same tab description, increase their level
3489 // of detail and try again.
3490 for (_, item_ixs) in tab_descriptions.drain() {
3491 if item_ixs.len() > 1 {
3492 done = false;
3493 for ix in item_ixs {
3494 tab_details[ix] += 1;
3495 }
3496 }
3497 }
3498 }
3499
3500 tab_details
3501}
3502
3503pub fn render_item_indicator(item: Box<dyn ItemHandle>, cx: &App) -> Option<Indicator> {
3504 maybe!({
3505 let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
3506 (true, _) => Color::Warning,
3507 (_, true) => Color::Accent,
3508 (false, false) => return None,
3509 };
3510
3511 Some(Indicator::dot().color(indicator_color))
3512 })
3513}
3514
3515impl Render for DraggedTab {
3516 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3517 let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
3518 let label = self.item.tab_content(
3519 TabContentParams {
3520 detail: Some(self.detail),
3521 selected: false,
3522 preview: false,
3523 },
3524 window,
3525 cx,
3526 );
3527 Tab::new("")
3528 .toggle_state(self.is_active)
3529 .child(label)
3530 .render(window, cx)
3531 .font(ui_font)
3532 }
3533}
3534
3535#[cfg(test)]
3536mod tests {
3537 use std::num::NonZero;
3538
3539 use super::*;
3540 use crate::item::test::{TestItem, TestProjectItem};
3541 use gpui::{TestAppContext, VisualTestContext};
3542 use project::FakeFs;
3543 use settings::SettingsStore;
3544 use theme::LoadThemes;
3545
3546 #[gpui::test]
3547 async fn test_remove_active_empty(cx: &mut TestAppContext) {
3548 init_test(cx);
3549 let fs = FakeFs::new(cx.executor());
3550
3551 let project = Project::test(fs, None, cx).await;
3552 let (workspace, cx) =
3553 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3554 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3555
3556 pane.update_in(cx, |pane, window, cx| {
3557 assert!(pane
3558 .close_active_item(&CloseActiveItem { save_intent: None }, window, cx)
3559 .is_none())
3560 });
3561 }
3562
3563 #[gpui::test]
3564 async fn test_add_item_capped_to_max_tabs(cx: &mut TestAppContext) {
3565 init_test(cx);
3566 let fs = FakeFs::new(cx.executor());
3567
3568 let project = Project::test(fs, None, cx).await;
3569 let (workspace, cx) =
3570 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3571 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3572
3573 for i in 0..7 {
3574 add_labeled_item(&pane, format!("{}", i).as_str(), false, cx);
3575 }
3576 set_max_tabs(cx, Some(5));
3577 add_labeled_item(&pane, "7", false, cx);
3578 // Remove items to respect the max tab cap.
3579 assert_item_labels(&pane, ["3", "4", "5", "6", "7*"], cx);
3580 pane.update_in(cx, |pane, window, cx| {
3581 pane.activate_item(0, false, false, window, cx);
3582 });
3583 add_labeled_item(&pane, "X", false, cx);
3584 // Respect activation order.
3585 assert_item_labels(&pane, ["3", "X*", "5", "6", "7"], cx);
3586
3587 for i in 0..7 {
3588 add_labeled_item(&pane, format!("D{}", i).as_str(), true, cx);
3589 }
3590 // Keeps dirty items, even over max tab cap.
3591 assert_item_labels(
3592 &pane,
3593 ["D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6*^"],
3594 cx,
3595 );
3596
3597 set_max_tabs(cx, None);
3598 for i in 0..7 {
3599 add_labeled_item(&pane, format!("N{}", i).as_str(), false, cx);
3600 }
3601 // No cap when max tabs is None.
3602 assert_item_labels(
3603 &pane,
3604 [
3605 "D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6^", "N0", "N1", "N2", "N3", "N4",
3606 "N5", "N6*",
3607 ],
3608 cx,
3609 );
3610 }
3611
3612 #[gpui::test]
3613 async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
3614 init_test(cx);
3615 let fs = FakeFs::new(cx.executor());
3616
3617 let project = Project::test(fs, None, cx).await;
3618 let (workspace, cx) =
3619 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3620 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3621
3622 // 1. Add with a destination index
3623 // a. Add before the active item
3624 set_labeled_items(&pane, ["A", "B*", "C"], cx);
3625 pane.update_in(cx, |pane, window, cx| {
3626 pane.add_item(
3627 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3628 false,
3629 false,
3630 Some(0),
3631 window,
3632 cx,
3633 );
3634 });
3635 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
3636
3637 // b. Add after the active item
3638 set_labeled_items(&pane, ["A", "B*", "C"], cx);
3639 pane.update_in(cx, |pane, window, cx| {
3640 pane.add_item(
3641 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3642 false,
3643 false,
3644 Some(2),
3645 window,
3646 cx,
3647 );
3648 });
3649 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3650
3651 // c. Add at the end of the item list (including off the length)
3652 set_labeled_items(&pane, ["A", "B*", "C"], cx);
3653 pane.update_in(cx, |pane, window, cx| {
3654 pane.add_item(
3655 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3656 false,
3657 false,
3658 Some(5),
3659 window,
3660 cx,
3661 );
3662 });
3663 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3664
3665 // 2. Add without a destination index
3666 // a. Add with active item at the start of the item list
3667 set_labeled_items(&pane, ["A*", "B", "C"], cx);
3668 pane.update_in(cx, |pane, window, cx| {
3669 pane.add_item(
3670 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3671 false,
3672 false,
3673 None,
3674 window,
3675 cx,
3676 );
3677 });
3678 set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
3679
3680 // b. Add with active item at the end of the item list
3681 set_labeled_items(&pane, ["A", "B", "C*"], cx);
3682 pane.update_in(cx, |pane, window, cx| {
3683 pane.add_item(
3684 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3685 false,
3686 false,
3687 None,
3688 window,
3689 cx,
3690 );
3691 });
3692 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3693 }
3694
3695 #[gpui::test]
3696 async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
3697 init_test(cx);
3698 let fs = FakeFs::new(cx.executor());
3699
3700 let project = Project::test(fs, None, cx).await;
3701 let (workspace, cx) =
3702 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3703 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3704
3705 // 1. Add with a destination index
3706 // 1a. Add before the active item
3707 let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3708 pane.update_in(cx, |pane, window, cx| {
3709 pane.add_item(d, false, false, Some(0), window, cx);
3710 });
3711 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
3712
3713 // 1b. Add after the active item
3714 let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3715 pane.update_in(cx, |pane, window, cx| {
3716 pane.add_item(d, false, false, Some(2), window, cx);
3717 });
3718 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3719
3720 // 1c. Add at the end of the item list (including off the length)
3721 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3722 pane.update_in(cx, |pane, window, cx| {
3723 pane.add_item(a, false, false, Some(5), window, cx);
3724 });
3725 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3726
3727 // 1d. Add same item to active index
3728 let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3729 pane.update_in(cx, |pane, window, cx| {
3730 pane.add_item(b, false, false, Some(1), window, cx);
3731 });
3732 assert_item_labels(&pane, ["A", "B*", "C"], cx);
3733
3734 // 1e. Add item to index after same item in last position
3735 let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3736 pane.update_in(cx, |pane, window, cx| {
3737 pane.add_item(c, false, false, Some(2), window, cx);
3738 });
3739 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3740
3741 // 2. Add without a destination index
3742 // 2a. Add with active item at the start of the item list
3743 let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
3744 pane.update_in(cx, |pane, window, cx| {
3745 pane.add_item(d, false, false, None, window, cx);
3746 });
3747 assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
3748
3749 // 2b. Add with active item at the end of the item list
3750 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
3751 pane.update_in(cx, |pane, window, cx| {
3752 pane.add_item(a, false, false, None, window, cx);
3753 });
3754 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3755
3756 // 2c. Add active item to active item at end of list
3757 let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
3758 pane.update_in(cx, |pane, window, cx| {
3759 pane.add_item(c, false, false, None, window, cx);
3760 });
3761 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3762
3763 // 2d. Add active item to active item at start of list
3764 let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
3765 pane.update_in(cx, |pane, window, cx| {
3766 pane.add_item(a, false, false, None, window, cx);
3767 });
3768 assert_item_labels(&pane, ["A*", "B", "C"], cx);
3769 }
3770
3771 #[gpui::test]
3772 async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
3773 init_test(cx);
3774 let fs = FakeFs::new(cx.executor());
3775
3776 let project = Project::test(fs, None, cx).await;
3777 let (workspace, cx) =
3778 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3779 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3780
3781 // singleton view
3782 pane.update_in(cx, |pane, window, cx| {
3783 pane.add_item(
3784 Box::new(cx.new(|cx| {
3785 TestItem::new(cx)
3786 .with_singleton(true)
3787 .with_label("buffer 1")
3788 .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
3789 })),
3790 false,
3791 false,
3792 None,
3793 window,
3794 cx,
3795 );
3796 });
3797 assert_item_labels(&pane, ["buffer 1*"], cx);
3798
3799 // new singleton view with the same project entry
3800 pane.update_in(cx, |pane, window, cx| {
3801 pane.add_item(
3802 Box::new(cx.new(|cx| {
3803 TestItem::new(cx)
3804 .with_singleton(true)
3805 .with_label("buffer 1")
3806 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3807 })),
3808 false,
3809 false,
3810 None,
3811 window,
3812 cx,
3813 );
3814 });
3815 assert_item_labels(&pane, ["buffer 1*"], cx);
3816
3817 // new singleton view with different project entry
3818 pane.update_in(cx, |pane, window, cx| {
3819 pane.add_item(
3820 Box::new(cx.new(|cx| {
3821 TestItem::new(cx)
3822 .with_singleton(true)
3823 .with_label("buffer 2")
3824 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
3825 })),
3826 false,
3827 false,
3828 None,
3829 window,
3830 cx,
3831 );
3832 });
3833 assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
3834
3835 // new multibuffer view with the same project entry
3836 pane.update_in(cx, |pane, window, cx| {
3837 pane.add_item(
3838 Box::new(cx.new(|cx| {
3839 TestItem::new(cx)
3840 .with_singleton(false)
3841 .with_label("multibuffer 1")
3842 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3843 })),
3844 false,
3845 false,
3846 None,
3847 window,
3848 cx,
3849 );
3850 });
3851 assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
3852
3853 // another multibuffer view with the same project entry
3854 pane.update_in(cx, |pane, window, cx| {
3855 pane.add_item(
3856 Box::new(cx.new(|cx| {
3857 TestItem::new(cx)
3858 .with_singleton(false)
3859 .with_label("multibuffer 1b")
3860 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3861 })),
3862 false,
3863 false,
3864 None,
3865 window,
3866 cx,
3867 );
3868 });
3869 assert_item_labels(
3870 &pane,
3871 ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
3872 cx,
3873 );
3874 }
3875
3876 #[gpui::test]
3877 async fn test_remove_item_ordering_history(cx: &mut TestAppContext) {
3878 init_test(cx);
3879 let fs = FakeFs::new(cx.executor());
3880
3881 let project = Project::test(fs, None, cx).await;
3882 let (workspace, cx) =
3883 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3884 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3885
3886 add_labeled_item(&pane, "A", false, cx);
3887 add_labeled_item(&pane, "B", false, cx);
3888 add_labeled_item(&pane, "C", false, cx);
3889 add_labeled_item(&pane, "D", false, cx);
3890 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3891
3892 pane.update_in(cx, |pane, window, cx| {
3893 pane.activate_item(1, false, false, window, cx)
3894 });
3895 add_labeled_item(&pane, "1", false, cx);
3896 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
3897
3898 pane.update_in(cx, |pane, window, cx| {
3899 pane.close_active_item(&CloseActiveItem { save_intent: None }, window, cx)
3900 })
3901 .unwrap()
3902 .await
3903 .unwrap();
3904 assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
3905
3906 pane.update_in(cx, |pane, window, cx| {
3907 pane.activate_item(3, false, false, window, cx)
3908 });
3909 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3910
3911 pane.update_in(cx, |pane, window, cx| {
3912 pane.close_active_item(&CloseActiveItem { save_intent: None }, window, cx)
3913 })
3914 .unwrap()
3915 .await
3916 .unwrap();
3917 assert_item_labels(&pane, ["A", "B*", "C"], cx);
3918
3919 pane.update_in(cx, |pane, window, cx| {
3920 pane.close_active_item(&CloseActiveItem { save_intent: None }, window, cx)
3921 })
3922 .unwrap()
3923 .await
3924 .unwrap();
3925 assert_item_labels(&pane, ["A", "C*"], cx);
3926
3927 pane.update_in(cx, |pane, window, cx| {
3928 pane.close_active_item(&CloseActiveItem { save_intent: None }, window, cx)
3929 })
3930 .unwrap()
3931 .await
3932 .unwrap();
3933 assert_item_labels(&pane, ["A*"], cx);
3934 }
3935
3936 #[gpui::test]
3937 async fn test_remove_item_ordering_neighbour(cx: &mut TestAppContext) {
3938 init_test(cx);
3939 cx.update_global::<SettingsStore, ()>(|s, cx| {
3940 s.update_user_settings::<ItemSettings>(cx, |s| {
3941 s.activate_on_close = Some(ActivateOnClose::Neighbour);
3942 });
3943 });
3944 let fs = FakeFs::new(cx.executor());
3945
3946 let project = Project::test(fs, None, cx).await;
3947 let (workspace, cx) =
3948 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3949 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3950
3951 add_labeled_item(&pane, "A", false, cx);
3952 add_labeled_item(&pane, "B", false, cx);
3953 add_labeled_item(&pane, "C", false, cx);
3954 add_labeled_item(&pane, "D", false, cx);
3955 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3956
3957 pane.update_in(cx, |pane, window, cx| {
3958 pane.activate_item(1, false, false, window, cx)
3959 });
3960 add_labeled_item(&pane, "1", false, cx);
3961 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
3962
3963 pane.update_in(cx, |pane, window, cx| {
3964 pane.close_active_item(&CloseActiveItem { save_intent: None }, window, cx)
3965 })
3966 .unwrap()
3967 .await
3968 .unwrap();
3969 assert_item_labels(&pane, ["A", "B", "C*", "D"], cx);
3970
3971 pane.update_in(cx, |pane, window, cx| {
3972 pane.activate_item(3, false, false, window, cx)
3973 });
3974 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3975
3976 pane.update_in(cx, |pane, window, cx| {
3977 pane.close_active_item(&CloseActiveItem { save_intent: None }, window, cx)
3978 })
3979 .unwrap()
3980 .await
3981 .unwrap();
3982 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3983
3984 pane.update_in(cx, |pane, window, cx| {
3985 pane.close_active_item(&CloseActiveItem { save_intent: None }, window, cx)
3986 })
3987 .unwrap()
3988 .await
3989 .unwrap();
3990 assert_item_labels(&pane, ["A", "B*"], cx);
3991
3992 pane.update_in(cx, |pane, window, cx| {
3993 pane.close_active_item(&CloseActiveItem { save_intent: None }, window, cx)
3994 })
3995 .unwrap()
3996 .await
3997 .unwrap();
3998 assert_item_labels(&pane, ["A*"], cx);
3999 }
4000
4001 #[gpui::test]
4002 async fn test_remove_item_ordering_left_neighbour(cx: &mut TestAppContext) {
4003 init_test(cx);
4004 cx.update_global::<SettingsStore, ()>(|s, cx| {
4005 s.update_user_settings::<ItemSettings>(cx, |s| {
4006 s.activate_on_close = Some(ActivateOnClose::LeftNeighbour);
4007 });
4008 });
4009 let fs = FakeFs::new(cx.executor());
4010
4011 let project = Project::test(fs, None, cx).await;
4012 let (workspace, cx) =
4013 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4014 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4015
4016 add_labeled_item(&pane, "A", false, cx);
4017 add_labeled_item(&pane, "B", false, cx);
4018 add_labeled_item(&pane, "C", false, cx);
4019 add_labeled_item(&pane, "D", false, cx);
4020 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4021
4022 pane.update_in(cx, |pane, window, cx| {
4023 pane.activate_item(1, false, false, window, cx)
4024 });
4025 add_labeled_item(&pane, "1", false, cx);
4026 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
4027
4028 pane.update_in(cx, |pane, window, cx| {
4029 pane.close_active_item(&CloseActiveItem { save_intent: None }, window, cx)
4030 })
4031 .unwrap()
4032 .await
4033 .unwrap();
4034 assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
4035
4036 pane.update_in(cx, |pane, window, cx| {
4037 pane.activate_item(3, false, false, window, cx)
4038 });
4039 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4040
4041 pane.update_in(cx, |pane, window, cx| {
4042 pane.close_active_item(&CloseActiveItem { save_intent: None }, window, cx)
4043 })
4044 .unwrap()
4045 .await
4046 .unwrap();
4047 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4048
4049 pane.update_in(cx, |pane, window, cx| {
4050 pane.activate_item(0, false, false, window, cx)
4051 });
4052 assert_item_labels(&pane, ["A*", "B", "C"], cx);
4053
4054 pane.update_in(cx, |pane, window, cx| {
4055 pane.close_active_item(&CloseActiveItem { save_intent: None }, window, cx)
4056 })
4057 .unwrap()
4058 .await
4059 .unwrap();
4060 assert_item_labels(&pane, ["B*", "C"], cx);
4061
4062 pane.update_in(cx, |pane, window, cx| {
4063 pane.close_active_item(&CloseActiveItem { save_intent: None }, window, cx)
4064 })
4065 .unwrap()
4066 .await
4067 .unwrap();
4068 assert_item_labels(&pane, ["C*"], cx);
4069 }
4070
4071 #[gpui::test]
4072 async fn test_close_inactive_items(cx: &mut TestAppContext) {
4073 init_test(cx);
4074 let fs = FakeFs::new(cx.executor());
4075
4076 let project = Project::test(fs, None, cx).await;
4077 let (workspace, cx) =
4078 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4079 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4080
4081 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
4082
4083 pane.update_in(cx, |pane, window, cx| {
4084 pane.close_inactive_items(
4085 &CloseInactiveItems {
4086 save_intent: None,
4087 close_pinned: false,
4088 },
4089 window,
4090 cx,
4091 )
4092 })
4093 .unwrap()
4094 .await
4095 .unwrap();
4096 assert_item_labels(&pane, ["C*"], cx);
4097 }
4098
4099 #[gpui::test]
4100 async fn test_close_clean_items(cx: &mut TestAppContext) {
4101 init_test(cx);
4102 let fs = FakeFs::new(cx.executor());
4103
4104 let project = Project::test(fs, None, cx).await;
4105 let (workspace, cx) =
4106 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4107 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4108
4109 add_labeled_item(&pane, "A", true, cx);
4110 add_labeled_item(&pane, "B", false, cx);
4111 add_labeled_item(&pane, "C", true, cx);
4112 add_labeled_item(&pane, "D", false, cx);
4113 add_labeled_item(&pane, "E", false, cx);
4114 assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
4115
4116 pane.update_in(cx, |pane, window, cx| {
4117 pane.close_clean_items(
4118 &CloseCleanItems {
4119 close_pinned: false,
4120 },
4121 window,
4122 cx,
4123 )
4124 })
4125 .unwrap()
4126 .await
4127 .unwrap();
4128 assert_item_labels(&pane, ["A^", "C*^"], cx);
4129 }
4130
4131 #[gpui::test]
4132 async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
4133 init_test(cx);
4134 let fs = FakeFs::new(cx.executor());
4135
4136 let project = Project::test(fs, None, cx).await;
4137 let (workspace, cx) =
4138 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4139 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4140
4141 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
4142
4143 pane.update_in(cx, |pane, window, cx| {
4144 pane.close_items_to_the_left(
4145 &CloseItemsToTheLeft {
4146 close_pinned: false,
4147 },
4148 window,
4149 cx,
4150 )
4151 })
4152 .unwrap()
4153 .await
4154 .unwrap();
4155 assert_item_labels(&pane, ["C*", "D", "E"], cx);
4156 }
4157
4158 #[gpui::test]
4159 async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
4160 init_test(cx);
4161 let fs = FakeFs::new(cx.executor());
4162
4163 let project = Project::test(fs, None, cx).await;
4164 let (workspace, cx) =
4165 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4166 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4167
4168 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
4169
4170 pane.update_in(cx, |pane, window, cx| {
4171 pane.close_items_to_the_right(
4172 &CloseItemsToTheRight {
4173 close_pinned: false,
4174 },
4175 window,
4176 cx,
4177 )
4178 })
4179 .unwrap()
4180 .await
4181 .unwrap();
4182 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4183 }
4184
4185 #[gpui::test]
4186 async fn test_close_all_items(cx: &mut TestAppContext) {
4187 init_test(cx);
4188 let fs = FakeFs::new(cx.executor());
4189
4190 let project = Project::test(fs, None, cx).await;
4191 let (workspace, cx) =
4192 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4193 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4194
4195 let item_a = add_labeled_item(&pane, "A", false, cx);
4196 add_labeled_item(&pane, "B", false, cx);
4197 add_labeled_item(&pane, "C", false, cx);
4198 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4199
4200 pane.update_in(cx, |pane, window, cx| {
4201 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4202 pane.pin_tab_at(ix, window, cx);
4203 pane.close_all_items(
4204 &CloseAllItems {
4205 save_intent: None,
4206 close_pinned: false,
4207 },
4208 window,
4209 cx,
4210 )
4211 })
4212 .unwrap()
4213 .await
4214 .unwrap();
4215 assert_item_labels(&pane, ["A*"], cx);
4216
4217 pane.update_in(cx, |pane, window, cx| {
4218 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4219 pane.unpin_tab_at(ix, window, cx);
4220 pane.close_all_items(
4221 &CloseAllItems {
4222 save_intent: None,
4223 close_pinned: false,
4224 },
4225 window,
4226 cx,
4227 )
4228 })
4229 .unwrap()
4230 .await
4231 .unwrap();
4232
4233 assert_item_labels(&pane, [], cx);
4234
4235 add_labeled_item(&pane, "A", true, cx).update(cx, |item, cx| {
4236 item.project_items
4237 .push(TestProjectItem::new(1, "A.txt", cx))
4238 });
4239 add_labeled_item(&pane, "B", true, cx).update(cx, |item, cx| {
4240 item.project_items
4241 .push(TestProjectItem::new(2, "B.txt", cx))
4242 });
4243 add_labeled_item(&pane, "C", true, cx).update(cx, |item, cx| {
4244 item.project_items
4245 .push(TestProjectItem::new(3, "C.txt", cx))
4246 });
4247 assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
4248
4249 let save = pane
4250 .update_in(cx, |pane, window, cx| {
4251 pane.close_all_items(
4252 &CloseAllItems {
4253 save_intent: None,
4254 close_pinned: false,
4255 },
4256 window,
4257 cx,
4258 )
4259 })
4260 .unwrap();
4261
4262 cx.executor().run_until_parked();
4263 cx.simulate_prompt_answer(2);
4264 save.await.unwrap();
4265 assert_item_labels(&pane, [], cx);
4266
4267 add_labeled_item(&pane, "A", true, cx);
4268 add_labeled_item(&pane, "B", true, cx);
4269 add_labeled_item(&pane, "C", true, cx);
4270 assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
4271 let save = pane
4272 .update_in(cx, |pane, window, cx| {
4273 pane.close_all_items(
4274 &CloseAllItems {
4275 save_intent: None,
4276 close_pinned: false,
4277 },
4278 window,
4279 cx,
4280 )
4281 })
4282 .unwrap();
4283
4284 cx.executor().run_until_parked();
4285 cx.simulate_prompt_answer(2);
4286 save.await.unwrap();
4287 assert_item_labels(&pane, [], cx);
4288 }
4289
4290 #[gpui::test]
4291 async fn test_close_all_items_including_pinned(cx: &mut TestAppContext) {
4292 init_test(cx);
4293 let fs = FakeFs::new(cx.executor());
4294
4295 let project = Project::test(fs, None, cx).await;
4296 let (workspace, cx) =
4297 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4298 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4299
4300 let item_a = add_labeled_item(&pane, "A", false, cx);
4301 add_labeled_item(&pane, "B", false, cx);
4302 add_labeled_item(&pane, "C", false, cx);
4303 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4304
4305 pane.update_in(cx, |pane, window, cx| {
4306 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4307 pane.pin_tab_at(ix, window, cx);
4308 pane.close_all_items(
4309 &CloseAllItems {
4310 save_intent: None,
4311 close_pinned: true,
4312 },
4313 window,
4314 cx,
4315 )
4316 })
4317 .unwrap()
4318 .await
4319 .unwrap();
4320 assert_item_labels(&pane, [], cx);
4321 }
4322
4323 fn init_test(cx: &mut TestAppContext) {
4324 cx.update(|cx| {
4325 let settings_store = SettingsStore::test(cx);
4326 cx.set_global(settings_store);
4327 theme::init(LoadThemes::JustBase, cx);
4328 crate::init_settings(cx);
4329 Project::init_settings(cx);
4330 });
4331 }
4332
4333 fn set_max_tabs(cx: &mut TestAppContext, value: Option<usize>) {
4334 cx.update_global(|store: &mut SettingsStore, cx| {
4335 store.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4336 settings.max_tabs = value.map(|v| NonZero::new(v).unwrap())
4337 });
4338 });
4339 }
4340
4341 fn add_labeled_item(
4342 pane: &Entity<Pane>,
4343 label: &str,
4344 is_dirty: bool,
4345 cx: &mut VisualTestContext,
4346 ) -> Box<Entity<TestItem>> {
4347 pane.update_in(cx, |pane, window, cx| {
4348 let labeled_item =
4349 Box::new(cx.new(|cx| TestItem::new(cx).with_label(label).with_dirty(is_dirty)));
4350 pane.add_item(labeled_item.clone(), false, false, None, window, cx);
4351 labeled_item
4352 })
4353 }
4354
4355 fn set_labeled_items<const COUNT: usize>(
4356 pane: &Entity<Pane>,
4357 labels: [&str; COUNT],
4358 cx: &mut VisualTestContext,
4359 ) -> [Box<Entity<TestItem>>; COUNT] {
4360 pane.update_in(cx, |pane, window, cx| {
4361 pane.items.clear();
4362 let mut active_item_index = 0;
4363
4364 let mut index = 0;
4365 let items = labels.map(|mut label| {
4366 if label.ends_with('*') {
4367 label = label.trim_end_matches('*');
4368 active_item_index = index;
4369 }
4370
4371 let labeled_item = Box::new(cx.new(|cx| TestItem::new(cx).with_label(label)));
4372 pane.add_item(labeled_item.clone(), false, false, None, window, cx);
4373 index += 1;
4374 labeled_item
4375 });
4376
4377 pane.activate_item(active_item_index, false, false, window, cx);
4378
4379 items
4380 })
4381 }
4382
4383 // Assert the item label, with the active item label suffixed with a '*'
4384 #[track_caller]
4385 fn assert_item_labels<const COUNT: usize>(
4386 pane: &Entity<Pane>,
4387 expected_states: [&str; COUNT],
4388 cx: &mut VisualTestContext,
4389 ) {
4390 let actual_states = pane.update(cx, |pane, cx| {
4391 pane.items
4392 .iter()
4393 .enumerate()
4394 .map(|(ix, item)| {
4395 let mut state = item
4396 .to_any()
4397 .downcast::<TestItem>()
4398 .unwrap()
4399 .read(cx)
4400 .label
4401 .clone();
4402 if ix == pane.active_item_index {
4403 state.push('*');
4404 }
4405 if item.is_dirty(cx) {
4406 state.push('^');
4407 }
4408 state
4409 })
4410 .collect::<Vec<_>>()
4411 });
4412 assert_eq!(
4413 actual_states, expected_states,
4414 "pane items do not match expectation"
4415 );
4416 }
4417}