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, async move |pane, cx| {
1586 let dirty_items = workspace.update(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(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(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(project.clone(), &pane, &*item_to_close, save_intent, cx)
1628 .await?
1629 {
1630 break;
1631 }
1632 }
1633
1634 // Remove the item from the pane.
1635 pane.update_in(cx, |pane, window, cx| {
1636 pane.remove_item(
1637 item_to_close.item_id(),
1638 false,
1639 pane.close_pane_if_empty,
1640 window,
1641 cx,
1642 );
1643 pane.remove_item(item_to_close.item_id(), false, true, window, cx);
1644 })
1645 .ok();
1646 }
1647
1648 pane.update(cx, |_, cx| cx.notify()).ok();
1649 Ok(())
1650 })
1651 }
1652
1653 pub fn remove_item(
1654 &mut self,
1655 item_id: EntityId,
1656 activate_pane: bool,
1657 close_pane_if_empty: bool,
1658 window: &mut Window,
1659 cx: &mut Context<Self>,
1660 ) {
1661 let Some(item_index) = self.index_for_item_id(item_id) else {
1662 return;
1663 };
1664 self._remove_item(
1665 item_index,
1666 activate_pane,
1667 close_pane_if_empty,
1668 None,
1669 window,
1670 cx,
1671 )
1672 }
1673
1674 pub fn remove_item_and_focus_on_pane(
1675 &mut self,
1676 item_index: usize,
1677 activate_pane: bool,
1678 focus_on_pane_if_closed: Entity<Pane>,
1679 window: &mut Window,
1680 cx: &mut Context<Self>,
1681 ) {
1682 self._remove_item(
1683 item_index,
1684 activate_pane,
1685 true,
1686 Some(focus_on_pane_if_closed),
1687 window,
1688 cx,
1689 )
1690 }
1691
1692 fn _remove_item(
1693 &mut self,
1694 item_index: usize,
1695 activate_pane: bool,
1696 close_pane_if_empty: bool,
1697 focus_on_pane_if_closed: Option<Entity<Pane>>,
1698 window: &mut Window,
1699 cx: &mut Context<Self>,
1700 ) {
1701 let activate_on_close = &ItemSettings::get_global(cx).activate_on_close;
1702 self.activation_history
1703 .retain(|entry| entry.entity_id != self.items[item_index].item_id());
1704
1705 if self.is_tab_pinned(item_index) {
1706 self.pinned_tab_count -= 1;
1707 }
1708 if item_index == self.active_item_index {
1709 let left_neighbour_index = || item_index.min(self.items.len()).saturating_sub(1);
1710 let index_to_activate = match activate_on_close {
1711 ActivateOnClose::History => self
1712 .activation_history
1713 .pop()
1714 .and_then(|last_activated_item| {
1715 self.items.iter().enumerate().find_map(|(index, item)| {
1716 (item.item_id() == last_activated_item.entity_id).then_some(index)
1717 })
1718 })
1719 // We didn't have a valid activation history entry, so fallback
1720 // to activating the item to the left
1721 .unwrap_or_else(left_neighbour_index),
1722 ActivateOnClose::Neighbour => {
1723 self.activation_history.pop();
1724 if item_index + 1 < self.items.len() {
1725 item_index + 1
1726 } else {
1727 item_index.saturating_sub(1)
1728 }
1729 }
1730 ActivateOnClose::LeftNeighbour => {
1731 self.activation_history.pop();
1732 left_neighbour_index()
1733 }
1734 };
1735
1736 let should_activate = activate_pane || self.has_focus(window, cx);
1737 if self.items.len() == 1 && should_activate {
1738 self.focus_handle.focus(window);
1739 } else {
1740 self.activate_item(
1741 index_to_activate,
1742 should_activate,
1743 should_activate,
1744 window,
1745 cx,
1746 );
1747 }
1748 }
1749
1750 let item = self.items.remove(item_index);
1751
1752 cx.emit(Event::RemovedItem { item: item.clone() });
1753 if self.items.is_empty() {
1754 item.deactivated(window, cx);
1755 if close_pane_if_empty {
1756 self.update_toolbar(window, cx);
1757 cx.emit(Event::Remove {
1758 focus_on_pane: focus_on_pane_if_closed,
1759 });
1760 }
1761 }
1762
1763 if item_index < self.active_item_index {
1764 self.active_item_index -= 1;
1765 }
1766
1767 let mode = self.nav_history.mode();
1768 self.nav_history.set_mode(NavigationMode::ClosingItem);
1769 item.deactivated(window, cx);
1770 self.nav_history.set_mode(mode);
1771
1772 if self.is_active_preview_item(item.item_id()) {
1773 self.set_preview_item_id(None, cx);
1774 }
1775
1776 if let Some(path) = item.project_path(cx) {
1777 let abs_path = self
1778 .nav_history
1779 .0
1780 .lock()
1781 .paths_by_item
1782 .get(&item.item_id())
1783 .and_then(|(_, abs_path)| abs_path.clone());
1784
1785 self.nav_history
1786 .0
1787 .lock()
1788 .paths_by_item
1789 .insert(item.item_id(), (path, abs_path));
1790 } else {
1791 self.nav_history
1792 .0
1793 .lock()
1794 .paths_by_item
1795 .remove(&item.item_id());
1796 }
1797
1798 if self.zoom_out_on_close && self.items.is_empty() && close_pane_if_empty && self.zoomed {
1799 cx.emit(Event::ZoomOut);
1800 }
1801
1802 cx.notify();
1803 }
1804
1805 pub async fn save_item(
1806 project: Entity<Project>,
1807 pane: &WeakEntity<Pane>,
1808 item: &dyn ItemHandle,
1809 save_intent: SaveIntent,
1810 cx: &mut AsyncWindowContext,
1811 ) -> Result<bool> {
1812 const CONFLICT_MESSAGE: &str =
1813 "This file has changed on disk since you started editing it. Do you want to overwrite it?";
1814
1815 const DELETED_MESSAGE: &str =
1816 "This file has been deleted on disk since you started editing it. Do you want to recreate it?";
1817
1818 if save_intent == SaveIntent::Skip {
1819 return Ok(true);
1820 }
1821 let Some(item_ix) = pane
1822 .update(cx, |pane, _| pane.index_for_item(item))
1823 .ok()
1824 .flatten()
1825 else {
1826 return Ok(true);
1827 };
1828
1829 let (
1830 mut has_conflict,
1831 mut is_dirty,
1832 mut can_save,
1833 can_save_as,
1834 is_singleton,
1835 has_deleted_file,
1836 ) = cx.update(|_window, cx| {
1837 (
1838 item.has_conflict(cx),
1839 item.is_dirty(cx),
1840 item.can_save(cx),
1841 item.can_save_as(cx),
1842 item.is_singleton(cx),
1843 item.has_deleted_file(cx),
1844 )
1845 })?;
1846
1847 // when saving a single buffer, we ignore whether or not it's dirty.
1848 if save_intent == SaveIntent::Save || save_intent == SaveIntent::SaveWithoutFormat {
1849 is_dirty = true;
1850 }
1851
1852 if save_intent == SaveIntent::SaveAs {
1853 is_dirty = true;
1854 has_conflict = false;
1855 can_save = false;
1856 }
1857
1858 if save_intent == SaveIntent::Overwrite {
1859 has_conflict = false;
1860 }
1861
1862 let should_format = save_intent != SaveIntent::SaveWithoutFormat;
1863
1864 if has_conflict && can_save {
1865 if has_deleted_file && is_singleton {
1866 let answer = pane.update_in(cx, |pane, window, cx| {
1867 pane.activate_item(item_ix, true, true, window, cx);
1868 window.prompt(
1869 PromptLevel::Warning,
1870 DELETED_MESSAGE,
1871 None,
1872 &["Save", "Close", "Cancel"],
1873 cx,
1874 )
1875 })?;
1876 match answer.await {
1877 Ok(0) => {
1878 pane.update_in(cx, |_, window, cx| {
1879 item.save(should_format, project, window, cx)
1880 })?
1881 .await?
1882 }
1883 Ok(1) => {
1884 pane.update_in(cx, |pane, window, cx| {
1885 pane.remove_item(item.item_id(), false, true, window, cx)
1886 })?;
1887 }
1888 _ => return Ok(false),
1889 }
1890 return Ok(true);
1891 } else {
1892 let answer = pane.update_in(cx, |pane, window, cx| {
1893 pane.activate_item(item_ix, true, true, window, cx);
1894 window.prompt(
1895 PromptLevel::Warning,
1896 CONFLICT_MESSAGE,
1897 None,
1898 &["Overwrite", "Discard", "Cancel"],
1899 cx,
1900 )
1901 })?;
1902 match answer.await {
1903 Ok(0) => {
1904 pane.update_in(cx, |_, window, cx| {
1905 item.save(should_format, project, window, cx)
1906 })?
1907 .await?
1908 }
1909 Ok(1) => {
1910 pane.update_in(cx, |_, window, cx| item.reload(project, window, cx))?
1911 .await?
1912 }
1913 _ => return Ok(false),
1914 }
1915 }
1916 } else if is_dirty && (can_save || can_save_as) {
1917 if save_intent == SaveIntent::Close {
1918 let will_autosave = cx.update(|_window, cx| {
1919 matches!(
1920 item.workspace_settings(cx).autosave,
1921 AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange
1922 ) && Self::can_autosave_item(item, cx)
1923 })?;
1924 if !will_autosave {
1925 let item_id = item.item_id();
1926 let answer_task = pane.update_in(cx, |pane, window, cx| {
1927 if pane.save_modals_spawned.insert(item_id) {
1928 pane.activate_item(item_ix, true, true, window, cx);
1929 let prompt = dirty_message_for(item.project_path(cx));
1930 Some(window.prompt(
1931 PromptLevel::Warning,
1932 &prompt,
1933 None,
1934 &["Save", "Don't Save", "Cancel"],
1935 cx,
1936 ))
1937 } else {
1938 None
1939 }
1940 })?;
1941 if let Some(answer_task) = answer_task {
1942 let answer = answer_task.await;
1943 pane.update(cx, |pane, _| {
1944 if !pane.save_modals_spawned.remove(&item_id) {
1945 debug_panic!(
1946 "save modal was not present in spawned modals after awaiting for its answer"
1947 )
1948 }
1949 })?;
1950 match answer {
1951 Ok(0) => {}
1952 Ok(1) => {
1953 // Don't save this file
1954 pane.update_in(cx, |pane, window, cx| {
1955 if pane.is_tab_pinned(item_ix) && !item.can_save(cx) {
1956 pane.pinned_tab_count -= 1;
1957 }
1958 item.discarded(project, window, cx)
1959 })
1960 .log_err();
1961 return Ok(true);
1962 }
1963 _ => return Ok(false), // Cancel
1964 }
1965 } else {
1966 return Ok(false);
1967 }
1968 }
1969 }
1970
1971 if can_save {
1972 pane.update_in(cx, |pane, window, cx| {
1973 if pane.is_active_preview_item(item.item_id()) {
1974 pane.set_preview_item_id(None, cx);
1975 }
1976 item.save(should_format, project, window, cx)
1977 })?
1978 .await?;
1979 } else if can_save_as && is_singleton {
1980 let abs_path = pane.update_in(cx, |pane, window, cx| {
1981 pane.activate_item(item_ix, true, true, window, cx);
1982 pane.workspace.update(cx, |workspace, cx| {
1983 workspace.prompt_for_new_path(window, cx)
1984 })
1985 })??;
1986 if let Some(abs_path) = abs_path.await.ok().flatten() {
1987 pane.update_in(cx, |pane, window, cx| {
1988 if let Some(item) = pane.item_for_path(abs_path.clone(), cx) {
1989 pane.remove_item(item.item_id(), false, false, window, cx);
1990 }
1991
1992 item.save_as(project, abs_path, window, cx)
1993 })?
1994 .await?;
1995 } else {
1996 return Ok(false);
1997 }
1998 }
1999 }
2000
2001 pane.update(cx, |_, cx| {
2002 cx.emit(Event::UserSavedItem {
2003 item: item.downgrade_item(),
2004 save_intent,
2005 });
2006 true
2007 })
2008 }
2009
2010 fn can_autosave_item(item: &dyn ItemHandle, cx: &App) -> bool {
2011 let is_deleted = item.project_entry_ids(cx).is_empty();
2012 item.is_dirty(cx) && !item.has_conflict(cx) && item.can_save(cx) && !is_deleted
2013 }
2014
2015 pub fn autosave_item(
2016 item: &dyn ItemHandle,
2017 project: Entity<Project>,
2018 window: &mut Window,
2019 cx: &mut App,
2020 ) -> Task<Result<()>> {
2021 let format = !matches!(
2022 item.workspace_settings(cx).autosave,
2023 AutosaveSetting::AfterDelay { .. }
2024 );
2025 if Self::can_autosave_item(item, cx) {
2026 item.save(format, project, window, cx)
2027 } else {
2028 Task::ready(Ok(()))
2029 }
2030 }
2031
2032 pub fn focus_active_item(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2033 if let Some(active_item) = self.active_item() {
2034 let focus_handle = active_item.item_focus_handle(cx);
2035 window.focus(&focus_handle);
2036 }
2037 }
2038
2039 pub fn split(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
2040 cx.emit(Event::Split(direction));
2041 }
2042
2043 pub fn toolbar(&self) -> &Entity<Toolbar> {
2044 &self.toolbar
2045 }
2046
2047 pub fn handle_deleted_project_item(
2048 &mut self,
2049 entry_id: ProjectEntryId,
2050 window: &mut Window,
2051 cx: &mut Context<Pane>,
2052 ) -> Option<()> {
2053 let item_id = self.items().find_map(|item| {
2054 if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
2055 Some(item.item_id())
2056 } else {
2057 None
2058 }
2059 })?;
2060
2061 self.remove_item(item_id, false, true, window, cx);
2062 self.nav_history.remove_item(item_id);
2063
2064 Some(())
2065 }
2066
2067 fn update_toolbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2068 let active_item = self
2069 .items
2070 .get(self.active_item_index)
2071 .map(|item| item.as_ref());
2072 self.toolbar.update(cx, |toolbar, cx| {
2073 toolbar.set_active_item(active_item, window, cx);
2074 });
2075 }
2076
2077 fn update_status_bar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2078 let workspace = self.workspace.clone();
2079 let pane = cx.entity().clone();
2080
2081 window.defer(cx, move |window, cx| {
2082 let Ok(status_bar) = workspace.update(cx, |workspace, _| workspace.status_bar.clone())
2083 else {
2084 return;
2085 };
2086
2087 status_bar.update(cx, move |status_bar, cx| {
2088 status_bar.set_active_pane(&pane, window, cx);
2089 });
2090 });
2091 }
2092
2093 fn entry_abs_path(&self, entry: ProjectEntryId, cx: &App) -> Option<PathBuf> {
2094 let worktree = self
2095 .workspace
2096 .upgrade()?
2097 .read(cx)
2098 .project()
2099 .read(cx)
2100 .worktree_for_entry(entry, cx)?
2101 .read(cx);
2102 let entry = worktree.entry_for_id(entry)?;
2103 match &entry.canonical_path {
2104 Some(canonical_path) => Some(canonical_path.to_path_buf()),
2105 None => worktree.absolutize(&entry.path).ok(),
2106 }
2107 }
2108
2109 pub fn icon_color(selected: bool) -> Color {
2110 if selected {
2111 Color::Default
2112 } else {
2113 Color::Muted
2114 }
2115 }
2116
2117 fn toggle_pin_tab(&mut self, _: &TogglePinTab, window: &mut Window, cx: &mut Context<Self>) {
2118 if self.items.is_empty() {
2119 return;
2120 }
2121 let active_tab_ix = self.active_item_index();
2122 if self.is_tab_pinned(active_tab_ix) {
2123 self.unpin_tab_at(active_tab_ix, window, cx);
2124 } else {
2125 self.pin_tab_at(active_tab_ix, window, cx);
2126 }
2127 }
2128
2129 fn pin_tab_at(&mut self, ix: usize, window: &mut Window, cx: &mut Context<Self>) {
2130 maybe!({
2131 let pane = cx.entity().clone();
2132 let destination_index = self.pinned_tab_count.min(ix);
2133 self.pinned_tab_count += 1;
2134 let id = self.item_for_index(ix)?.item_id();
2135
2136 if self.is_active_preview_item(id) {
2137 self.set_preview_item_id(None, cx);
2138 }
2139
2140 self.workspace
2141 .update(cx, |_, cx| {
2142 cx.defer_in(window, move |_, window, cx| {
2143 move_item(&pane, &pane, id, destination_index, window, cx)
2144 });
2145 })
2146 .ok()?;
2147
2148 Some(())
2149 });
2150 }
2151
2152 fn unpin_tab_at(&mut self, ix: usize, window: &mut Window, cx: &mut Context<Self>) {
2153 maybe!({
2154 let pane = cx.entity().clone();
2155 self.pinned_tab_count = self.pinned_tab_count.checked_sub(1)?;
2156 let destination_index = self.pinned_tab_count;
2157
2158 let id = self.item_for_index(ix)?.item_id();
2159
2160 self.workspace
2161 .update(cx, |_, cx| {
2162 cx.defer_in(window, move |_, window, cx| {
2163 move_item(&pane, &pane, id, destination_index, window, cx)
2164 });
2165 })
2166 .ok()?;
2167
2168 Some(())
2169 });
2170 }
2171
2172 fn is_tab_pinned(&self, ix: usize) -> bool {
2173 self.pinned_tab_count > ix
2174 }
2175
2176 fn has_pinned_tabs(&self) -> bool {
2177 self.pinned_tab_count != 0
2178 }
2179
2180 fn has_unpinned_tabs(&self) -> bool {
2181 self.pinned_tab_count < self.items.len()
2182 }
2183
2184 fn activate_unpinned_tab(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2185 if self.items.is_empty() {
2186 return;
2187 }
2188 let Some(index) = self
2189 .items()
2190 .enumerate()
2191 .find_map(|(index, _item)| (!self.is_tab_pinned(index)).then_some(index))
2192 else {
2193 return;
2194 };
2195 self.activate_item(index, true, true, window, cx);
2196 }
2197
2198 fn render_tab(
2199 &self,
2200 ix: usize,
2201 item: &dyn ItemHandle,
2202 detail: usize,
2203 focus_handle: &FocusHandle,
2204 window: &mut Window,
2205 cx: &mut Context<Pane>,
2206 ) -> impl IntoElement {
2207 let is_active = ix == self.active_item_index;
2208 let is_preview = self
2209 .preview_item_id
2210 .map(|id| id == item.item_id())
2211 .unwrap_or(false);
2212
2213 let label = item.tab_content(
2214 TabContentParams {
2215 detail: Some(detail),
2216 selected: is_active,
2217 preview: is_preview,
2218 },
2219 window,
2220 cx,
2221 );
2222
2223 let item_diagnostic = item
2224 .project_path(cx)
2225 .map_or(None, |project_path| self.diagnostics.get(&project_path));
2226
2227 let decorated_icon = item_diagnostic.map_or(None, |diagnostic| {
2228 let icon = match item.tab_icon(window, cx) {
2229 Some(icon) => icon,
2230 None => return None,
2231 };
2232
2233 let knockout_item_color = if is_active {
2234 cx.theme().colors().tab_active_background
2235 } else {
2236 cx.theme().colors().tab_bar_background
2237 };
2238
2239 let (icon_decoration, icon_color) = if matches!(diagnostic, &DiagnosticSeverity::ERROR)
2240 {
2241 (IconDecorationKind::X, Color::Error)
2242 } else {
2243 (IconDecorationKind::Triangle, Color::Warning)
2244 };
2245
2246 Some(DecoratedIcon::new(
2247 icon.size(IconSize::Small).color(Color::Muted),
2248 Some(
2249 IconDecoration::new(icon_decoration, knockout_item_color, cx)
2250 .color(icon_color.color(cx))
2251 .position(Point {
2252 x: px(-2.),
2253 y: px(-2.),
2254 }),
2255 ),
2256 ))
2257 });
2258
2259 let icon = if decorated_icon.is_none() {
2260 match item_diagnostic {
2261 Some(&DiagnosticSeverity::ERROR) => None,
2262 Some(&DiagnosticSeverity::WARNING) => None,
2263 _ => item
2264 .tab_icon(window, cx)
2265 .map(|icon| icon.color(Color::Muted)),
2266 }
2267 .map(|icon| icon.size(IconSize::Small))
2268 } else {
2269 None
2270 };
2271
2272 let settings = ItemSettings::get_global(cx);
2273 let close_side = &settings.close_position;
2274 let show_close_button = &settings.show_close_button;
2275 let indicator = render_item_indicator(item.boxed_clone(), cx);
2276 let item_id = item.item_id();
2277 let is_first_item = ix == 0;
2278 let is_last_item = ix == self.items.len() - 1;
2279 let is_pinned = self.is_tab_pinned(ix);
2280 let position_relative_to_active_item = ix.cmp(&self.active_item_index);
2281
2282 let tab = Tab::new(ix)
2283 .position(if is_first_item {
2284 TabPosition::First
2285 } else if is_last_item {
2286 TabPosition::Last
2287 } else {
2288 TabPosition::Middle(position_relative_to_active_item)
2289 })
2290 .close_side(match close_side {
2291 ClosePosition::Left => ui::TabCloseSide::Start,
2292 ClosePosition::Right => ui::TabCloseSide::End,
2293 })
2294 .toggle_state(is_active)
2295 .on_click(cx.listener(move |pane: &mut Self, _, window, cx| {
2296 pane.activate_item(ix, true, true, window, cx)
2297 }))
2298 // TODO: This should be a click listener with the middle mouse button instead of a mouse down listener.
2299 .on_mouse_down(
2300 MouseButton::Middle,
2301 cx.listener(move |pane, _event, window, cx| {
2302 pane.close_item_by_id(item_id, SaveIntent::Close, window, cx)
2303 .detach_and_log_err(cx);
2304 }),
2305 )
2306 .on_mouse_down(
2307 MouseButton::Left,
2308 cx.listener(move |pane, event: &MouseDownEvent, _, cx| {
2309 if let Some(id) = pane.preview_item_id {
2310 if id == item_id && event.click_count > 1 {
2311 pane.set_preview_item_id(None, cx);
2312 }
2313 }
2314 }),
2315 )
2316 .on_drag(
2317 DraggedTab {
2318 item: item.boxed_clone(),
2319 pane: cx.entity().clone(),
2320 detail,
2321 is_active,
2322 ix,
2323 },
2324 |tab, _, _, cx| cx.new(|_| tab.clone()),
2325 )
2326 .drag_over::<DraggedTab>(|tab, _, _, cx| {
2327 tab.bg(cx.theme().colors().drop_target_background)
2328 })
2329 .drag_over::<DraggedSelection>(|tab, _, _, cx| {
2330 tab.bg(cx.theme().colors().drop_target_background)
2331 })
2332 .when_some(self.can_drop_predicate.clone(), |this, p| {
2333 this.can_drop(move |a, window, cx| p(a, window, cx))
2334 })
2335 .on_drop(
2336 cx.listener(move |this, dragged_tab: &DraggedTab, window, cx| {
2337 this.drag_split_direction = None;
2338 this.handle_tab_drop(dragged_tab, ix, window, cx)
2339 }),
2340 )
2341 .on_drop(
2342 cx.listener(move |this, selection: &DraggedSelection, window, cx| {
2343 this.drag_split_direction = None;
2344 this.handle_dragged_selection_drop(selection, Some(ix), window, cx)
2345 }),
2346 )
2347 .on_drop(cx.listener(move |this, paths, window, cx| {
2348 this.drag_split_direction = None;
2349 this.handle_external_paths_drop(paths, window, cx)
2350 }))
2351 .when_some(item.tab_tooltip_content(cx), |tab, content| match content {
2352 TabTooltipContent::Text(text) => tab.tooltip(Tooltip::text(text.clone())),
2353 TabTooltipContent::Custom(element_fn) => {
2354 tab.tooltip(move |window, cx| element_fn(window, cx))
2355 }
2356 })
2357 .start_slot::<Indicator>(indicator)
2358 .map(|this| {
2359 let end_slot_action: &'static dyn Action;
2360 let end_slot_tooltip_text: &'static str;
2361 let end_slot = if is_pinned {
2362 end_slot_action = &TogglePinTab;
2363 end_slot_tooltip_text = "Unpin Tab";
2364 IconButton::new("unpin tab", IconName::Pin)
2365 .shape(IconButtonShape::Square)
2366 .icon_color(Color::Muted)
2367 .size(ButtonSize::None)
2368 .icon_size(IconSize::XSmall)
2369 .on_click(cx.listener(move |pane, _, window, cx| {
2370 pane.unpin_tab_at(ix, window, cx);
2371 }))
2372 } else {
2373 end_slot_action = &CloseActiveItem {
2374 save_intent: None,
2375 close_pinned: false,
2376 };
2377 end_slot_tooltip_text = "Close Tab";
2378 match show_close_button {
2379 ShowCloseButton::Always => IconButton::new("close tab", IconName::Close),
2380 ShowCloseButton::Hover => {
2381 IconButton::new("close tab", IconName::Close).visible_on_hover("")
2382 }
2383 ShowCloseButton::Hidden => return this,
2384 }
2385 .shape(IconButtonShape::Square)
2386 .icon_color(Color::Muted)
2387 .size(ButtonSize::None)
2388 .icon_size(IconSize::XSmall)
2389 .on_click(cx.listener(move |pane, _, window, cx| {
2390 pane.close_item_by_id(item_id, SaveIntent::Close, window, cx)
2391 .detach_and_log_err(cx);
2392 }))
2393 }
2394 .map(|this| {
2395 if is_active {
2396 let focus_handle = focus_handle.clone();
2397 this.tooltip(move |window, cx| {
2398 Tooltip::for_action_in(
2399 end_slot_tooltip_text,
2400 end_slot_action,
2401 &focus_handle,
2402 window,
2403 cx,
2404 )
2405 })
2406 } else {
2407 this.tooltip(Tooltip::text(end_slot_tooltip_text))
2408 }
2409 });
2410 this.end_slot(end_slot)
2411 })
2412 .child(
2413 h_flex()
2414 .gap_1()
2415 .items_center()
2416 .children(
2417 std::iter::once(if let Some(decorated_icon) = decorated_icon {
2418 Some(div().child(decorated_icon.into_any_element()))
2419 } else if let Some(icon) = icon {
2420 Some(div().child(icon.into_any_element()))
2421 } else {
2422 None
2423 })
2424 .flatten(),
2425 )
2426 .child(label),
2427 );
2428
2429 let single_entry_to_resolve = self.items[ix]
2430 .is_singleton(cx)
2431 .then(|| self.items[ix].project_entry_ids(cx).get(0).copied())
2432 .flatten();
2433
2434 let total_items = self.items.len();
2435 let has_items_to_left = ix > 0;
2436 let has_items_to_right = ix < total_items - 1;
2437 let is_pinned = self.is_tab_pinned(ix);
2438 let pane = cx.entity().downgrade();
2439 let menu_context = item.item_focus_handle(cx);
2440 right_click_menu(ix).trigger(tab).menu(move |window, cx| {
2441 let pane = pane.clone();
2442 let menu_context = menu_context.clone();
2443 ContextMenu::build(window, cx, move |mut menu, window, cx| {
2444 if let Some(pane) = pane.upgrade() {
2445 menu = menu
2446 .entry(
2447 "Close",
2448 Some(Box::new(CloseActiveItem {
2449 save_intent: None,
2450 close_pinned: true,
2451 })),
2452 window.handler_for(&pane, move |pane, window, cx| {
2453 pane.close_item_by_id(item_id, SaveIntent::Close, window, cx)
2454 .detach_and_log_err(cx);
2455 }),
2456 )
2457 .item(ContextMenuItem::Entry(
2458 ContextMenuEntry::new("Close Others")
2459 .action(Box::new(CloseInactiveItems {
2460 save_intent: None,
2461 close_pinned: false,
2462 }))
2463 .disabled(total_items == 1)
2464 .handler(window.handler_for(&pane, move |pane, window, cx| {
2465 pane.close_items(window, cx, SaveIntent::Close, |id| {
2466 id != item_id
2467 })
2468 .detach_and_log_err(cx);
2469 })),
2470 ))
2471 .separator()
2472 .item(ContextMenuItem::Entry(
2473 ContextMenuEntry::new("Close Left")
2474 .action(Box::new(CloseItemsToTheLeft {
2475 close_pinned: false,
2476 }))
2477 .disabled(!has_items_to_left)
2478 .handler(window.handler_for(&pane, move |pane, window, cx| {
2479 pane.close_items_to_the_left_by_id(
2480 item_id,
2481 &CloseItemsToTheLeft {
2482 close_pinned: false,
2483 },
2484 pane.get_non_closeable_item_ids(false),
2485 window,
2486 cx,
2487 )
2488 .detach_and_log_err(cx);
2489 })),
2490 ))
2491 .item(ContextMenuItem::Entry(
2492 ContextMenuEntry::new("Close Right")
2493 .action(Box::new(CloseItemsToTheRight {
2494 close_pinned: false,
2495 }))
2496 .disabled(!has_items_to_right)
2497 .handler(window.handler_for(&pane, move |pane, window, cx| {
2498 pane.close_items_to_the_right_by_id(
2499 item_id,
2500 &CloseItemsToTheRight {
2501 close_pinned: false,
2502 },
2503 pane.get_non_closeable_item_ids(false),
2504 window,
2505 cx,
2506 )
2507 .detach_and_log_err(cx);
2508 })),
2509 ))
2510 .separator()
2511 .entry(
2512 "Close Clean",
2513 Some(Box::new(CloseCleanItems {
2514 close_pinned: false,
2515 })),
2516 window.handler_for(&pane, move |pane, window, cx| {
2517 if let Some(task) = pane.close_clean_items(
2518 &CloseCleanItems {
2519 close_pinned: false,
2520 },
2521 window,
2522 cx,
2523 ) {
2524 task.detach_and_log_err(cx)
2525 }
2526 }),
2527 )
2528 .entry(
2529 "Close All",
2530 Some(Box::new(CloseAllItems {
2531 save_intent: None,
2532 close_pinned: false,
2533 })),
2534 window.handler_for(&pane, |pane, window, cx| {
2535 if let Some(task) = pane.close_all_items(
2536 &CloseAllItems {
2537 save_intent: None,
2538 close_pinned: false,
2539 },
2540 window,
2541 cx,
2542 ) {
2543 task.detach_and_log_err(cx)
2544 }
2545 }),
2546 );
2547
2548 let pin_tab_entries = |menu: ContextMenu| {
2549 menu.separator().map(|this| {
2550 if is_pinned {
2551 this.entry(
2552 "Unpin Tab",
2553 Some(TogglePinTab.boxed_clone()),
2554 window.handler_for(&pane, move |pane, window, cx| {
2555 pane.unpin_tab_at(ix, window, cx);
2556 }),
2557 )
2558 } else {
2559 this.entry(
2560 "Pin Tab",
2561 Some(TogglePinTab.boxed_clone()),
2562 window.handler_for(&pane, move |pane, window, cx| {
2563 pane.pin_tab_at(ix, window, cx);
2564 }),
2565 )
2566 }
2567 })
2568 };
2569 if let Some(entry) = single_entry_to_resolve {
2570 let project_path = pane
2571 .read(cx)
2572 .item_for_entry(entry, cx)
2573 .and_then(|item| item.project_path(cx));
2574 let worktree = project_path.as_ref().and_then(|project_path| {
2575 pane.read(cx)
2576 .project
2577 .upgrade()?
2578 .read(cx)
2579 .worktree_for_id(project_path.worktree_id, cx)
2580 });
2581 let has_relative_path = worktree.as_ref().is_some_and(|worktree| {
2582 worktree
2583 .read(cx)
2584 .root_entry()
2585 .map_or(false, |entry| entry.is_dir())
2586 });
2587
2588 let entry_abs_path = pane.read(cx).entry_abs_path(entry, cx);
2589 let parent_abs_path = entry_abs_path
2590 .as_deref()
2591 .and_then(|abs_path| Some(abs_path.parent()?.to_path_buf()));
2592 let relative_path = project_path
2593 .map(|project_path| project_path.path)
2594 .filter(|_| has_relative_path);
2595
2596 let visible_in_project_panel = relative_path.is_some()
2597 && worktree.is_some_and(|worktree| worktree.read(cx).is_visible());
2598
2599 let entry_id = entry.to_proto();
2600 menu = menu
2601 .separator()
2602 .when_some(entry_abs_path, |menu, abs_path| {
2603 menu.entry(
2604 "Copy Path",
2605 Some(Box::new(zed_actions::workspace::CopyPath)),
2606 window.handler_for(&pane, move |_, _, cx| {
2607 cx.write_to_clipboard(ClipboardItem::new_string(
2608 abs_path.to_string_lossy().to_string(),
2609 ));
2610 }),
2611 )
2612 })
2613 .when_some(relative_path, |menu, relative_path| {
2614 menu.entry(
2615 "Copy Relative Path",
2616 Some(Box::new(zed_actions::workspace::CopyRelativePath)),
2617 window.handler_for(&pane, move |_, _, cx| {
2618 cx.write_to_clipboard(ClipboardItem::new_string(
2619 relative_path.to_string_lossy().to_string(),
2620 ));
2621 }),
2622 )
2623 })
2624 .map(pin_tab_entries)
2625 .separator()
2626 .when(visible_in_project_panel, |menu| {
2627 menu.entry(
2628 "Reveal In Project Panel",
2629 Some(Box::new(RevealInProjectPanel {
2630 entry_id: Some(entry_id),
2631 })),
2632 window.handler_for(&pane, move |pane, _, cx| {
2633 pane.project
2634 .update(cx, |_, cx| {
2635 cx.emit(project::Event::RevealInProjectPanel(
2636 ProjectEntryId::from_proto(entry_id),
2637 ))
2638 })
2639 .ok();
2640 }),
2641 )
2642 })
2643 .when_some(parent_abs_path, |menu, parent_abs_path| {
2644 menu.entry(
2645 "Open in Terminal",
2646 Some(Box::new(OpenInTerminal)),
2647 window.handler_for(&pane, move |_, window, cx| {
2648 window.dispatch_action(
2649 OpenTerminal {
2650 working_directory: parent_abs_path.clone(),
2651 }
2652 .boxed_clone(),
2653 cx,
2654 );
2655 }),
2656 )
2657 });
2658 } else {
2659 menu = menu.map(pin_tab_entries);
2660 }
2661 }
2662
2663 menu.context(menu_context)
2664 })
2665 })
2666 }
2667
2668 fn render_tab_bar(&mut self, window: &mut Window, cx: &mut Context<Pane>) -> impl IntoElement {
2669 let focus_handle = self.focus_handle.clone();
2670 let navigate_backward = IconButton::new("navigate_backward", IconName::ArrowLeft)
2671 .icon_size(IconSize::Small)
2672 .on_click({
2673 let entity = cx.entity().clone();
2674 move |_, window, cx| {
2675 entity.update(cx, |pane, cx| pane.navigate_backward(window, cx))
2676 }
2677 })
2678 .disabled(!self.can_navigate_backward())
2679 .tooltip({
2680 let focus_handle = focus_handle.clone();
2681 move |window, cx| {
2682 Tooltip::for_action_in("Go Back", &GoBack, &focus_handle, window, cx)
2683 }
2684 });
2685
2686 let navigate_forward = IconButton::new("navigate_forward", IconName::ArrowRight)
2687 .icon_size(IconSize::Small)
2688 .on_click({
2689 let entity = cx.entity().clone();
2690 move |_, window, cx| entity.update(cx, |pane, cx| pane.navigate_forward(window, cx))
2691 })
2692 .disabled(!self.can_navigate_forward())
2693 .tooltip({
2694 let focus_handle = focus_handle.clone();
2695 move |window, cx| {
2696 Tooltip::for_action_in("Go Forward", &GoForward, &focus_handle, window, cx)
2697 }
2698 });
2699
2700 let mut tab_items = self
2701 .items
2702 .iter()
2703 .enumerate()
2704 .zip(tab_details(&self.items, cx))
2705 .map(|((ix, item), detail)| {
2706 self.render_tab(ix, &**item, detail, &focus_handle, window, cx)
2707 })
2708 .collect::<Vec<_>>();
2709 let tab_count = tab_items.len();
2710 let unpinned_tabs = tab_items.split_off(self.pinned_tab_count);
2711 let pinned_tabs = tab_items;
2712 TabBar::new("tab_bar")
2713 .when(
2714 self.display_nav_history_buttons.unwrap_or_default(),
2715 |tab_bar| {
2716 tab_bar
2717 .start_child(navigate_backward)
2718 .start_child(navigate_forward)
2719 },
2720 )
2721 .map(|tab_bar| {
2722 if self.show_tab_bar_buttons {
2723 let render_tab_buttons = self.render_tab_bar_buttons.clone();
2724 let (left_children, right_children) = render_tab_buttons(self, window, cx);
2725 tab_bar
2726 .start_children(left_children)
2727 .end_children(right_children)
2728 } else {
2729 tab_bar
2730 }
2731 })
2732 .children(pinned_tabs.len().ne(&0).then(|| {
2733 h_flex()
2734 .children(pinned_tabs)
2735 .border_r_2()
2736 .border_color(cx.theme().colors().border)
2737 }))
2738 .child(
2739 h_flex()
2740 .id("unpinned tabs")
2741 .overflow_x_scroll()
2742 .w_full()
2743 .track_scroll(&self.tab_bar_scroll_handle)
2744 .children(unpinned_tabs)
2745 .child(
2746 div()
2747 .id("tab_bar_drop_target")
2748 .min_w_6()
2749 // HACK: This empty child is currently necessary to force the drop target to appear
2750 // despite us setting a min width above.
2751 .child("")
2752 .h_full()
2753 .flex_grow()
2754 .drag_over::<DraggedTab>(|bar, _, _, cx| {
2755 bar.bg(cx.theme().colors().drop_target_background)
2756 })
2757 .drag_over::<DraggedSelection>(|bar, _, _, cx| {
2758 bar.bg(cx.theme().colors().drop_target_background)
2759 })
2760 .on_drop(cx.listener(
2761 move |this, dragged_tab: &DraggedTab, window, cx| {
2762 this.drag_split_direction = None;
2763 this.handle_tab_drop(dragged_tab, this.items.len(), window, cx)
2764 },
2765 ))
2766 .on_drop(cx.listener(
2767 move |this, selection: &DraggedSelection, window, cx| {
2768 this.drag_split_direction = None;
2769 this.handle_project_entry_drop(
2770 &selection.active_selection.entry_id,
2771 Some(tab_count),
2772 window,
2773 cx,
2774 )
2775 },
2776 ))
2777 .on_drop(cx.listener(move |this, paths, window, cx| {
2778 this.drag_split_direction = None;
2779 this.handle_external_paths_drop(paths, window, cx)
2780 }))
2781 .on_click(cx.listener(move |this, event: &ClickEvent, window, cx| {
2782 if event.up.click_count == 2 {
2783 window.dispatch_action(
2784 this.double_click_dispatch_action.boxed_clone(),
2785 cx,
2786 );
2787 }
2788 })),
2789 ),
2790 )
2791 }
2792
2793 pub fn render_menu_overlay(menu: &Entity<ContextMenu>) -> Div {
2794 div().absolute().bottom_0().right_0().size_0().child(
2795 deferred(anchored().anchor(Corner::TopRight).child(menu.clone())).with_priority(1),
2796 )
2797 }
2798
2799 pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut Context<Self>) {
2800 self.zoomed = zoomed;
2801 cx.notify();
2802 }
2803
2804 pub fn is_zoomed(&self) -> bool {
2805 self.zoomed
2806 }
2807
2808 fn handle_drag_move<T: 'static>(
2809 &mut self,
2810 event: &DragMoveEvent<T>,
2811 window: &mut Window,
2812 cx: &mut Context<Self>,
2813 ) {
2814 let can_split_predicate = self.can_split_predicate.take();
2815 let can_split = match &can_split_predicate {
2816 Some(can_split_predicate) => {
2817 can_split_predicate(self, event.dragged_item(), window, cx)
2818 }
2819 None => false,
2820 };
2821 self.can_split_predicate = can_split_predicate;
2822 if !can_split {
2823 return;
2824 }
2825
2826 let rect = event.bounds.size;
2827
2828 let size = event.bounds.size.width.min(event.bounds.size.height)
2829 * WorkspaceSettings::get_global(cx).drop_target_size;
2830
2831 let relative_cursor = Point::new(
2832 event.event.position.x - event.bounds.left(),
2833 event.event.position.y - event.bounds.top(),
2834 );
2835
2836 let direction = if relative_cursor.x < size
2837 || relative_cursor.x > rect.width - size
2838 || relative_cursor.y < size
2839 || relative_cursor.y > rect.height - size
2840 {
2841 [
2842 SplitDirection::Up,
2843 SplitDirection::Right,
2844 SplitDirection::Down,
2845 SplitDirection::Left,
2846 ]
2847 .iter()
2848 .min_by_key(|side| match side {
2849 SplitDirection::Up => relative_cursor.y,
2850 SplitDirection::Right => rect.width - relative_cursor.x,
2851 SplitDirection::Down => rect.height - relative_cursor.y,
2852 SplitDirection::Left => relative_cursor.x,
2853 })
2854 .cloned()
2855 } else {
2856 None
2857 };
2858
2859 if direction != self.drag_split_direction {
2860 self.drag_split_direction = direction;
2861 }
2862 }
2863
2864 fn handle_tab_drop(
2865 &mut self,
2866 dragged_tab: &DraggedTab,
2867 ix: usize,
2868 window: &mut Window,
2869 cx: &mut Context<Self>,
2870 ) {
2871 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2872 if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_tab, window, cx) {
2873 return;
2874 }
2875 }
2876 let mut to_pane = cx.entity().clone();
2877 let split_direction = self.drag_split_direction;
2878 let item_id = dragged_tab.item.item_id();
2879 if let Some(preview_item_id) = self.preview_item_id {
2880 if item_id == preview_item_id {
2881 self.set_preview_item_id(None, cx);
2882 }
2883 }
2884
2885 let from_pane = dragged_tab.pane.clone();
2886 self.workspace
2887 .update(cx, |_, cx| {
2888 cx.defer_in(window, move |workspace, window, cx| {
2889 if let Some(split_direction) = split_direction {
2890 to_pane = workspace.split_pane(to_pane, split_direction, window, cx);
2891 }
2892 let old_ix = from_pane.read(cx).index_for_item_id(item_id);
2893 let old_len = to_pane.read(cx).items.len();
2894 move_item(&from_pane, &to_pane, item_id, ix, window, cx);
2895 if to_pane == from_pane {
2896 if let Some(old_index) = old_ix {
2897 to_pane.update(cx, |this, _| {
2898 if old_index < this.pinned_tab_count
2899 && (ix == this.items.len() || ix > this.pinned_tab_count)
2900 {
2901 this.pinned_tab_count -= 1;
2902 } else if this.has_pinned_tabs()
2903 && old_index >= this.pinned_tab_count
2904 && ix < this.pinned_tab_count
2905 {
2906 this.pinned_tab_count += 1;
2907 }
2908 });
2909 }
2910 } else {
2911 to_pane.update(cx, |this, _| {
2912 if this.items.len() > old_len // Did we not deduplicate on drag?
2913 && this.has_pinned_tabs()
2914 && ix < this.pinned_tab_count
2915 {
2916 this.pinned_tab_count += 1;
2917 }
2918 });
2919 from_pane.update(cx, |this, _| {
2920 if let Some(index) = old_ix {
2921 if this.pinned_tab_count > index {
2922 this.pinned_tab_count -= 1;
2923 }
2924 }
2925 })
2926 }
2927 });
2928 })
2929 .log_err();
2930 }
2931
2932 fn handle_dragged_selection_drop(
2933 &mut self,
2934 dragged_selection: &DraggedSelection,
2935 dragged_onto: Option<usize>,
2936 window: &mut Window,
2937 cx: &mut Context<Self>,
2938 ) {
2939 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2940 if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_selection, window, cx)
2941 {
2942 return;
2943 }
2944 }
2945 self.handle_project_entry_drop(
2946 &dragged_selection.active_selection.entry_id,
2947 dragged_onto,
2948 window,
2949 cx,
2950 );
2951 }
2952
2953 fn handle_project_entry_drop(
2954 &mut self,
2955 project_entry_id: &ProjectEntryId,
2956 target: Option<usize>,
2957 window: &mut Window,
2958 cx: &mut Context<Self>,
2959 ) {
2960 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2961 if let ControlFlow::Break(()) = custom_drop_handle(self, project_entry_id, window, cx) {
2962 return;
2963 }
2964 }
2965 let mut to_pane = cx.entity().clone();
2966 let split_direction = self.drag_split_direction;
2967 let project_entry_id = *project_entry_id;
2968 self.workspace
2969 .update(cx, |_, cx| {
2970 cx.defer_in(window, move |workspace, window, cx| {
2971 if let Some(path) = workspace
2972 .project()
2973 .read(cx)
2974 .path_for_entry(project_entry_id, cx)
2975 {
2976 let load_path_task = workspace.load_path(path, window, cx);
2977 cx.spawn_in(window, async move |workspace, cx| {
2978 if let Some((project_entry_id, build_item)) =
2979 load_path_task.await.notify_async_err(cx)
2980 {
2981 let (to_pane, new_item_handle) = workspace
2982 .update_in(cx, |workspace, window, cx| {
2983 if let Some(split_direction) = split_direction {
2984 to_pane = workspace.split_pane(
2985 to_pane,
2986 split_direction,
2987 window,
2988 cx,
2989 );
2990 }
2991 let new_item_handle = to_pane.update(cx, |pane, cx| {
2992 pane.open_item(
2993 project_entry_id,
2994 true,
2995 false,
2996 true,
2997 target,
2998 window,
2999 cx,
3000 build_item,
3001 )
3002 });
3003 (to_pane, new_item_handle)
3004 })
3005 .log_err()?;
3006 to_pane
3007 .update_in(cx, |this, window, cx| {
3008 let Some(index) = this.index_for_item(&*new_item_handle)
3009 else {
3010 return;
3011 };
3012
3013 if target.map_or(false, |target| this.is_tab_pinned(target))
3014 {
3015 this.pin_tab_at(index, window, cx);
3016 }
3017 })
3018 .ok()?
3019 }
3020 Some(())
3021 })
3022 .detach();
3023 };
3024 });
3025 })
3026 .log_err();
3027 }
3028
3029 fn handle_external_paths_drop(
3030 &mut self,
3031 paths: &ExternalPaths,
3032 window: &mut Window,
3033 cx: &mut Context<Self>,
3034 ) {
3035 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
3036 if let ControlFlow::Break(()) = custom_drop_handle(self, paths, window, cx) {
3037 return;
3038 }
3039 }
3040 let mut to_pane = cx.entity().clone();
3041 let mut split_direction = self.drag_split_direction;
3042 let paths = paths.paths().to_vec();
3043 let is_remote = self
3044 .workspace
3045 .update(cx, |workspace, cx| {
3046 if workspace.project().read(cx).is_via_collab() {
3047 workspace.show_error(
3048 &anyhow::anyhow!("Cannot drop files on a remote project"),
3049 cx,
3050 );
3051 true
3052 } else {
3053 false
3054 }
3055 })
3056 .unwrap_or(true);
3057 if is_remote {
3058 return;
3059 }
3060
3061 self.workspace
3062 .update(cx, |workspace, cx| {
3063 let fs = Arc::clone(workspace.project().read(cx).fs());
3064 cx.spawn_in(window, async move |workspace, cx| {
3065 let mut is_file_checks = FuturesUnordered::new();
3066 for path in &paths {
3067 is_file_checks.push(fs.is_file(path))
3068 }
3069 let mut has_files_to_open = false;
3070 while let Some(is_file) = is_file_checks.next().await {
3071 if is_file {
3072 has_files_to_open = true;
3073 break;
3074 }
3075 }
3076 drop(is_file_checks);
3077 if !has_files_to_open {
3078 split_direction = None;
3079 }
3080
3081 if let Ok(open_task) = workspace.update_in(cx, |workspace, window, cx| {
3082 if let Some(split_direction) = split_direction {
3083 to_pane = workspace.split_pane(to_pane, split_direction, window, cx);
3084 }
3085 workspace.open_paths(
3086 paths,
3087 OpenOptions {
3088 visible: Some(OpenVisible::OnlyDirectories),
3089 ..Default::default()
3090 },
3091 Some(to_pane.downgrade()),
3092 window,
3093 cx,
3094 )
3095 }) {
3096 let opened_items: Vec<_> = open_task.await;
3097 _ = workspace.update(cx, |workspace, cx| {
3098 for item in opened_items.into_iter().flatten() {
3099 if let Err(e) = item {
3100 workspace.show_error(&e, cx);
3101 }
3102 }
3103 });
3104 }
3105 })
3106 .detach();
3107 })
3108 .log_err();
3109 }
3110
3111 pub fn display_nav_history_buttons(&mut self, display: Option<bool>) {
3112 self.display_nav_history_buttons = display;
3113 }
3114
3115 fn get_non_closeable_item_ids(&self, close_pinned: bool) -> Vec<EntityId> {
3116 if close_pinned {
3117 return vec![];
3118 }
3119
3120 self.items
3121 .iter()
3122 .enumerate()
3123 .filter(|(index, _item)| self.is_tab_pinned(*index))
3124 .map(|(_, item)| item.item_id())
3125 .collect()
3126 }
3127
3128 pub fn drag_split_direction(&self) -> Option<SplitDirection> {
3129 self.drag_split_direction
3130 }
3131
3132 pub fn set_zoom_out_on_close(&mut self, zoom_out_on_close: bool) {
3133 self.zoom_out_on_close = zoom_out_on_close;
3134 }
3135}
3136
3137impl Focusable for Pane {
3138 fn focus_handle(&self, _cx: &App) -> FocusHandle {
3139 self.focus_handle.clone()
3140 }
3141}
3142
3143impl Render for Pane {
3144 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3145 let mut key_context = KeyContext::new_with_defaults();
3146 key_context.add("Pane");
3147 if self.active_item().is_none() {
3148 key_context.add("EmptyPane");
3149 }
3150
3151 let should_display_tab_bar = self.should_display_tab_bar.clone();
3152 let display_tab_bar = should_display_tab_bar(window, cx);
3153 let Some(project) = self.project.upgrade() else {
3154 return div().track_focus(&self.focus_handle(cx));
3155 };
3156 let is_local = project.read(cx).is_local();
3157
3158 v_flex()
3159 .key_context(key_context)
3160 .track_focus(&self.focus_handle(cx))
3161 .size_full()
3162 .flex_none()
3163 .overflow_hidden()
3164 .on_action(cx.listener(|pane, _: &AlternateFile, window, cx| {
3165 pane.alternate_file(window, cx);
3166 }))
3167 .on_action(
3168 cx.listener(|pane, _: &SplitLeft, _, cx| pane.split(SplitDirection::Left, cx)),
3169 )
3170 .on_action(cx.listener(|pane, _: &SplitUp, _, cx| pane.split(SplitDirection::Up, cx)))
3171 .on_action(cx.listener(|pane, _: &SplitHorizontal, _, cx| {
3172 pane.split(SplitDirection::horizontal(cx), cx)
3173 }))
3174 .on_action(cx.listener(|pane, _: &SplitVertical, _, cx| {
3175 pane.split(SplitDirection::vertical(cx), cx)
3176 }))
3177 .on_action(
3178 cx.listener(|pane, _: &SplitRight, _, cx| pane.split(SplitDirection::Right, cx)),
3179 )
3180 .on_action(
3181 cx.listener(|pane, _: &SplitDown, _, cx| pane.split(SplitDirection::Down, cx)),
3182 )
3183 .on_action(
3184 cx.listener(|pane, _: &GoBack, window, cx| pane.navigate_backward(window, cx)),
3185 )
3186 .on_action(
3187 cx.listener(|pane, _: &GoForward, window, cx| pane.navigate_forward(window, cx)),
3188 )
3189 .on_action(cx.listener(|_, _: &JoinIntoNext, _, cx| {
3190 cx.emit(Event::JoinIntoNext);
3191 }))
3192 .on_action(cx.listener(|_, _: &JoinAll, _, cx| {
3193 cx.emit(Event::JoinAll);
3194 }))
3195 .on_action(cx.listener(Pane::toggle_zoom))
3196 .on_action(
3197 cx.listener(|pane: &mut Pane, action: &ActivateItem, window, cx| {
3198 pane.activate_item(action.0, true, true, window, cx);
3199 }),
3200 )
3201 .on_action(
3202 cx.listener(|pane: &mut Pane, _: &ActivateLastItem, window, cx| {
3203 pane.activate_item(pane.items.len() - 1, true, true, window, cx);
3204 }),
3205 )
3206 .on_action(
3207 cx.listener(|pane: &mut Pane, _: &ActivatePreviousItem, window, cx| {
3208 pane.activate_prev_item(true, window, cx);
3209 }),
3210 )
3211 .on_action(
3212 cx.listener(|pane: &mut Pane, _: &ActivateNextItem, window, cx| {
3213 pane.activate_next_item(true, window, cx);
3214 }),
3215 )
3216 .on_action(
3217 cx.listener(|pane, _: &SwapItemLeft, window, cx| pane.swap_item_left(window, cx)),
3218 )
3219 .on_action(
3220 cx.listener(|pane, _: &SwapItemRight, window, cx| pane.swap_item_right(window, cx)),
3221 )
3222 .on_action(cx.listener(|pane, action, window, cx| {
3223 pane.toggle_pin_tab(action, window, cx);
3224 }))
3225 .when(PreviewTabsSettings::get_global(cx).enabled, |this| {
3226 this.on_action(cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, _, cx| {
3227 if let Some(active_item_id) = pane.active_item().map(|i| i.item_id()) {
3228 if pane.is_active_preview_item(active_item_id) {
3229 pane.set_preview_item_id(None, cx);
3230 } else {
3231 pane.set_preview_item_id(Some(active_item_id), cx);
3232 }
3233 }
3234 }))
3235 })
3236 .on_action(
3237 cx.listener(|pane: &mut Self, action: &CloseActiveItem, window, cx| {
3238 if let Some(task) = pane.close_active_item(action, window, cx) {
3239 task.detach_and_log_err(cx)
3240 }
3241 }),
3242 )
3243 .on_action(
3244 cx.listener(|pane: &mut Self, action: &CloseInactiveItems, window, cx| {
3245 if let Some(task) = pane.close_inactive_items(action, window, cx) {
3246 task.detach_and_log_err(cx)
3247 }
3248 }),
3249 )
3250 .on_action(
3251 cx.listener(|pane: &mut Self, action: &CloseCleanItems, window, cx| {
3252 if let Some(task) = pane.close_clean_items(action, window, cx) {
3253 task.detach_and_log_err(cx)
3254 }
3255 }),
3256 )
3257 .on_action(cx.listener(
3258 |pane: &mut Self, action: &CloseItemsToTheLeft, window, cx| {
3259 if let Some(task) = pane.close_items_to_the_left(action, window, cx) {
3260 task.detach_and_log_err(cx)
3261 }
3262 },
3263 ))
3264 .on_action(cx.listener(
3265 |pane: &mut Self, action: &CloseItemsToTheRight, window, cx| {
3266 if let Some(task) = pane.close_items_to_the_right(action, window, cx) {
3267 task.detach_and_log_err(cx)
3268 }
3269 },
3270 ))
3271 .on_action(
3272 cx.listener(|pane: &mut Self, action: &CloseAllItems, window, cx| {
3273 if let Some(task) = pane.close_all_items(action, window, cx) {
3274 task.detach_and_log_err(cx)
3275 }
3276 }),
3277 )
3278 .on_action(
3279 cx.listener(|pane: &mut Self, action: &CloseActiveItem, window, cx| {
3280 if let Some(task) = pane.close_active_item(action, window, cx) {
3281 task.detach_and_log_err(cx)
3282 }
3283 }),
3284 )
3285 .on_action(
3286 cx.listener(|pane: &mut Self, action: &RevealInProjectPanel, _, cx| {
3287 let entry_id = action
3288 .entry_id
3289 .map(ProjectEntryId::from_proto)
3290 .or_else(|| pane.active_item()?.project_entry_ids(cx).first().copied());
3291 if let Some(entry_id) = entry_id {
3292 pane.project
3293 .update(cx, |_, cx| {
3294 cx.emit(project::Event::RevealInProjectPanel(entry_id))
3295 })
3296 .ok();
3297 }
3298 }),
3299 )
3300 .when(self.active_item().is_some() && display_tab_bar, |pane| {
3301 pane.child(self.render_tab_bar(window, cx))
3302 })
3303 .child({
3304 let has_worktrees = project.read(cx).visible_worktrees(cx).next().is_some();
3305 // main content
3306 div()
3307 .flex_1()
3308 .relative()
3309 .group("")
3310 .overflow_hidden()
3311 .on_drag_move::<DraggedTab>(cx.listener(Self::handle_drag_move))
3312 .on_drag_move::<DraggedSelection>(cx.listener(Self::handle_drag_move))
3313 .when(is_local, |div| {
3314 div.on_drag_move::<ExternalPaths>(cx.listener(Self::handle_drag_move))
3315 })
3316 .map(|div| {
3317 if let Some(item) = self.active_item() {
3318 div.id("pane_placeholder")
3319 .v_flex()
3320 .size_full()
3321 .overflow_hidden()
3322 .child(self.toolbar.clone())
3323 .child(item.to_any())
3324 } else {
3325 let placeholder = div
3326 .id("pane_placeholder")
3327 .h_flex()
3328 .size_full()
3329 .justify_center()
3330 .on_click(cx.listener(
3331 move |this, event: &ClickEvent, window, cx| {
3332 if event.up.click_count == 2 {
3333 window.dispatch_action(
3334 this.double_click_dispatch_action.boxed_clone(),
3335 cx,
3336 );
3337 }
3338 },
3339 ));
3340 if has_worktrees {
3341 placeholder
3342 } else {
3343 placeholder.child(
3344 Label::new("Open a file or project to get started.")
3345 .color(Color::Muted),
3346 )
3347 }
3348 }
3349 })
3350 .child(
3351 // drag target
3352 div()
3353 .invisible()
3354 .absolute()
3355 .bg(cx.theme().colors().drop_target_background)
3356 .group_drag_over::<DraggedTab>("", |style| style.visible())
3357 .group_drag_over::<DraggedSelection>("", |style| style.visible())
3358 .when(is_local, |div| {
3359 div.group_drag_over::<ExternalPaths>("", |style| style.visible())
3360 })
3361 .when_some(self.can_drop_predicate.clone(), |this, p| {
3362 this.can_drop(move |a, window, cx| p(a, window, cx))
3363 })
3364 .on_drop(cx.listener(move |this, dragged_tab, window, cx| {
3365 this.handle_tab_drop(
3366 dragged_tab,
3367 this.active_item_index(),
3368 window,
3369 cx,
3370 )
3371 }))
3372 .on_drop(cx.listener(
3373 move |this, selection: &DraggedSelection, window, cx| {
3374 this.handle_dragged_selection_drop(selection, None, window, cx)
3375 },
3376 ))
3377 .on_drop(cx.listener(move |this, paths, window, cx| {
3378 this.handle_external_paths_drop(paths, window, cx)
3379 }))
3380 .map(|div| {
3381 let size = DefiniteLength::Fraction(0.5);
3382 match self.drag_split_direction {
3383 None => div.top_0().right_0().bottom_0().left_0(),
3384 Some(SplitDirection::Up) => {
3385 div.top_0().left_0().right_0().h(size)
3386 }
3387 Some(SplitDirection::Down) => {
3388 div.left_0().bottom_0().right_0().h(size)
3389 }
3390 Some(SplitDirection::Left) => {
3391 div.top_0().left_0().bottom_0().w(size)
3392 }
3393 Some(SplitDirection::Right) => {
3394 div.top_0().bottom_0().right_0().w(size)
3395 }
3396 }
3397 }),
3398 )
3399 })
3400 .on_mouse_down(
3401 MouseButton::Navigate(NavigationDirection::Back),
3402 cx.listener(|pane, _, window, cx| {
3403 if let Some(workspace) = pane.workspace.upgrade() {
3404 let pane = cx.entity().downgrade();
3405 window.defer(cx, move |window, cx| {
3406 workspace.update(cx, |workspace, cx| {
3407 workspace.go_back(pane, window, cx).detach_and_log_err(cx)
3408 })
3409 })
3410 }
3411 }),
3412 )
3413 .on_mouse_down(
3414 MouseButton::Navigate(NavigationDirection::Forward),
3415 cx.listener(|pane, _, window, cx| {
3416 if let Some(workspace) = pane.workspace.upgrade() {
3417 let pane = cx.entity().downgrade();
3418 window.defer(cx, move |window, cx| {
3419 workspace.update(cx, |workspace, cx| {
3420 workspace
3421 .go_forward(pane, window, cx)
3422 .detach_and_log_err(cx)
3423 })
3424 })
3425 }
3426 }),
3427 )
3428 }
3429}
3430
3431impl ItemNavHistory {
3432 pub fn push<D: 'static + Send + Any>(&mut self, data: Option<D>, cx: &mut App) {
3433 if self
3434 .item
3435 .upgrade()
3436 .is_some_and(|item| item.include_in_nav_history())
3437 {
3438 self.history
3439 .push(data, self.item.clone(), self.is_preview, cx);
3440 }
3441 }
3442
3443 pub fn pop_backward(&mut self, cx: &mut App) -> Option<NavigationEntry> {
3444 self.history.pop(NavigationMode::GoingBack, cx)
3445 }
3446
3447 pub fn pop_forward(&mut self, cx: &mut App) -> Option<NavigationEntry> {
3448 self.history.pop(NavigationMode::GoingForward, cx)
3449 }
3450}
3451
3452impl NavHistory {
3453 pub fn for_each_entry(
3454 &self,
3455 cx: &App,
3456 mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
3457 ) {
3458 let borrowed_history = self.0.lock();
3459 borrowed_history
3460 .forward_stack
3461 .iter()
3462 .chain(borrowed_history.backward_stack.iter())
3463 .chain(borrowed_history.closed_stack.iter())
3464 .for_each(|entry| {
3465 if let Some(project_and_abs_path) =
3466 borrowed_history.paths_by_item.get(&entry.item.id())
3467 {
3468 f(entry, project_and_abs_path.clone());
3469 } else if let Some(item) = entry.item.upgrade() {
3470 if let Some(path) = item.project_path(cx) {
3471 f(entry, (path, None));
3472 }
3473 }
3474 })
3475 }
3476
3477 pub fn set_mode(&mut self, mode: NavigationMode) {
3478 self.0.lock().mode = mode;
3479 }
3480
3481 pub fn mode(&self) -> NavigationMode {
3482 self.0.lock().mode
3483 }
3484
3485 pub fn disable(&mut self) {
3486 self.0.lock().mode = NavigationMode::Disabled;
3487 }
3488
3489 pub fn enable(&mut self) {
3490 self.0.lock().mode = NavigationMode::Normal;
3491 }
3492
3493 pub fn pop(&mut self, mode: NavigationMode, cx: &mut App) -> Option<NavigationEntry> {
3494 let mut state = self.0.lock();
3495 let entry = match mode {
3496 NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
3497 return None
3498 }
3499 NavigationMode::GoingBack => &mut state.backward_stack,
3500 NavigationMode::GoingForward => &mut state.forward_stack,
3501 NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
3502 }
3503 .pop_back();
3504 if entry.is_some() {
3505 state.did_update(cx);
3506 }
3507 entry
3508 }
3509
3510 pub fn push<D: 'static + Send + Any>(
3511 &mut self,
3512 data: Option<D>,
3513 item: Arc<dyn WeakItemHandle>,
3514 is_preview: bool,
3515 cx: &mut App,
3516 ) {
3517 let state = &mut *self.0.lock();
3518 match state.mode {
3519 NavigationMode::Disabled => {}
3520 NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
3521 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3522 state.backward_stack.pop_front();
3523 }
3524 state.backward_stack.push_back(NavigationEntry {
3525 item,
3526 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3527 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3528 is_preview,
3529 });
3530 state.forward_stack.clear();
3531 }
3532 NavigationMode::GoingBack => {
3533 if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3534 state.forward_stack.pop_front();
3535 }
3536 state.forward_stack.push_back(NavigationEntry {
3537 item,
3538 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3539 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3540 is_preview,
3541 });
3542 }
3543 NavigationMode::GoingForward => {
3544 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3545 state.backward_stack.pop_front();
3546 }
3547 state.backward_stack.push_back(NavigationEntry {
3548 item,
3549 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3550 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3551 is_preview,
3552 });
3553 }
3554 NavigationMode::ClosingItem => {
3555 if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3556 state.closed_stack.pop_front();
3557 }
3558 state.closed_stack.push_back(NavigationEntry {
3559 item,
3560 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3561 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3562 is_preview,
3563 });
3564 }
3565 }
3566 state.did_update(cx);
3567 }
3568
3569 pub fn remove_item(&mut self, item_id: EntityId) {
3570 let mut state = self.0.lock();
3571 state.paths_by_item.remove(&item_id);
3572 state
3573 .backward_stack
3574 .retain(|entry| entry.item.id() != item_id);
3575 state
3576 .forward_stack
3577 .retain(|entry| entry.item.id() != item_id);
3578 state
3579 .closed_stack
3580 .retain(|entry| entry.item.id() != item_id);
3581 }
3582
3583 pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option<PathBuf>)> {
3584 self.0.lock().paths_by_item.get(&item_id).cloned()
3585 }
3586}
3587
3588impl NavHistoryState {
3589 pub fn did_update(&self, cx: &mut App) {
3590 if let Some(pane) = self.pane.upgrade() {
3591 cx.defer(move |cx| {
3592 pane.update(cx, |pane, cx| pane.history_updated(cx));
3593 });
3594 }
3595 }
3596}
3597
3598fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
3599 let path = buffer_path
3600 .as_ref()
3601 .and_then(|p| {
3602 p.path
3603 .to_str()
3604 .and_then(|s| if s.is_empty() { None } else { Some(s) })
3605 })
3606 .unwrap_or("This buffer");
3607 let path = truncate_and_remove_front(path, 80);
3608 format!("{path} contains unsaved edits. Do you want to save it?")
3609}
3610
3611pub fn tab_details(items: &[Box<dyn ItemHandle>], cx: &App) -> Vec<usize> {
3612 let mut tab_details = items.iter().map(|_| 0).collect::<Vec<_>>();
3613 let mut tab_descriptions = HashMap::default();
3614 let mut done = false;
3615 while !done {
3616 done = true;
3617
3618 // Store item indices by their tab description.
3619 for (ix, (item, detail)) in items.iter().zip(&tab_details).enumerate() {
3620 if let Some(description) = item.tab_description(*detail, cx) {
3621 if *detail == 0
3622 || Some(&description) != item.tab_description(detail - 1, cx).as_ref()
3623 {
3624 tab_descriptions
3625 .entry(description)
3626 .or_insert(Vec::new())
3627 .push(ix);
3628 }
3629 }
3630 }
3631
3632 // If two or more items have the same tab description, increase their level
3633 // of detail and try again.
3634 for (_, item_ixs) in tab_descriptions.drain() {
3635 if item_ixs.len() > 1 {
3636 done = false;
3637 for ix in item_ixs {
3638 tab_details[ix] += 1;
3639 }
3640 }
3641 }
3642 }
3643
3644 tab_details
3645}
3646
3647pub fn render_item_indicator(item: Box<dyn ItemHandle>, cx: &App) -> Option<Indicator> {
3648 maybe!({
3649 let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
3650 (true, _) => Color::Warning,
3651 (_, true) => Color::Accent,
3652 (false, false) => return None,
3653 };
3654
3655 Some(Indicator::dot().color(indicator_color))
3656 })
3657}
3658
3659impl Render for DraggedTab {
3660 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3661 let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
3662 let label = self.item.tab_content(
3663 TabContentParams {
3664 detail: Some(self.detail),
3665 selected: false,
3666 preview: false,
3667 },
3668 window,
3669 cx,
3670 );
3671 Tab::new("")
3672 .toggle_state(self.is_active)
3673 .child(label)
3674 .render(window, cx)
3675 .font(ui_font)
3676 }
3677}
3678
3679#[cfg(test)]
3680mod tests {
3681 use std::num::NonZero;
3682
3683 use super::*;
3684 use crate::item::test::{TestItem, TestProjectItem};
3685 use gpui::{TestAppContext, VisualTestContext};
3686 use project::FakeFs;
3687 use settings::SettingsStore;
3688 use theme::LoadThemes;
3689
3690 #[gpui::test]
3691 async fn test_remove_active_empty(cx: &mut TestAppContext) {
3692 init_test(cx);
3693 let fs = FakeFs::new(cx.executor());
3694
3695 let project = Project::test(fs, None, cx).await;
3696 let (workspace, cx) =
3697 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3698 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3699
3700 pane.update_in(cx, |pane, window, cx| {
3701 assert!(pane
3702 .close_active_item(
3703 &CloseActiveItem {
3704 save_intent: None,
3705 close_pinned: false
3706 },
3707 window,
3708 cx
3709 )
3710 .is_none())
3711 });
3712 }
3713
3714 #[gpui::test]
3715 async fn test_add_item_capped_to_max_tabs(cx: &mut TestAppContext) {
3716 init_test(cx);
3717 let fs = FakeFs::new(cx.executor());
3718
3719 let project = Project::test(fs, None, cx).await;
3720 let (workspace, cx) =
3721 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3722 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3723
3724 for i in 0..7 {
3725 add_labeled_item(&pane, format!("{}", i).as_str(), false, cx);
3726 }
3727 set_max_tabs(cx, Some(5));
3728 add_labeled_item(&pane, "7", false, cx);
3729 // Remove items to respect the max tab cap.
3730 assert_item_labels(&pane, ["3", "4", "5", "6", "7*"], cx);
3731 pane.update_in(cx, |pane, window, cx| {
3732 pane.activate_item(0, false, false, window, cx);
3733 });
3734 add_labeled_item(&pane, "X", false, cx);
3735 // Respect activation order.
3736 assert_item_labels(&pane, ["3", "X*", "5", "6", "7"], cx);
3737
3738 for i in 0..7 {
3739 add_labeled_item(&pane, format!("D{}", i).as_str(), true, cx);
3740 }
3741 // Keeps dirty items, even over max tab cap.
3742 assert_item_labels(
3743 &pane,
3744 ["D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6*^"],
3745 cx,
3746 );
3747
3748 set_max_tabs(cx, None);
3749 for i in 0..7 {
3750 add_labeled_item(&pane, format!("N{}", i).as_str(), false, cx);
3751 }
3752 // No cap when max tabs is None.
3753 assert_item_labels(
3754 &pane,
3755 [
3756 "D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6^", "N0", "N1", "N2", "N3", "N4",
3757 "N5", "N6*",
3758 ],
3759 cx,
3760 );
3761 }
3762
3763 #[gpui::test]
3764 async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
3765 init_test(cx);
3766 let fs = FakeFs::new(cx.executor());
3767
3768 let project = Project::test(fs, None, cx).await;
3769 let (workspace, cx) =
3770 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3771 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3772
3773 // 1. Add with a destination index
3774 // a. Add before the active item
3775 set_labeled_items(&pane, ["A", "B*", "C"], cx);
3776 pane.update_in(cx, |pane, window, cx| {
3777 pane.add_item(
3778 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3779 false,
3780 false,
3781 Some(0),
3782 window,
3783 cx,
3784 );
3785 });
3786 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
3787
3788 // b. Add after the active item
3789 set_labeled_items(&pane, ["A", "B*", "C"], cx);
3790 pane.update_in(cx, |pane, window, cx| {
3791 pane.add_item(
3792 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3793 false,
3794 false,
3795 Some(2),
3796 window,
3797 cx,
3798 );
3799 });
3800 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3801
3802 // c. Add at the end of the item list (including off the length)
3803 set_labeled_items(&pane, ["A", "B*", "C"], cx);
3804 pane.update_in(cx, |pane, window, cx| {
3805 pane.add_item(
3806 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3807 false,
3808 false,
3809 Some(5),
3810 window,
3811 cx,
3812 );
3813 });
3814 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3815
3816 // 2. Add without a destination index
3817 // a. Add with active item at the start of the item list
3818 set_labeled_items(&pane, ["A*", "B", "C"], cx);
3819 pane.update_in(cx, |pane, window, cx| {
3820 pane.add_item(
3821 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3822 false,
3823 false,
3824 None,
3825 window,
3826 cx,
3827 );
3828 });
3829 set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
3830
3831 // b. Add with active item at the end of the item list
3832 set_labeled_items(&pane, ["A", "B", "C*"], cx);
3833 pane.update_in(cx, |pane, window, cx| {
3834 pane.add_item(
3835 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3836 false,
3837 false,
3838 None,
3839 window,
3840 cx,
3841 );
3842 });
3843 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3844 }
3845
3846 #[gpui::test]
3847 async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
3848 init_test(cx);
3849 let fs = FakeFs::new(cx.executor());
3850
3851 let project = Project::test(fs, None, cx).await;
3852 let (workspace, cx) =
3853 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3854 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3855
3856 // 1. Add with a destination index
3857 // 1a. Add before the active item
3858 let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3859 pane.update_in(cx, |pane, window, cx| {
3860 pane.add_item(d, false, false, Some(0), window, cx);
3861 });
3862 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
3863
3864 // 1b. Add after the active item
3865 let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3866 pane.update_in(cx, |pane, window, cx| {
3867 pane.add_item(d, false, false, Some(2), window, cx);
3868 });
3869 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3870
3871 // 1c. Add at the end of the item list (including off the length)
3872 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3873 pane.update_in(cx, |pane, window, cx| {
3874 pane.add_item(a, false, false, Some(5), window, cx);
3875 });
3876 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3877
3878 // 1d. Add same item to active index
3879 let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3880 pane.update_in(cx, |pane, window, cx| {
3881 pane.add_item(b, false, false, Some(1), window, cx);
3882 });
3883 assert_item_labels(&pane, ["A", "B*", "C"], cx);
3884
3885 // 1e. Add item to index after same item in last position
3886 let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3887 pane.update_in(cx, |pane, window, cx| {
3888 pane.add_item(c, false, false, Some(2), window, cx);
3889 });
3890 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3891
3892 // 2. Add without a destination index
3893 // 2a. Add with active item at the start of the item list
3894 let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
3895 pane.update_in(cx, |pane, window, cx| {
3896 pane.add_item(d, false, false, None, window, cx);
3897 });
3898 assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
3899
3900 // 2b. Add with active item at the end of the item list
3901 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
3902 pane.update_in(cx, |pane, window, cx| {
3903 pane.add_item(a, false, false, None, window, cx);
3904 });
3905 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3906
3907 // 2c. Add active item to active item at end of list
3908 let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
3909 pane.update_in(cx, |pane, window, cx| {
3910 pane.add_item(c, false, false, None, window, cx);
3911 });
3912 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3913
3914 // 2d. Add active item to active item at start of list
3915 let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
3916 pane.update_in(cx, |pane, window, cx| {
3917 pane.add_item(a, false, false, None, window, cx);
3918 });
3919 assert_item_labels(&pane, ["A*", "B", "C"], cx);
3920 }
3921
3922 #[gpui::test]
3923 async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
3924 init_test(cx);
3925 let fs = FakeFs::new(cx.executor());
3926
3927 let project = Project::test(fs, None, cx).await;
3928 let (workspace, cx) =
3929 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3930 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3931
3932 // singleton view
3933 pane.update_in(cx, |pane, window, cx| {
3934 pane.add_item(
3935 Box::new(cx.new(|cx| {
3936 TestItem::new(cx)
3937 .with_singleton(true)
3938 .with_label("buffer 1")
3939 .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
3940 })),
3941 false,
3942 false,
3943 None,
3944 window,
3945 cx,
3946 );
3947 });
3948 assert_item_labels(&pane, ["buffer 1*"], cx);
3949
3950 // new singleton view with the same project entry
3951 pane.update_in(cx, |pane, window, cx| {
3952 pane.add_item(
3953 Box::new(cx.new(|cx| {
3954 TestItem::new(cx)
3955 .with_singleton(true)
3956 .with_label("buffer 1")
3957 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3958 })),
3959 false,
3960 false,
3961 None,
3962 window,
3963 cx,
3964 );
3965 });
3966 assert_item_labels(&pane, ["buffer 1*"], cx);
3967
3968 // new singleton view with different project entry
3969 pane.update_in(cx, |pane, window, cx| {
3970 pane.add_item(
3971 Box::new(cx.new(|cx| {
3972 TestItem::new(cx)
3973 .with_singleton(true)
3974 .with_label("buffer 2")
3975 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
3976 })),
3977 false,
3978 false,
3979 None,
3980 window,
3981 cx,
3982 );
3983 });
3984 assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
3985
3986 // new multibuffer view with the same project entry
3987 pane.update_in(cx, |pane, window, cx| {
3988 pane.add_item(
3989 Box::new(cx.new(|cx| {
3990 TestItem::new(cx)
3991 .with_singleton(false)
3992 .with_label("multibuffer 1")
3993 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3994 })),
3995 false,
3996 false,
3997 None,
3998 window,
3999 cx,
4000 );
4001 });
4002 assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
4003
4004 // another multibuffer view with the same project entry
4005 pane.update_in(cx, |pane, window, cx| {
4006 pane.add_item(
4007 Box::new(cx.new(|cx| {
4008 TestItem::new(cx)
4009 .with_singleton(false)
4010 .with_label("multibuffer 1b")
4011 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4012 })),
4013 false,
4014 false,
4015 None,
4016 window,
4017 cx,
4018 );
4019 });
4020 assert_item_labels(
4021 &pane,
4022 ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
4023 cx,
4024 );
4025 }
4026
4027 #[gpui::test]
4028 async fn test_remove_item_ordering_history(cx: &mut TestAppContext) {
4029 init_test(cx);
4030 let fs = FakeFs::new(cx.executor());
4031
4032 let project = Project::test(fs, None, cx).await;
4033 let (workspace, cx) =
4034 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4035 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4036
4037 add_labeled_item(&pane, "A", false, cx);
4038 add_labeled_item(&pane, "B", false, cx);
4039 add_labeled_item(&pane, "C", false, cx);
4040 add_labeled_item(&pane, "D", false, cx);
4041 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4042
4043 pane.update_in(cx, |pane, window, cx| {
4044 pane.activate_item(1, false, false, window, cx)
4045 });
4046 add_labeled_item(&pane, "1", false, cx);
4047 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
4048
4049 pane.update_in(cx, |pane, window, cx| {
4050 pane.close_active_item(
4051 &CloseActiveItem {
4052 save_intent: None,
4053 close_pinned: false,
4054 },
4055 window,
4056 cx,
4057 )
4058 })
4059 .unwrap()
4060 .await
4061 .unwrap();
4062 assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
4063
4064 pane.update_in(cx, |pane, window, cx| {
4065 pane.activate_item(3, false, false, window, cx)
4066 });
4067 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4068
4069 pane.update_in(cx, |pane, window, cx| {
4070 pane.close_active_item(
4071 &CloseActiveItem {
4072 save_intent: None,
4073 close_pinned: false,
4074 },
4075 window,
4076 cx,
4077 )
4078 })
4079 .unwrap()
4080 .await
4081 .unwrap();
4082 assert_item_labels(&pane, ["A", "B*", "C"], cx);
4083
4084 pane.update_in(cx, |pane, window, cx| {
4085 pane.close_active_item(
4086 &CloseActiveItem {
4087 save_intent: None,
4088 close_pinned: false,
4089 },
4090 window,
4091 cx,
4092 )
4093 })
4094 .unwrap()
4095 .await
4096 .unwrap();
4097 assert_item_labels(&pane, ["A", "C*"], cx);
4098
4099 pane.update_in(cx, |pane, window, cx| {
4100 pane.close_active_item(
4101 &CloseActiveItem {
4102 save_intent: None,
4103 close_pinned: false,
4104 },
4105 window,
4106 cx,
4107 )
4108 })
4109 .unwrap()
4110 .await
4111 .unwrap();
4112 assert_item_labels(&pane, ["A*"], cx);
4113 }
4114
4115 #[gpui::test]
4116 async fn test_remove_item_ordering_neighbour(cx: &mut TestAppContext) {
4117 init_test(cx);
4118 cx.update_global::<SettingsStore, ()>(|s, cx| {
4119 s.update_user_settings::<ItemSettings>(cx, |s| {
4120 s.activate_on_close = Some(ActivateOnClose::Neighbour);
4121 });
4122 });
4123 let fs = FakeFs::new(cx.executor());
4124
4125 let project = Project::test(fs, None, cx).await;
4126 let (workspace, cx) =
4127 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4128 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4129
4130 add_labeled_item(&pane, "A", false, cx);
4131 add_labeled_item(&pane, "B", false, cx);
4132 add_labeled_item(&pane, "C", false, cx);
4133 add_labeled_item(&pane, "D", false, cx);
4134 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4135
4136 pane.update_in(cx, |pane, window, cx| {
4137 pane.activate_item(1, false, false, window, cx)
4138 });
4139 add_labeled_item(&pane, "1", false, cx);
4140 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
4141
4142 pane.update_in(cx, |pane, window, cx| {
4143 pane.close_active_item(
4144 &CloseActiveItem {
4145 save_intent: None,
4146 close_pinned: false,
4147 },
4148 window,
4149 cx,
4150 )
4151 })
4152 .unwrap()
4153 .await
4154 .unwrap();
4155 assert_item_labels(&pane, ["A", "B", "C*", "D"], cx);
4156
4157 pane.update_in(cx, |pane, window, cx| {
4158 pane.activate_item(3, false, false, window, cx)
4159 });
4160 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4161
4162 pane.update_in(cx, |pane, window, cx| {
4163 pane.close_active_item(
4164 &CloseActiveItem {
4165 save_intent: None,
4166 close_pinned: false,
4167 },
4168 window,
4169 cx,
4170 )
4171 })
4172 .unwrap()
4173 .await
4174 .unwrap();
4175 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4176
4177 pane.update_in(cx, |pane, window, cx| {
4178 pane.close_active_item(
4179 &CloseActiveItem {
4180 save_intent: None,
4181 close_pinned: false,
4182 },
4183 window,
4184 cx,
4185 )
4186 })
4187 .unwrap()
4188 .await
4189 .unwrap();
4190 assert_item_labels(&pane, ["A", "B*"], cx);
4191
4192 pane.update_in(cx, |pane, window, cx| {
4193 pane.close_active_item(
4194 &CloseActiveItem {
4195 save_intent: None,
4196 close_pinned: false,
4197 },
4198 window,
4199 cx,
4200 )
4201 })
4202 .unwrap()
4203 .await
4204 .unwrap();
4205 assert_item_labels(&pane, ["A*"], cx);
4206 }
4207
4208 #[gpui::test]
4209 async fn test_remove_item_ordering_left_neighbour(cx: &mut TestAppContext) {
4210 init_test(cx);
4211 cx.update_global::<SettingsStore, ()>(|s, cx| {
4212 s.update_user_settings::<ItemSettings>(cx, |s| {
4213 s.activate_on_close = Some(ActivateOnClose::LeftNeighbour);
4214 });
4215 });
4216 let fs = FakeFs::new(cx.executor());
4217
4218 let project = Project::test(fs, None, cx).await;
4219 let (workspace, cx) =
4220 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4221 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4222
4223 add_labeled_item(&pane, "A", false, cx);
4224 add_labeled_item(&pane, "B", false, cx);
4225 add_labeled_item(&pane, "C", false, cx);
4226 add_labeled_item(&pane, "D", false, cx);
4227 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4228
4229 pane.update_in(cx, |pane, window, cx| {
4230 pane.activate_item(1, false, false, window, cx)
4231 });
4232 add_labeled_item(&pane, "1", false, cx);
4233 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
4234
4235 pane.update_in(cx, |pane, window, cx| {
4236 pane.close_active_item(
4237 &CloseActiveItem {
4238 save_intent: None,
4239 close_pinned: false,
4240 },
4241 window,
4242 cx,
4243 )
4244 })
4245 .unwrap()
4246 .await
4247 .unwrap();
4248 assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
4249
4250 pane.update_in(cx, |pane, window, cx| {
4251 pane.activate_item(3, false, false, window, cx)
4252 });
4253 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4254
4255 pane.update_in(cx, |pane, window, cx| {
4256 pane.close_active_item(
4257 &CloseActiveItem {
4258 save_intent: None,
4259 close_pinned: false,
4260 },
4261 window,
4262 cx,
4263 )
4264 })
4265 .unwrap()
4266 .await
4267 .unwrap();
4268 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4269
4270 pane.update_in(cx, |pane, window, cx| {
4271 pane.activate_item(0, false, false, window, cx)
4272 });
4273 assert_item_labels(&pane, ["A*", "B", "C"], cx);
4274
4275 pane.update_in(cx, |pane, window, cx| {
4276 pane.close_active_item(
4277 &CloseActiveItem {
4278 save_intent: None,
4279 close_pinned: false,
4280 },
4281 window,
4282 cx,
4283 )
4284 })
4285 .unwrap()
4286 .await
4287 .unwrap();
4288 assert_item_labels(&pane, ["B*", "C"], cx);
4289
4290 pane.update_in(cx, |pane, window, cx| {
4291 pane.close_active_item(
4292 &CloseActiveItem {
4293 save_intent: None,
4294 close_pinned: false,
4295 },
4296 window,
4297 cx,
4298 )
4299 })
4300 .unwrap()
4301 .await
4302 .unwrap();
4303 assert_item_labels(&pane, ["C*"], cx);
4304 }
4305
4306 #[gpui::test]
4307 async fn test_close_inactive_items(cx: &mut TestAppContext) {
4308 init_test(cx);
4309 let fs = FakeFs::new(cx.executor());
4310
4311 let project = Project::test(fs, None, cx).await;
4312 let (workspace, cx) =
4313 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4314 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4315
4316 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
4317
4318 pane.update_in(cx, |pane, window, cx| {
4319 pane.close_inactive_items(
4320 &CloseInactiveItems {
4321 save_intent: None,
4322 close_pinned: false,
4323 },
4324 window,
4325 cx,
4326 )
4327 })
4328 .unwrap()
4329 .await
4330 .unwrap();
4331 assert_item_labels(&pane, ["C*"], cx);
4332 }
4333
4334 #[gpui::test]
4335 async fn test_close_clean_items(cx: &mut TestAppContext) {
4336 init_test(cx);
4337 let fs = FakeFs::new(cx.executor());
4338
4339 let project = Project::test(fs, None, cx).await;
4340 let (workspace, cx) =
4341 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4342 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4343
4344 add_labeled_item(&pane, "A", true, cx);
4345 add_labeled_item(&pane, "B", false, cx);
4346 add_labeled_item(&pane, "C", true, cx);
4347 add_labeled_item(&pane, "D", false, cx);
4348 add_labeled_item(&pane, "E", false, cx);
4349 assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
4350
4351 pane.update_in(cx, |pane, window, cx| {
4352 pane.close_clean_items(
4353 &CloseCleanItems {
4354 close_pinned: false,
4355 },
4356 window,
4357 cx,
4358 )
4359 })
4360 .unwrap()
4361 .await
4362 .unwrap();
4363 assert_item_labels(&pane, ["A^", "C*^"], cx);
4364 }
4365
4366 #[gpui::test]
4367 async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
4368 init_test(cx);
4369 let fs = FakeFs::new(cx.executor());
4370
4371 let project = Project::test(fs, None, cx).await;
4372 let (workspace, cx) =
4373 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4374 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4375
4376 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
4377
4378 pane.update_in(cx, |pane, window, cx| {
4379 pane.close_items_to_the_left(
4380 &CloseItemsToTheLeft {
4381 close_pinned: false,
4382 },
4383 window,
4384 cx,
4385 )
4386 })
4387 .unwrap()
4388 .await
4389 .unwrap();
4390 assert_item_labels(&pane, ["C*", "D", "E"], cx);
4391 }
4392
4393 #[gpui::test]
4394 async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
4395 init_test(cx);
4396 let fs = FakeFs::new(cx.executor());
4397
4398 let project = Project::test(fs, None, cx).await;
4399 let (workspace, cx) =
4400 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4401 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4402
4403 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
4404
4405 pane.update_in(cx, |pane, window, cx| {
4406 pane.close_items_to_the_right(
4407 &CloseItemsToTheRight {
4408 close_pinned: false,
4409 },
4410 window,
4411 cx,
4412 )
4413 })
4414 .unwrap()
4415 .await
4416 .unwrap();
4417 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4418 }
4419
4420 #[gpui::test]
4421 async fn test_close_all_items(cx: &mut TestAppContext) {
4422 init_test(cx);
4423 let fs = FakeFs::new(cx.executor());
4424
4425 let project = Project::test(fs, None, cx).await;
4426 let (workspace, cx) =
4427 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4428 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4429
4430 let item_a = add_labeled_item(&pane, "A", false, cx);
4431 add_labeled_item(&pane, "B", false, cx);
4432 add_labeled_item(&pane, "C", false, cx);
4433 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4434
4435 pane.update_in(cx, |pane, window, cx| {
4436 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4437 pane.pin_tab_at(ix, window, cx);
4438 pane.close_all_items(
4439 &CloseAllItems {
4440 save_intent: None,
4441 close_pinned: false,
4442 },
4443 window,
4444 cx,
4445 )
4446 })
4447 .unwrap()
4448 .await
4449 .unwrap();
4450 assert_item_labels(&pane, ["A*"], cx);
4451
4452 pane.update_in(cx, |pane, window, cx| {
4453 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4454 pane.unpin_tab_at(ix, window, cx);
4455 pane.close_all_items(
4456 &CloseAllItems {
4457 save_intent: None,
4458 close_pinned: false,
4459 },
4460 window,
4461 cx,
4462 )
4463 })
4464 .unwrap()
4465 .await
4466 .unwrap();
4467
4468 assert_item_labels(&pane, [], cx);
4469
4470 add_labeled_item(&pane, "A", true, cx).update(cx, |item, cx| {
4471 item.project_items
4472 .push(TestProjectItem::new_dirty(1, "A.txt", cx))
4473 });
4474 add_labeled_item(&pane, "B", true, cx).update(cx, |item, cx| {
4475 item.project_items
4476 .push(TestProjectItem::new_dirty(2, "B.txt", cx))
4477 });
4478 add_labeled_item(&pane, "C", true, cx).update(cx, |item, cx| {
4479 item.project_items
4480 .push(TestProjectItem::new_dirty(3, "C.txt", cx))
4481 });
4482 assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
4483
4484 let save = pane
4485 .update_in(cx, |pane, window, cx| {
4486 pane.close_all_items(
4487 &CloseAllItems {
4488 save_intent: None,
4489 close_pinned: false,
4490 },
4491 window,
4492 cx,
4493 )
4494 })
4495 .unwrap();
4496
4497 cx.executor().run_until_parked();
4498 cx.simulate_prompt_answer("Save all");
4499 save.await.unwrap();
4500 assert_item_labels(&pane, [], cx);
4501
4502 add_labeled_item(&pane, "A", true, cx);
4503 add_labeled_item(&pane, "B", true, cx);
4504 add_labeled_item(&pane, "C", true, cx);
4505 assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
4506 let save = pane
4507 .update_in(cx, |pane, window, cx| {
4508 pane.close_all_items(
4509 &CloseAllItems {
4510 save_intent: None,
4511 close_pinned: false,
4512 },
4513 window,
4514 cx,
4515 )
4516 })
4517 .unwrap();
4518
4519 cx.executor().run_until_parked();
4520 cx.simulate_prompt_answer("Discard all");
4521 save.await.unwrap();
4522 assert_item_labels(&pane, [], cx);
4523 }
4524
4525 #[gpui::test]
4526 async fn test_close_with_save_intent(cx: &mut TestAppContext) {
4527 init_test(cx);
4528 let fs = FakeFs::new(cx.executor());
4529
4530 let project = Project::test(fs, None, cx).await;
4531 let (workspace, cx) =
4532 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
4533 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4534
4535 let a = cx.update(|_, cx| TestProjectItem::new_dirty(1, "A.txt", cx));
4536 let b = cx.update(|_, cx| TestProjectItem::new_dirty(1, "B.txt", cx));
4537 let c = cx.update(|_, cx| TestProjectItem::new_dirty(1, "C.txt", cx));
4538
4539 add_labeled_item(&pane, "AB", true, cx).update(cx, |item, _| {
4540 item.project_items.push(a.clone());
4541 item.project_items.push(b.clone());
4542 });
4543 add_labeled_item(&pane, "C", true, cx)
4544 .update(cx, |item, _| item.project_items.push(c.clone()));
4545 assert_item_labels(&pane, ["AB^", "C*^"], cx);
4546
4547 pane.update_in(cx, |pane, window, cx| {
4548 pane.close_all_items(
4549 &CloseAllItems {
4550 save_intent: Some(SaveIntent::Save),
4551 close_pinned: false,
4552 },
4553 window,
4554 cx,
4555 )
4556 })
4557 .unwrap()
4558 .await
4559 .unwrap();
4560
4561 assert_item_labels(&pane, [], cx);
4562 cx.update(|_, cx| {
4563 assert!(!a.read(cx).is_dirty);
4564 assert!(!b.read(cx).is_dirty);
4565 assert!(!c.read(cx).is_dirty);
4566 });
4567 }
4568
4569 #[gpui::test]
4570 async fn test_close_all_items_including_pinned(cx: &mut TestAppContext) {
4571 init_test(cx);
4572 let fs = FakeFs::new(cx.executor());
4573
4574 let project = Project::test(fs, None, cx).await;
4575 let (workspace, cx) =
4576 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
4577 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4578
4579 let item_a = add_labeled_item(&pane, "A", false, cx);
4580 add_labeled_item(&pane, "B", false, cx);
4581 add_labeled_item(&pane, "C", false, cx);
4582 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4583
4584 pane.update_in(cx, |pane, window, cx| {
4585 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4586 pane.pin_tab_at(ix, window, cx);
4587 pane.close_all_items(
4588 &CloseAllItems {
4589 save_intent: None,
4590 close_pinned: true,
4591 },
4592 window,
4593 cx,
4594 )
4595 })
4596 .unwrap()
4597 .await
4598 .unwrap();
4599 assert_item_labels(&pane, [], cx);
4600 }
4601
4602 #[gpui::test]
4603 async fn test_close_pinned_tab_with_non_pinned_in_same_pane(cx: &mut TestAppContext) {
4604 init_test(cx);
4605 let fs = FakeFs::new(cx.executor());
4606 let project = Project::test(fs, None, cx).await;
4607 let (workspace, cx) =
4608 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
4609
4610 // Non-pinned tabs in same pane
4611 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4612 add_labeled_item(&pane, "A", false, cx);
4613 add_labeled_item(&pane, "B", false, cx);
4614 add_labeled_item(&pane, "C", false, cx);
4615 pane.update_in(cx, |pane, window, cx| {
4616 pane.pin_tab_at(0, window, cx);
4617 });
4618 set_labeled_items(&pane, ["A*", "B", "C"], cx);
4619 pane.update_in(cx, |pane, window, cx| {
4620 pane.close_active_item(
4621 &CloseActiveItem {
4622 save_intent: None,
4623 close_pinned: false,
4624 },
4625 window,
4626 cx,
4627 );
4628 });
4629 // Non-pinned tab should be active
4630 assert_item_labels(&pane, ["A", "B*", "C"], cx);
4631 }
4632
4633 #[gpui::test]
4634 async fn test_close_pinned_tab_with_non_pinned_in_different_pane(cx: &mut TestAppContext) {
4635 init_test(cx);
4636 let fs = FakeFs::new(cx.executor());
4637 let project = Project::test(fs, None, cx).await;
4638 let (workspace, cx) =
4639 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
4640
4641 // No non-pinned tabs in same pane, non-pinned tabs in another pane
4642 let pane1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4643 let pane2 = workspace.update_in(cx, |workspace, window, cx| {
4644 workspace.split_pane(pane1.clone(), SplitDirection::Right, window, cx)
4645 });
4646 add_labeled_item(&pane1, "A", false, cx);
4647 pane1.update_in(cx, |pane, window, cx| {
4648 pane.pin_tab_at(0, window, cx);
4649 });
4650 set_labeled_items(&pane1, ["A*"], cx);
4651 add_labeled_item(&pane2, "B", false, cx);
4652 set_labeled_items(&pane2, ["B"], cx);
4653 pane1.update_in(cx, |pane, window, cx| {
4654 pane.close_active_item(
4655 &CloseActiveItem {
4656 save_intent: None,
4657 close_pinned: false,
4658 },
4659 window,
4660 cx,
4661 );
4662 });
4663 // Non-pinned tab of other pane should be active
4664 assert_item_labels(&pane2, ["B*"], cx);
4665 }
4666
4667 fn init_test(cx: &mut TestAppContext) {
4668 cx.update(|cx| {
4669 let settings_store = SettingsStore::test(cx);
4670 cx.set_global(settings_store);
4671 theme::init(LoadThemes::JustBase, cx);
4672 crate::init_settings(cx);
4673 Project::init_settings(cx);
4674 });
4675 }
4676
4677 fn set_max_tabs(cx: &mut TestAppContext, value: Option<usize>) {
4678 cx.update_global(|store: &mut SettingsStore, cx| {
4679 store.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4680 settings.max_tabs = value.map(|v| NonZero::new(v).unwrap())
4681 });
4682 });
4683 }
4684
4685 fn add_labeled_item(
4686 pane: &Entity<Pane>,
4687 label: &str,
4688 is_dirty: bool,
4689 cx: &mut VisualTestContext,
4690 ) -> Box<Entity<TestItem>> {
4691 pane.update_in(cx, |pane, window, cx| {
4692 let labeled_item =
4693 Box::new(cx.new(|cx| TestItem::new(cx).with_label(label).with_dirty(is_dirty)));
4694 pane.add_item(labeled_item.clone(), false, false, None, window, cx);
4695 labeled_item
4696 })
4697 }
4698
4699 fn set_labeled_items<const COUNT: usize>(
4700 pane: &Entity<Pane>,
4701 labels: [&str; COUNT],
4702 cx: &mut VisualTestContext,
4703 ) -> [Box<Entity<TestItem>>; COUNT] {
4704 pane.update_in(cx, |pane, window, cx| {
4705 pane.items.clear();
4706 let mut active_item_index = 0;
4707
4708 let mut index = 0;
4709 let items = labels.map(|mut label| {
4710 if label.ends_with('*') {
4711 label = label.trim_end_matches('*');
4712 active_item_index = index;
4713 }
4714
4715 let labeled_item = Box::new(cx.new(|cx| TestItem::new(cx).with_label(label)));
4716 pane.add_item(labeled_item.clone(), false, false, None, window, cx);
4717 index += 1;
4718 labeled_item
4719 });
4720
4721 pane.activate_item(active_item_index, false, false, window, cx);
4722
4723 items
4724 })
4725 }
4726
4727 // Assert the item label, with the active item label suffixed with a '*'
4728 #[track_caller]
4729 fn assert_item_labels<const COUNT: usize>(
4730 pane: &Entity<Pane>,
4731 expected_states: [&str; COUNT],
4732 cx: &mut VisualTestContext,
4733 ) {
4734 let actual_states = pane.update(cx, |pane, cx| {
4735 pane.items
4736 .iter()
4737 .enumerate()
4738 .map(|(ix, item)| {
4739 let mut state = item
4740 .to_any()
4741 .downcast::<TestItem>()
4742 .unwrap()
4743 .read(cx)
4744 .label
4745 .clone();
4746 if ix == pane.active_item_index {
4747 state.push('*');
4748 }
4749 if item.is_dirty(cx) {
4750 state.push('^');
4751 }
4752 state
4753 })
4754 .collect::<Vec<_>>()
4755 });
4756 assert_eq!(
4757 actual_states, expected_states,
4758 "pane items do not match expectation"
4759 );
4760 }
4761}