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