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