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