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