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