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