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