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