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