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_pinned_tabs(&self) -> bool {
2178 self.pinned_tab_count != 0
2179 }
2180
2181 fn has_unpinned_tabs(&self) -> bool {
2182 self.pinned_tab_count < self.items.len()
2183 }
2184
2185 fn activate_unpinned_tab(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2186 if self.items.is_empty() {
2187 return;
2188 }
2189 let Some(index) = self
2190 .items()
2191 .enumerate()
2192 .find_map(|(index, _item)| (!self.is_tab_pinned(index)).then_some(index))
2193 else {
2194 return;
2195 };
2196 self.activate_item(index, true, true, window, cx);
2197 }
2198
2199 fn render_tab(
2200 &self,
2201 ix: usize,
2202 item: &dyn ItemHandle,
2203 detail: usize,
2204 focus_handle: &FocusHandle,
2205 window: &mut Window,
2206 cx: &mut Context<Pane>,
2207 ) -> impl IntoElement + use<> {
2208 let is_active = ix == self.active_item_index;
2209 let is_preview = self
2210 .preview_item_id
2211 .map(|id| id == item.item_id())
2212 .unwrap_or(false);
2213
2214 let label = item.tab_content(
2215 TabContentParams {
2216 detail: Some(detail),
2217 selected: is_active,
2218 preview: is_preview,
2219 deemphasized: !self.has_focus(window, cx),
2220 },
2221 window,
2222 cx,
2223 );
2224
2225 let item_diagnostic = item
2226 .project_path(cx)
2227 .map_or(None, |project_path| self.diagnostics.get(&project_path));
2228
2229 let decorated_icon = item_diagnostic.map_or(None, |diagnostic| {
2230 let icon = match item.tab_icon(window, cx) {
2231 Some(icon) => icon,
2232 None => return None,
2233 };
2234
2235 let knockout_item_color = if is_active {
2236 cx.theme().colors().tab_active_background
2237 } else {
2238 cx.theme().colors().tab_bar_background
2239 };
2240
2241 let (icon_decoration, icon_color) = if matches!(diagnostic, &DiagnosticSeverity::ERROR)
2242 {
2243 (IconDecorationKind::X, Color::Error)
2244 } else {
2245 (IconDecorationKind::Triangle, Color::Warning)
2246 };
2247
2248 Some(DecoratedIcon::new(
2249 icon.size(IconSize::Small).color(Color::Muted),
2250 Some(
2251 IconDecoration::new(icon_decoration, knockout_item_color, cx)
2252 .color(icon_color.color(cx))
2253 .position(Point {
2254 x: px(-2.),
2255 y: px(-2.),
2256 }),
2257 ),
2258 ))
2259 });
2260
2261 let icon = if decorated_icon.is_none() {
2262 match item_diagnostic {
2263 Some(&DiagnosticSeverity::ERROR) => None,
2264 Some(&DiagnosticSeverity::WARNING) => None,
2265 _ => item
2266 .tab_icon(window, cx)
2267 .map(|icon| icon.color(Color::Muted)),
2268 }
2269 .map(|icon| icon.size(IconSize::Small))
2270 } else {
2271 None
2272 };
2273
2274 let settings = ItemSettings::get_global(cx);
2275 let close_side = &settings.close_position;
2276 let show_close_button = &settings.show_close_button;
2277 let indicator = render_item_indicator(item.boxed_clone(), cx);
2278 let item_id = item.item_id();
2279 let is_first_item = ix == 0;
2280 let is_last_item = ix == self.items.len() - 1;
2281 let is_pinned = self.is_tab_pinned(ix);
2282 let position_relative_to_active_item = ix.cmp(&self.active_item_index);
2283
2284 let tab = Tab::new(ix)
2285 .position(if is_first_item {
2286 TabPosition::First
2287 } else if is_last_item {
2288 TabPosition::Last
2289 } else {
2290 TabPosition::Middle(position_relative_to_active_item)
2291 })
2292 .close_side(match close_side {
2293 ClosePosition::Left => ui::TabCloseSide::Start,
2294 ClosePosition::Right => ui::TabCloseSide::End,
2295 })
2296 .toggle_state(is_active)
2297 .on_click(cx.listener(move |pane: &mut Self, _, window, cx| {
2298 pane.activate_item(ix, true, true, window, cx)
2299 }))
2300 // TODO: This should be a click listener with the middle mouse button instead of a mouse down listener.
2301 .on_mouse_down(
2302 MouseButton::Middle,
2303 cx.listener(move |pane, _event, window, cx| {
2304 pane.close_item_by_id(item_id, SaveIntent::Close, window, cx)
2305 .detach_and_log_err(cx);
2306 }),
2307 )
2308 .on_mouse_down(
2309 MouseButton::Left,
2310 cx.listener(move |pane, event: &MouseDownEvent, _, cx| {
2311 if let Some(id) = pane.preview_item_id {
2312 if id == item_id && event.click_count > 1 {
2313 pane.set_preview_item_id(None, cx);
2314 }
2315 }
2316 }),
2317 )
2318 .on_drag(
2319 DraggedTab {
2320 item: item.boxed_clone(),
2321 pane: cx.entity().clone(),
2322 detail,
2323 is_active,
2324 ix,
2325 },
2326 |tab, _, _, cx| cx.new(|_| tab.clone()),
2327 )
2328 .drag_over::<DraggedTab>(|tab, _, _, cx| {
2329 tab.bg(cx.theme().colors().drop_target_background)
2330 })
2331 .drag_over::<DraggedSelection>(|tab, _, _, cx| {
2332 tab.bg(cx.theme().colors().drop_target_background)
2333 })
2334 .when_some(self.can_drop_predicate.clone(), |this, p| {
2335 this.can_drop(move |a, window, cx| p(a, window, cx))
2336 })
2337 .on_drop(
2338 cx.listener(move |this, dragged_tab: &DraggedTab, window, cx| {
2339 this.drag_split_direction = None;
2340 this.handle_tab_drop(dragged_tab, ix, window, cx)
2341 }),
2342 )
2343 .on_drop(
2344 cx.listener(move |this, selection: &DraggedSelection, window, cx| {
2345 this.drag_split_direction = None;
2346 this.handle_dragged_selection_drop(selection, Some(ix), window, cx)
2347 }),
2348 )
2349 .on_drop(cx.listener(move |this, paths, window, cx| {
2350 this.drag_split_direction = None;
2351 this.handle_external_paths_drop(paths, window, cx)
2352 }))
2353 .when_some(item.tab_tooltip_content(cx), |tab, content| match content {
2354 TabTooltipContent::Text(text) => tab.tooltip(Tooltip::text(text.clone())),
2355 TabTooltipContent::Custom(element_fn) => {
2356 tab.tooltip(move |window, cx| element_fn(window, cx))
2357 }
2358 })
2359 .start_slot::<Indicator>(indicator)
2360 .map(|this| {
2361 let end_slot_action: &'static dyn Action;
2362 let end_slot_tooltip_text: &'static str;
2363 let end_slot = if is_pinned {
2364 end_slot_action = &TogglePinTab;
2365 end_slot_tooltip_text = "Unpin Tab";
2366 IconButton::new("unpin tab", IconName::Pin)
2367 .shape(IconButtonShape::Square)
2368 .icon_color(Color::Muted)
2369 .size(ButtonSize::None)
2370 .icon_size(IconSize::XSmall)
2371 .on_click(cx.listener(move |pane, _, window, cx| {
2372 pane.unpin_tab_at(ix, window, cx);
2373 }))
2374 } else {
2375 end_slot_action = &CloseActiveItem {
2376 save_intent: None,
2377 close_pinned: false,
2378 };
2379 end_slot_tooltip_text = "Close Tab";
2380 match show_close_button {
2381 ShowCloseButton::Always => IconButton::new("close tab", IconName::Close),
2382 ShowCloseButton::Hover => {
2383 IconButton::new("close tab", IconName::Close).visible_on_hover("")
2384 }
2385 ShowCloseButton::Hidden => return this,
2386 }
2387 .shape(IconButtonShape::Square)
2388 .icon_color(Color::Muted)
2389 .size(ButtonSize::None)
2390 .icon_size(IconSize::XSmall)
2391 .on_click(cx.listener(move |pane, _, window, cx| {
2392 pane.close_item_by_id(item_id, SaveIntent::Close, window, cx)
2393 .detach_and_log_err(cx);
2394 }))
2395 }
2396 .map(|this| {
2397 if is_active {
2398 let focus_handle = focus_handle.clone();
2399 this.tooltip(move |window, cx| {
2400 Tooltip::for_action_in(
2401 end_slot_tooltip_text,
2402 end_slot_action,
2403 &focus_handle,
2404 window,
2405 cx,
2406 )
2407 })
2408 } else {
2409 this.tooltip(Tooltip::text(end_slot_tooltip_text))
2410 }
2411 });
2412 this.end_slot(end_slot)
2413 })
2414 .child(
2415 h_flex()
2416 .gap_1()
2417 .items_center()
2418 .children(
2419 std::iter::once(if let Some(decorated_icon) = decorated_icon {
2420 Some(div().child(decorated_icon.into_any_element()))
2421 } else if let Some(icon) = icon {
2422 Some(div().child(icon.into_any_element()))
2423 } else {
2424 None
2425 })
2426 .flatten(),
2427 )
2428 .child(label),
2429 );
2430
2431 let single_entry_to_resolve = self.items[ix]
2432 .is_singleton(cx)
2433 .then(|| self.items[ix].project_entry_ids(cx).get(0).copied())
2434 .flatten();
2435
2436 let total_items = self.items.len();
2437 let has_items_to_left = ix > 0;
2438 let has_items_to_right = ix < total_items - 1;
2439 let has_clean_items = self.items.iter().any(|item| !item.is_dirty(cx));
2440 let is_pinned = self.is_tab_pinned(ix);
2441 let pane = cx.entity().downgrade();
2442 let menu_context = item.item_focus_handle(cx);
2443 right_click_menu(ix)
2444 .trigger(|_| tab)
2445 .menu(move |window, cx| {
2446 let pane = pane.clone();
2447 let menu_context = menu_context.clone();
2448 ContextMenu::build(window, cx, move |mut menu, window, cx| {
2449 let close_active_item_action = CloseActiveItem {
2450 save_intent: None,
2451 close_pinned: true,
2452 };
2453 let close_inactive_items_action = CloseInactiveItems {
2454 save_intent: None,
2455 close_pinned: false,
2456 };
2457 let close_items_to_the_left_action = CloseItemsToTheLeft {
2458 close_pinned: false,
2459 };
2460 let close_items_to_the_right_action = CloseItemsToTheRight {
2461 close_pinned: false,
2462 };
2463 let close_clean_items_action = CloseCleanItems {
2464 close_pinned: false,
2465 };
2466 let close_all_items_action = CloseAllItems {
2467 save_intent: None,
2468 close_pinned: false,
2469 };
2470 if let Some(pane) = pane.upgrade() {
2471 menu = menu
2472 .entry(
2473 "Close",
2474 Some(Box::new(close_active_item_action)),
2475 window.handler_for(&pane, move |pane, window, cx| {
2476 pane.close_item_by_id(item_id, SaveIntent::Close, window, cx)
2477 .detach_and_log_err(cx);
2478 }),
2479 )
2480 .item(ContextMenuItem::Entry(
2481 ContextMenuEntry::new("Close Others")
2482 .action(Box::new(close_inactive_items_action.clone()))
2483 .disabled(total_items == 1)
2484 .handler(window.handler_for(&pane, move |pane, window, cx| {
2485 pane.close_inactive_items(
2486 &close_inactive_items_action,
2487 window,
2488 cx,
2489 )
2490 .detach_and_log_err(cx);
2491 })),
2492 ))
2493 .separator()
2494 .item(ContextMenuItem::Entry(
2495 ContextMenuEntry::new("Close Left")
2496 .action(Box::new(close_items_to_the_left_action.clone()))
2497 .disabled(!has_items_to_left)
2498 .handler(window.handler_for(&pane, move |pane, window, cx| {
2499 pane.close_items_to_the_left_by_id(
2500 Some(item_id),
2501 &close_items_to_the_left_action,
2502 window,
2503 cx,
2504 )
2505 .detach_and_log_err(cx);
2506 })),
2507 ))
2508 .item(ContextMenuItem::Entry(
2509 ContextMenuEntry::new("Close Right")
2510 .action(Box::new(close_items_to_the_right_action.clone()))
2511 .disabled(!has_items_to_right)
2512 .handler(window.handler_for(&pane, move |pane, window, cx| {
2513 pane.close_items_to_the_right_by_id(
2514 Some(item_id),
2515 &close_items_to_the_right_action,
2516 window,
2517 cx,
2518 )
2519 .detach_and_log_err(cx);
2520 })),
2521 ))
2522 .separator()
2523 .item(ContextMenuItem::Entry(
2524 ContextMenuEntry::new("Close Clean")
2525 .action(Box::new(close_clean_items_action.clone()))
2526 .disabled(!has_clean_items)
2527 .handler(window.handler_for(&pane, move |pane, window, cx| {
2528 pane.close_clean_items(
2529 &close_clean_items_action,
2530 window,
2531 cx,
2532 )
2533 .detach_and_log_err(cx)
2534 })),
2535 ))
2536 .entry(
2537 "Close All",
2538 Some(Box::new(close_all_items_action.clone())),
2539 window.handler_for(&pane, move |pane, window, cx| {
2540 pane.close_all_items(&close_all_items_action, window, cx)
2541 .detach_and_log_err(cx)
2542 }),
2543 );
2544
2545 let pin_tab_entries = |menu: ContextMenu| {
2546 menu.separator().map(|this| {
2547 if is_pinned {
2548 this.entry(
2549 "Unpin Tab",
2550 Some(TogglePinTab.boxed_clone()),
2551 window.handler_for(&pane, move |pane, window, cx| {
2552 pane.unpin_tab_at(ix, window, cx);
2553 }),
2554 )
2555 } else {
2556 this.entry(
2557 "Pin Tab",
2558 Some(TogglePinTab.boxed_clone()),
2559 window.handler_for(&pane, move |pane, window, cx| {
2560 pane.pin_tab_at(ix, window, cx);
2561 }),
2562 )
2563 }
2564 })
2565 };
2566 if let Some(entry) = single_entry_to_resolve {
2567 let project_path = pane
2568 .read(cx)
2569 .item_for_entry(entry, cx)
2570 .and_then(|item| item.project_path(cx));
2571 let worktree = project_path.as_ref().and_then(|project_path| {
2572 pane.read(cx)
2573 .project
2574 .upgrade()?
2575 .read(cx)
2576 .worktree_for_id(project_path.worktree_id, cx)
2577 });
2578 let has_relative_path = worktree.as_ref().is_some_and(|worktree| {
2579 worktree
2580 .read(cx)
2581 .root_entry()
2582 .map_or(false, |entry| entry.is_dir())
2583 });
2584
2585 let entry_abs_path = pane.read(cx).entry_abs_path(entry, cx);
2586 let parent_abs_path = entry_abs_path
2587 .as_deref()
2588 .and_then(|abs_path| Some(abs_path.parent()?.to_path_buf()));
2589 let relative_path = project_path
2590 .map(|project_path| project_path.path)
2591 .filter(|_| has_relative_path);
2592
2593 let visible_in_project_panel = relative_path.is_some()
2594 && worktree.is_some_and(|worktree| worktree.read(cx).is_visible());
2595
2596 let entry_id = entry.to_proto();
2597 menu = menu
2598 .separator()
2599 .when_some(entry_abs_path, |menu, abs_path| {
2600 menu.entry(
2601 "Copy Path",
2602 Some(Box::new(zed_actions::workspace::CopyPath)),
2603 window.handler_for(&pane, move |_, _, cx| {
2604 cx.write_to_clipboard(ClipboardItem::new_string(
2605 abs_path.to_string_lossy().to_string(),
2606 ));
2607 }),
2608 )
2609 })
2610 .when_some(relative_path, |menu, relative_path| {
2611 menu.entry(
2612 "Copy Relative Path",
2613 Some(Box::new(zed_actions::workspace::CopyRelativePath)),
2614 window.handler_for(&pane, move |_, _, cx| {
2615 cx.write_to_clipboard(ClipboardItem::new_string(
2616 relative_path.to_string_lossy().to_string(),
2617 ));
2618 }),
2619 )
2620 })
2621 .map(pin_tab_entries)
2622 .separator()
2623 .when(visible_in_project_panel, |menu| {
2624 menu.entry(
2625 "Reveal In Project Panel",
2626 Some(Box::new(RevealInProjectPanel {
2627 entry_id: Some(entry_id),
2628 })),
2629 window.handler_for(&pane, move |pane, _, cx| {
2630 pane.project
2631 .update(cx, |_, cx| {
2632 cx.emit(project::Event::RevealInProjectPanel(
2633 ProjectEntryId::from_proto(entry_id),
2634 ))
2635 })
2636 .ok();
2637 }),
2638 )
2639 })
2640 .when_some(parent_abs_path, |menu, parent_abs_path| {
2641 menu.entry(
2642 "Open in Terminal",
2643 Some(Box::new(OpenInTerminal)),
2644 window.handler_for(&pane, move |_, window, cx| {
2645 window.dispatch_action(
2646 OpenTerminal {
2647 working_directory: parent_abs_path.clone(),
2648 }
2649 .boxed_clone(),
2650 cx,
2651 );
2652 }),
2653 )
2654 });
2655 } else {
2656 menu = menu.map(pin_tab_entries);
2657 }
2658 }
2659
2660 menu.context(menu_context)
2661 })
2662 })
2663 }
2664
2665 fn render_tab_bar(&mut self, window: &mut Window, cx: &mut Context<Pane>) -> AnyElement {
2666 let focus_handle = self.focus_handle.clone();
2667 let navigate_backward = IconButton::new("navigate_backward", IconName::ArrowLeft)
2668 .icon_size(IconSize::Small)
2669 .on_click({
2670 let entity = cx.entity().clone();
2671 move |_, window, cx| {
2672 entity.update(cx, |pane, cx| pane.navigate_backward(window, cx))
2673 }
2674 })
2675 .disabled(!self.can_navigate_backward())
2676 .tooltip({
2677 let focus_handle = focus_handle.clone();
2678 move |window, cx| {
2679 Tooltip::for_action_in("Go Back", &GoBack, &focus_handle, window, cx)
2680 }
2681 });
2682
2683 let navigate_forward = IconButton::new("navigate_forward", IconName::ArrowRight)
2684 .icon_size(IconSize::Small)
2685 .on_click({
2686 let entity = cx.entity().clone();
2687 move |_, window, cx| entity.update(cx, |pane, cx| pane.navigate_forward(window, cx))
2688 })
2689 .disabled(!self.can_navigate_forward())
2690 .tooltip({
2691 let focus_handle = focus_handle.clone();
2692 move |window, cx| {
2693 Tooltip::for_action_in("Go Forward", &GoForward, &focus_handle, window, cx)
2694 }
2695 });
2696
2697 let mut tab_items = self
2698 .items
2699 .iter()
2700 .enumerate()
2701 .zip(tab_details(&self.items, window, cx))
2702 .map(|((ix, item), detail)| {
2703 self.render_tab(ix, &**item, detail, &focus_handle, window, cx)
2704 })
2705 .collect::<Vec<_>>();
2706 let tab_count = tab_items.len();
2707 let unpinned_tabs = tab_items.split_off(self.pinned_tab_count);
2708 let pinned_tabs = tab_items;
2709 TabBar::new("tab_bar")
2710 .when(
2711 self.display_nav_history_buttons.unwrap_or_default(),
2712 |tab_bar| {
2713 tab_bar
2714 .start_child(navigate_backward)
2715 .start_child(navigate_forward)
2716 },
2717 )
2718 .map(|tab_bar| {
2719 if self.show_tab_bar_buttons {
2720 let render_tab_buttons = self.render_tab_bar_buttons.clone();
2721 let (left_children, right_children) = render_tab_buttons(self, window, cx);
2722 tab_bar
2723 .start_children(left_children)
2724 .end_children(right_children)
2725 } else {
2726 tab_bar
2727 }
2728 })
2729 .children(pinned_tabs.len().ne(&0).then(|| {
2730 let content_width = self.tab_bar_scroll_handle.content_size().width;
2731 let viewport_width = self.tab_bar_scroll_handle.viewport().size.width;
2732 // We need to check both because offset returns delta values even when the scroll handle is not scrollable
2733 let is_scrollable = content_width > viewport_width;
2734 let is_scrolled = self.tab_bar_scroll_handle.offset().x < px(0.);
2735 let has_active_unpinned_tab = self.active_item_index >= self.pinned_tab_count;
2736 h_flex()
2737 .children(pinned_tabs)
2738 .when(is_scrollable && is_scrolled, |this| {
2739 this.when(has_active_unpinned_tab, |this| this.border_r_2())
2740 .when(!has_active_unpinned_tab, |this| this.border_r_1())
2741 .border_color(cx.theme().colors().border)
2742 })
2743 }))
2744 .child(
2745 h_flex()
2746 .id("unpinned tabs")
2747 .overflow_x_scroll()
2748 .w_full()
2749 .track_scroll(&self.tab_bar_scroll_handle)
2750 .children(unpinned_tabs)
2751 .child(
2752 div()
2753 .id("tab_bar_drop_target")
2754 .min_w_6()
2755 // HACK: This empty child is currently necessary to force the drop target to appear
2756 // despite us setting a min width above.
2757 .child("")
2758 .h_full()
2759 .flex_grow()
2760 .drag_over::<DraggedTab>(|bar, _, _, cx| {
2761 bar.bg(cx.theme().colors().drop_target_background)
2762 })
2763 .drag_over::<DraggedSelection>(|bar, _, _, cx| {
2764 bar.bg(cx.theme().colors().drop_target_background)
2765 })
2766 .on_drop(cx.listener(
2767 move |this, dragged_tab: &DraggedTab, window, cx| {
2768 this.drag_split_direction = None;
2769 this.handle_tab_drop(dragged_tab, this.items.len(), window, cx)
2770 },
2771 ))
2772 .on_drop(cx.listener(
2773 move |this, selection: &DraggedSelection, window, cx| {
2774 this.drag_split_direction = None;
2775 this.handle_project_entry_drop(
2776 &selection.active_selection.entry_id,
2777 Some(tab_count),
2778 window,
2779 cx,
2780 )
2781 },
2782 ))
2783 .on_drop(cx.listener(move |this, paths, window, cx| {
2784 this.drag_split_direction = None;
2785 this.handle_external_paths_drop(paths, window, cx)
2786 }))
2787 .on_click(cx.listener(move |this, event: &ClickEvent, window, cx| {
2788 if event.up.click_count == 2 {
2789 window.dispatch_action(
2790 this.double_click_dispatch_action.boxed_clone(),
2791 cx,
2792 );
2793 }
2794 })),
2795 ),
2796 )
2797 .into_any_element()
2798 }
2799
2800 pub fn render_menu_overlay(menu: &Entity<ContextMenu>) -> Div {
2801 div().absolute().bottom_0().right_0().size_0().child(
2802 deferred(anchored().anchor(Corner::TopRight).child(menu.clone())).with_priority(1),
2803 )
2804 }
2805
2806 pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut Context<Self>) {
2807 self.zoomed = zoomed;
2808 cx.notify();
2809 }
2810
2811 pub fn is_zoomed(&self) -> bool {
2812 self.zoomed
2813 }
2814
2815 fn handle_drag_move<T: 'static>(
2816 &mut self,
2817 event: &DragMoveEvent<T>,
2818 window: &mut Window,
2819 cx: &mut Context<Self>,
2820 ) {
2821 let can_split_predicate = self.can_split_predicate.take();
2822 let can_split = match &can_split_predicate {
2823 Some(can_split_predicate) => {
2824 can_split_predicate(self, event.dragged_item(), window, cx)
2825 }
2826 None => false,
2827 };
2828 self.can_split_predicate = can_split_predicate;
2829 if !can_split {
2830 return;
2831 }
2832
2833 let rect = event.bounds.size;
2834
2835 let size = event.bounds.size.width.min(event.bounds.size.height)
2836 * WorkspaceSettings::get_global(cx).drop_target_size;
2837
2838 let relative_cursor = Point::new(
2839 event.event.position.x - event.bounds.left(),
2840 event.event.position.y - event.bounds.top(),
2841 );
2842
2843 let direction = if relative_cursor.x < size
2844 || relative_cursor.x > rect.width - size
2845 || relative_cursor.y < size
2846 || relative_cursor.y > rect.height - size
2847 {
2848 [
2849 SplitDirection::Up,
2850 SplitDirection::Right,
2851 SplitDirection::Down,
2852 SplitDirection::Left,
2853 ]
2854 .iter()
2855 .min_by_key(|side| match side {
2856 SplitDirection::Up => relative_cursor.y,
2857 SplitDirection::Right => rect.width - relative_cursor.x,
2858 SplitDirection::Down => rect.height - relative_cursor.y,
2859 SplitDirection::Left => relative_cursor.x,
2860 })
2861 .cloned()
2862 } else {
2863 None
2864 };
2865
2866 if direction != self.drag_split_direction {
2867 self.drag_split_direction = direction;
2868 }
2869 }
2870
2871 pub fn handle_tab_drop(
2872 &mut self,
2873 dragged_tab: &DraggedTab,
2874 ix: usize,
2875 window: &mut Window,
2876 cx: &mut Context<Self>,
2877 ) {
2878 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2879 if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_tab, window, cx) {
2880 return;
2881 }
2882 }
2883 let mut to_pane = cx.entity().clone();
2884 let split_direction = self.drag_split_direction;
2885 let item_id = dragged_tab.item.item_id();
2886 if let Some(preview_item_id) = self.preview_item_id {
2887 if item_id == preview_item_id {
2888 self.set_preview_item_id(None, cx);
2889 }
2890 }
2891
2892 let is_clone = cfg!(target_os = "macos") && window.modifiers().alt
2893 || cfg!(not(target_os = "macos")) && window.modifiers().control;
2894
2895 let from_pane = dragged_tab.pane.clone();
2896 self.workspace
2897 .update(cx, |_, cx| {
2898 cx.defer_in(window, move |workspace, window, cx| {
2899 if let Some(split_direction) = split_direction {
2900 to_pane = workspace.split_pane(to_pane, split_direction, window, cx);
2901 }
2902 let database_id = workspace.database_id();
2903 let old_ix = from_pane.read(cx).index_for_item_id(item_id);
2904 let old_len = to_pane.read(cx).items.len();
2905 if is_clone {
2906 let Some(item) = from_pane
2907 .read(cx)
2908 .items()
2909 .find(|item| item.item_id() == item_id)
2910 .map(|item| item.clone())
2911 else {
2912 return;
2913 };
2914 if let Some(item) = item.clone_on_split(database_id, window, cx) {
2915 to_pane.update(cx, |pane, cx| {
2916 pane.add_item(item, true, true, None, window, cx);
2917 })
2918 }
2919 } else {
2920 move_item(&from_pane, &to_pane, item_id, ix, true, window, cx);
2921 }
2922 if to_pane == from_pane {
2923 if let Some(old_index) = old_ix {
2924 to_pane.update(cx, |this, _| {
2925 if old_index < this.pinned_tab_count
2926 && (ix == this.items.len() || ix > this.pinned_tab_count)
2927 {
2928 this.pinned_tab_count -= 1;
2929 } else if this.has_pinned_tabs()
2930 && old_index >= this.pinned_tab_count
2931 && ix < this.pinned_tab_count
2932 {
2933 this.pinned_tab_count += 1;
2934 }
2935 });
2936 }
2937 } else {
2938 to_pane.update(cx, |this, _| {
2939 if this.items.len() > old_len // Did we not deduplicate on drag?
2940 && this.has_pinned_tabs()
2941 && ix < this.pinned_tab_count
2942 {
2943 this.pinned_tab_count += 1;
2944 }
2945 });
2946 from_pane.update(cx, |this, _| {
2947 if let Some(index) = old_ix {
2948 if this.pinned_tab_count > index {
2949 this.pinned_tab_count -= 1;
2950 }
2951 }
2952 })
2953 }
2954 });
2955 })
2956 .log_err();
2957 }
2958
2959 fn handle_dragged_selection_drop(
2960 &mut self,
2961 dragged_selection: &DraggedSelection,
2962 dragged_onto: Option<usize>,
2963 window: &mut Window,
2964 cx: &mut Context<Self>,
2965 ) {
2966 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2967 if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_selection, window, cx)
2968 {
2969 return;
2970 }
2971 }
2972 self.handle_project_entry_drop(
2973 &dragged_selection.active_selection.entry_id,
2974 dragged_onto,
2975 window,
2976 cx,
2977 );
2978 }
2979
2980 fn handle_project_entry_drop(
2981 &mut self,
2982 project_entry_id: &ProjectEntryId,
2983 target: Option<usize>,
2984 window: &mut Window,
2985 cx: &mut Context<Self>,
2986 ) {
2987 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2988 if let ControlFlow::Break(()) = custom_drop_handle(self, project_entry_id, window, cx) {
2989 return;
2990 }
2991 }
2992 let mut to_pane = cx.entity().clone();
2993 let split_direction = self.drag_split_direction;
2994 let project_entry_id = *project_entry_id;
2995 self.workspace
2996 .update(cx, |_, cx| {
2997 cx.defer_in(window, move |workspace, window, cx| {
2998 if let Some(project_path) = workspace
2999 .project()
3000 .read(cx)
3001 .path_for_entry(project_entry_id, cx)
3002 {
3003 let load_path_task = workspace.load_path(project_path.clone(), window, cx);
3004 cx.spawn_in(window, async move |workspace, cx| {
3005 if let Some((project_entry_id, build_item)) =
3006 load_path_task.await.notify_async_err(cx)
3007 {
3008 let (to_pane, new_item_handle) = workspace
3009 .update_in(cx, |workspace, window, cx| {
3010 if let Some(split_direction) = split_direction {
3011 to_pane = workspace.split_pane(
3012 to_pane,
3013 split_direction,
3014 window,
3015 cx,
3016 );
3017 }
3018 let new_item_handle = to_pane.update(cx, |pane, cx| {
3019 pane.open_item(
3020 project_entry_id,
3021 project_path,
3022 true,
3023 false,
3024 true,
3025 target,
3026 window,
3027 cx,
3028 build_item,
3029 )
3030 });
3031 (to_pane, new_item_handle)
3032 })
3033 .log_err()?;
3034 to_pane
3035 .update_in(cx, |this, window, cx| {
3036 let Some(index) = this.index_for_item(&*new_item_handle)
3037 else {
3038 return;
3039 };
3040
3041 if target.map_or(false, |target| this.is_tab_pinned(target))
3042 {
3043 this.pin_tab_at(index, window, cx);
3044 }
3045 })
3046 .ok()?
3047 }
3048 Some(())
3049 })
3050 .detach();
3051 };
3052 });
3053 })
3054 .log_err();
3055 }
3056
3057 fn handle_external_paths_drop(
3058 &mut self,
3059 paths: &ExternalPaths,
3060 window: &mut Window,
3061 cx: &mut Context<Self>,
3062 ) {
3063 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
3064 if let ControlFlow::Break(()) = custom_drop_handle(self, paths, window, cx) {
3065 return;
3066 }
3067 }
3068 let mut to_pane = cx.entity().clone();
3069 let mut split_direction = self.drag_split_direction;
3070 let paths = paths.paths().to_vec();
3071 let is_remote = self
3072 .workspace
3073 .update(cx, |workspace, cx| {
3074 if workspace.project().read(cx).is_via_collab() {
3075 workspace.show_error(
3076 &anyhow::anyhow!("Cannot drop files on a remote project"),
3077 cx,
3078 );
3079 true
3080 } else {
3081 false
3082 }
3083 })
3084 .unwrap_or(true);
3085 if is_remote {
3086 return;
3087 }
3088
3089 self.workspace
3090 .update(cx, |workspace, cx| {
3091 let fs = Arc::clone(workspace.project().read(cx).fs());
3092 cx.spawn_in(window, async move |workspace, cx| {
3093 let mut is_file_checks = FuturesUnordered::new();
3094 for path in &paths {
3095 is_file_checks.push(fs.is_file(path))
3096 }
3097 let mut has_files_to_open = false;
3098 while let Some(is_file) = is_file_checks.next().await {
3099 if is_file {
3100 has_files_to_open = true;
3101 break;
3102 }
3103 }
3104 drop(is_file_checks);
3105 if !has_files_to_open {
3106 split_direction = None;
3107 }
3108
3109 if let Ok(open_task) = workspace.update_in(cx, |workspace, window, cx| {
3110 if let Some(split_direction) = split_direction {
3111 to_pane = workspace.split_pane(to_pane, split_direction, window, cx);
3112 }
3113 workspace.open_paths(
3114 paths,
3115 OpenOptions {
3116 visible: Some(OpenVisible::OnlyDirectories),
3117 ..Default::default()
3118 },
3119 Some(to_pane.downgrade()),
3120 window,
3121 cx,
3122 )
3123 }) {
3124 let opened_items: Vec<_> = open_task.await;
3125 _ = workspace.update(cx, |workspace, cx| {
3126 for item in opened_items.into_iter().flatten() {
3127 if let Err(e) = item {
3128 workspace.show_error(&e, cx);
3129 }
3130 }
3131 });
3132 }
3133 })
3134 .detach();
3135 })
3136 .log_err();
3137 }
3138
3139 pub fn display_nav_history_buttons(&mut self, display: Option<bool>) {
3140 self.display_nav_history_buttons = display;
3141 }
3142
3143 fn pinned_item_ids(&self) -> HashSet<EntityId> {
3144 self.items
3145 .iter()
3146 .enumerate()
3147 .filter_map(|(index, item)| {
3148 if self.is_tab_pinned(index) {
3149 return Some(item.item_id());
3150 }
3151
3152 None
3153 })
3154 .collect()
3155 }
3156
3157 fn clean_item_ids(&self, cx: &mut Context<Pane>) -> HashSet<EntityId> {
3158 self.items()
3159 .filter_map(|item| {
3160 if !item.is_dirty(cx) {
3161 return Some(item.item_id());
3162 }
3163
3164 None
3165 })
3166 .collect()
3167 }
3168
3169 fn to_the_side_item_ids(&self, item_id: EntityId, side: Side) -> HashSet<EntityId> {
3170 match side {
3171 Side::Left => self
3172 .items()
3173 .take_while(|item| item.item_id() != item_id)
3174 .map(|item| item.item_id())
3175 .collect(),
3176 Side::Right => self
3177 .items()
3178 .rev()
3179 .take_while(|item| item.item_id() != item_id)
3180 .map(|item| item.item_id())
3181 .collect(),
3182 }
3183 }
3184
3185 pub fn drag_split_direction(&self) -> Option<SplitDirection> {
3186 self.drag_split_direction
3187 }
3188
3189 pub fn set_zoom_out_on_close(&mut self, zoom_out_on_close: bool) {
3190 self.zoom_out_on_close = zoom_out_on_close;
3191 }
3192}
3193
3194fn default_render_tab_bar_buttons(
3195 pane: &mut Pane,
3196 window: &mut Window,
3197 cx: &mut Context<Pane>,
3198) -> (Option<AnyElement>, Option<AnyElement>) {
3199 if !pane.has_focus(window, cx) && !pane.context_menu_focused(window, cx) {
3200 return (None, None);
3201 }
3202 // Ideally we would return a vec of elements here to pass directly to the [TabBar]'s
3203 // `end_slot`, but due to needing a view here that isn't possible.
3204 let right_children = h_flex()
3205 // Instead we need to replicate the spacing from the [TabBar]'s `end_slot` here.
3206 .gap(DynamicSpacing::Base04.rems(cx))
3207 .child(
3208 PopoverMenu::new("pane-tab-bar-popover-menu")
3209 .trigger_with_tooltip(
3210 IconButton::new("plus", IconName::Plus).icon_size(IconSize::Small),
3211 Tooltip::text("New..."),
3212 )
3213 .anchor(Corner::TopRight)
3214 .with_handle(pane.new_item_context_menu_handle.clone())
3215 .menu(move |window, cx| {
3216 Some(ContextMenu::build(window, cx, |menu, _, _| {
3217 menu.action("New File", NewFile.boxed_clone())
3218 .action("Open File", ToggleFileFinder::default().boxed_clone())
3219 .separator()
3220 .action(
3221 "Search Project",
3222 DeploySearch {
3223 replace_enabled: false,
3224 included_files: None,
3225 excluded_files: None,
3226 }
3227 .boxed_clone(),
3228 )
3229 .action("Search Symbols", ToggleProjectSymbols.boxed_clone())
3230 .separator()
3231 .action("New Terminal", NewTerminal.boxed_clone())
3232 }))
3233 }),
3234 )
3235 .child(
3236 PopoverMenu::new("pane-tab-bar-split")
3237 .trigger_with_tooltip(
3238 IconButton::new("split", IconName::Split).icon_size(IconSize::Small),
3239 Tooltip::text("Split Pane"),
3240 )
3241 .anchor(Corner::TopRight)
3242 .with_handle(pane.split_item_context_menu_handle.clone())
3243 .menu(move |window, cx| {
3244 ContextMenu::build(window, cx, |menu, _, _| {
3245 menu.action("Split Right", SplitRight.boxed_clone())
3246 .action("Split Left", SplitLeft.boxed_clone())
3247 .action("Split Up", SplitUp.boxed_clone())
3248 .action("Split Down", SplitDown.boxed_clone())
3249 })
3250 .into()
3251 }),
3252 )
3253 .child({
3254 let zoomed = pane.is_zoomed();
3255 IconButton::new("toggle_zoom", IconName::Maximize)
3256 .icon_size(IconSize::Small)
3257 .toggle_state(zoomed)
3258 .selected_icon(IconName::Minimize)
3259 .on_click(cx.listener(|pane, _, window, cx| {
3260 pane.toggle_zoom(&crate::ToggleZoom, window, cx);
3261 }))
3262 .tooltip(move |window, cx| {
3263 Tooltip::for_action(
3264 if zoomed { "Zoom Out" } else { "Zoom In" },
3265 &ToggleZoom,
3266 window,
3267 cx,
3268 )
3269 })
3270 })
3271 .into_any_element()
3272 .into();
3273 (None, right_children)
3274}
3275
3276impl Focusable for Pane {
3277 fn focus_handle(&self, _cx: &App) -> FocusHandle {
3278 self.focus_handle.clone()
3279 }
3280}
3281
3282impl Render for Pane {
3283 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3284 let mut key_context = KeyContext::new_with_defaults();
3285 key_context.add("Pane");
3286 if self.active_item().is_none() {
3287 key_context.add("EmptyPane");
3288 }
3289
3290 let should_display_tab_bar = self.should_display_tab_bar.clone();
3291 let display_tab_bar = should_display_tab_bar(window, cx);
3292 let Some(project) = self.project.upgrade() else {
3293 return div().track_focus(&self.focus_handle(cx));
3294 };
3295 let is_local = project.read(cx).is_local();
3296
3297 v_flex()
3298 .key_context(key_context)
3299 .track_focus(&self.focus_handle(cx))
3300 .size_full()
3301 .flex_none()
3302 .overflow_hidden()
3303 .on_action(cx.listener(|pane, _: &AlternateFile, window, cx| {
3304 pane.alternate_file(window, cx);
3305 }))
3306 .on_action(
3307 cx.listener(|pane, _: &SplitLeft, _, cx| pane.split(SplitDirection::Left, cx)),
3308 )
3309 .on_action(cx.listener(|pane, _: &SplitUp, _, cx| pane.split(SplitDirection::Up, cx)))
3310 .on_action(cx.listener(|pane, _: &SplitHorizontal, _, cx| {
3311 pane.split(SplitDirection::horizontal(cx), cx)
3312 }))
3313 .on_action(cx.listener(|pane, _: &SplitVertical, _, cx| {
3314 pane.split(SplitDirection::vertical(cx), cx)
3315 }))
3316 .on_action(
3317 cx.listener(|pane, _: &SplitRight, _, cx| pane.split(SplitDirection::Right, cx)),
3318 )
3319 .on_action(
3320 cx.listener(|pane, _: &SplitDown, _, cx| pane.split(SplitDirection::Down, cx)),
3321 )
3322 .on_action(
3323 cx.listener(|pane, _: &GoBack, window, cx| pane.navigate_backward(window, cx)),
3324 )
3325 .on_action(
3326 cx.listener(|pane, _: &GoForward, window, cx| pane.navigate_forward(window, cx)),
3327 )
3328 .on_action(cx.listener(|_, _: &JoinIntoNext, _, cx| {
3329 cx.emit(Event::JoinIntoNext);
3330 }))
3331 .on_action(cx.listener(|_, _: &JoinAll, _, cx| {
3332 cx.emit(Event::JoinAll);
3333 }))
3334 .on_action(cx.listener(Pane::toggle_zoom))
3335 .on_action(
3336 cx.listener(|pane: &mut Pane, action: &ActivateItem, window, cx| {
3337 pane.activate_item(
3338 action.0.min(pane.items.len().saturating_sub(1)),
3339 true,
3340 true,
3341 window,
3342 cx,
3343 );
3344 }),
3345 )
3346 .on_action(
3347 cx.listener(|pane: &mut Pane, _: &ActivateLastItem, window, cx| {
3348 pane.activate_item(pane.items.len().saturating_sub(1), true, true, window, cx);
3349 }),
3350 )
3351 .on_action(
3352 cx.listener(|pane: &mut Pane, _: &ActivatePreviousItem, window, cx| {
3353 pane.activate_prev_item(true, window, cx);
3354 }),
3355 )
3356 .on_action(
3357 cx.listener(|pane: &mut Pane, _: &ActivateNextItem, window, cx| {
3358 pane.activate_next_item(true, window, cx);
3359 }),
3360 )
3361 .on_action(
3362 cx.listener(|pane, _: &SwapItemLeft, window, cx| pane.swap_item_left(window, cx)),
3363 )
3364 .on_action(
3365 cx.listener(|pane, _: &SwapItemRight, window, cx| pane.swap_item_right(window, cx)),
3366 )
3367 .on_action(cx.listener(|pane, action, window, cx| {
3368 pane.toggle_pin_tab(action, window, cx);
3369 }))
3370 .when(PreviewTabsSettings::get_global(cx).enabled, |this| {
3371 this.on_action(cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, _, cx| {
3372 if let Some(active_item_id) = pane.active_item().map(|i| i.item_id()) {
3373 if pane.is_active_preview_item(active_item_id) {
3374 pane.set_preview_item_id(None, cx);
3375 } else {
3376 pane.set_preview_item_id(Some(active_item_id), cx);
3377 }
3378 }
3379 }))
3380 })
3381 .on_action(
3382 cx.listener(|pane: &mut Self, action: &CloseActiveItem, window, cx| {
3383 pane.close_active_item(action, window, cx)
3384 .detach_and_log_err(cx)
3385 }),
3386 )
3387 .on_action(
3388 cx.listener(|pane: &mut Self, action: &CloseInactiveItems, window, cx| {
3389 pane.close_inactive_items(action, window, cx)
3390 .detach_and_log_err(cx);
3391 }),
3392 )
3393 .on_action(
3394 cx.listener(|pane: &mut Self, action: &CloseCleanItems, window, cx| {
3395 pane.close_clean_items(action, window, cx)
3396 .detach_and_log_err(cx)
3397 }),
3398 )
3399 .on_action(cx.listener(
3400 |pane: &mut Self, action: &CloseItemsToTheLeft, window, cx| {
3401 pane.close_items_to_the_left_by_id(None, action, window, cx)
3402 .detach_and_log_err(cx)
3403 },
3404 ))
3405 .on_action(cx.listener(
3406 |pane: &mut Self, action: &CloseItemsToTheRight, window, cx| {
3407 pane.close_items_to_the_right_by_id(None, action, window, cx)
3408 .detach_and_log_err(cx)
3409 },
3410 ))
3411 .on_action(
3412 cx.listener(|pane: &mut Self, action: &CloseAllItems, window, cx| {
3413 pane.close_all_items(action, window, cx)
3414 .detach_and_log_err(cx)
3415 }),
3416 )
3417 .on_action(
3418 cx.listener(|pane: &mut Self, action: &RevealInProjectPanel, _, cx| {
3419 let entry_id = action
3420 .entry_id
3421 .map(ProjectEntryId::from_proto)
3422 .or_else(|| pane.active_item()?.project_entry_ids(cx).first().copied());
3423 if let Some(entry_id) = entry_id {
3424 pane.project
3425 .update(cx, |_, cx| {
3426 cx.emit(project::Event::RevealInProjectPanel(entry_id))
3427 })
3428 .ok();
3429 }
3430 }),
3431 )
3432 .on_action(cx.listener(|_, _: &menu::Cancel, window, cx| {
3433 if cx.stop_active_drag(window) {
3434 return;
3435 } else {
3436 cx.propagate();
3437 }
3438 }))
3439 .when(self.active_item().is_some() && display_tab_bar, |pane| {
3440 pane.child((self.render_tab_bar.clone())(self, window, cx))
3441 })
3442 .child({
3443 let has_worktrees = project.read(cx).visible_worktrees(cx).next().is_some();
3444 // main content
3445 div()
3446 .flex_1()
3447 .relative()
3448 .group("")
3449 .overflow_hidden()
3450 .on_drag_move::<DraggedTab>(cx.listener(Self::handle_drag_move))
3451 .on_drag_move::<DraggedSelection>(cx.listener(Self::handle_drag_move))
3452 .when(is_local, |div| {
3453 div.on_drag_move::<ExternalPaths>(cx.listener(Self::handle_drag_move))
3454 })
3455 .map(|div| {
3456 if let Some(item) = self.active_item() {
3457 div.id("pane_placeholder")
3458 .v_flex()
3459 .size_full()
3460 .overflow_hidden()
3461 .child(self.toolbar.clone())
3462 .child(item.to_any())
3463 } else {
3464 let placeholder = div
3465 .id("pane_placeholder")
3466 .h_flex()
3467 .size_full()
3468 .justify_center()
3469 .on_click(cx.listener(
3470 move |this, event: &ClickEvent, window, cx| {
3471 if event.up.click_count == 2 {
3472 window.dispatch_action(
3473 this.double_click_dispatch_action.boxed_clone(),
3474 cx,
3475 );
3476 }
3477 },
3478 ));
3479 if has_worktrees {
3480 placeholder
3481 } else {
3482 placeholder.child(
3483 Label::new("Open a file or project to get started.")
3484 .color(Color::Muted),
3485 )
3486 }
3487 }
3488 })
3489 .child(
3490 // drag target
3491 div()
3492 .invisible()
3493 .absolute()
3494 .bg(cx.theme().colors().drop_target_background)
3495 .group_drag_over::<DraggedTab>("", |style| style.visible())
3496 .group_drag_over::<DraggedSelection>("", |style| style.visible())
3497 .when(is_local, |div| {
3498 div.group_drag_over::<ExternalPaths>("", |style| style.visible())
3499 })
3500 .when_some(self.can_drop_predicate.clone(), |this, p| {
3501 this.can_drop(move |a, window, cx| p(a, window, cx))
3502 })
3503 .on_drop(cx.listener(move |this, dragged_tab, window, cx| {
3504 this.handle_tab_drop(
3505 dragged_tab,
3506 this.active_item_index(),
3507 window,
3508 cx,
3509 )
3510 }))
3511 .on_drop(cx.listener(
3512 move |this, selection: &DraggedSelection, window, cx| {
3513 this.handle_dragged_selection_drop(selection, None, window, cx)
3514 },
3515 ))
3516 .on_drop(cx.listener(move |this, paths, window, cx| {
3517 this.handle_external_paths_drop(paths, window, cx)
3518 }))
3519 .map(|div| {
3520 let size = DefiniteLength::Fraction(0.5);
3521 match self.drag_split_direction {
3522 None => div.top_0().right_0().bottom_0().left_0(),
3523 Some(SplitDirection::Up) => {
3524 div.top_0().left_0().right_0().h(size)
3525 }
3526 Some(SplitDirection::Down) => {
3527 div.left_0().bottom_0().right_0().h(size)
3528 }
3529 Some(SplitDirection::Left) => {
3530 div.top_0().left_0().bottom_0().w(size)
3531 }
3532 Some(SplitDirection::Right) => {
3533 div.top_0().bottom_0().right_0().w(size)
3534 }
3535 }
3536 }),
3537 )
3538 })
3539 .on_mouse_down(
3540 MouseButton::Navigate(NavigationDirection::Back),
3541 cx.listener(|pane, _, window, cx| {
3542 if let Some(workspace) = pane.workspace.upgrade() {
3543 let pane = cx.entity().downgrade();
3544 window.defer(cx, move |window, cx| {
3545 workspace.update(cx, |workspace, cx| {
3546 workspace.go_back(pane, window, cx).detach_and_log_err(cx)
3547 })
3548 })
3549 }
3550 }),
3551 )
3552 .on_mouse_down(
3553 MouseButton::Navigate(NavigationDirection::Forward),
3554 cx.listener(|pane, _, window, cx| {
3555 if let Some(workspace) = pane.workspace.upgrade() {
3556 let pane = cx.entity().downgrade();
3557 window.defer(cx, move |window, cx| {
3558 workspace.update(cx, |workspace, cx| {
3559 workspace
3560 .go_forward(pane, window, cx)
3561 .detach_and_log_err(cx)
3562 })
3563 })
3564 }
3565 }),
3566 )
3567 }
3568}
3569
3570impl ItemNavHistory {
3571 pub fn push<D: 'static + Send + Any>(&mut self, data: Option<D>, cx: &mut App) {
3572 if self
3573 .item
3574 .upgrade()
3575 .is_some_and(|item| item.include_in_nav_history())
3576 {
3577 self.history
3578 .push(data, self.item.clone(), self.is_preview, cx);
3579 }
3580 }
3581
3582 pub fn pop_backward(&mut self, cx: &mut App) -> Option<NavigationEntry> {
3583 self.history.pop(NavigationMode::GoingBack, cx)
3584 }
3585
3586 pub fn pop_forward(&mut self, cx: &mut App) -> Option<NavigationEntry> {
3587 self.history.pop(NavigationMode::GoingForward, cx)
3588 }
3589}
3590
3591impl NavHistory {
3592 pub fn for_each_entry(
3593 &self,
3594 cx: &App,
3595 mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
3596 ) {
3597 let borrowed_history = self.0.lock();
3598 borrowed_history
3599 .forward_stack
3600 .iter()
3601 .chain(borrowed_history.backward_stack.iter())
3602 .chain(borrowed_history.closed_stack.iter())
3603 .for_each(|entry| {
3604 if let Some(project_and_abs_path) =
3605 borrowed_history.paths_by_item.get(&entry.item.id())
3606 {
3607 f(entry, project_and_abs_path.clone());
3608 } else if let Some(item) = entry.item.upgrade() {
3609 if let Some(path) = item.project_path(cx) {
3610 f(entry, (path, None));
3611 }
3612 }
3613 })
3614 }
3615
3616 pub fn set_mode(&mut self, mode: NavigationMode) {
3617 self.0.lock().mode = mode;
3618 }
3619
3620 pub fn mode(&self) -> NavigationMode {
3621 self.0.lock().mode
3622 }
3623
3624 pub fn disable(&mut self) {
3625 self.0.lock().mode = NavigationMode::Disabled;
3626 }
3627
3628 pub fn enable(&mut self) {
3629 self.0.lock().mode = NavigationMode::Normal;
3630 }
3631
3632 pub fn pop(&mut self, mode: NavigationMode, cx: &mut App) -> Option<NavigationEntry> {
3633 let mut state = self.0.lock();
3634 let entry = match mode {
3635 NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
3636 return None;
3637 }
3638 NavigationMode::GoingBack => &mut state.backward_stack,
3639 NavigationMode::GoingForward => &mut state.forward_stack,
3640 NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
3641 }
3642 .pop_back();
3643 if entry.is_some() {
3644 state.did_update(cx);
3645 }
3646 entry
3647 }
3648
3649 pub fn push<D: 'static + Send + Any>(
3650 &mut self,
3651 data: Option<D>,
3652 item: Arc<dyn WeakItemHandle>,
3653 is_preview: bool,
3654 cx: &mut App,
3655 ) {
3656 let state = &mut *self.0.lock();
3657 match state.mode {
3658 NavigationMode::Disabled => {}
3659 NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
3660 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3661 state.backward_stack.pop_front();
3662 }
3663 state.backward_stack.push_back(NavigationEntry {
3664 item,
3665 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3666 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3667 is_preview,
3668 });
3669 state.forward_stack.clear();
3670 }
3671 NavigationMode::GoingBack => {
3672 if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3673 state.forward_stack.pop_front();
3674 }
3675 state.forward_stack.push_back(NavigationEntry {
3676 item,
3677 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3678 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3679 is_preview,
3680 });
3681 }
3682 NavigationMode::GoingForward => {
3683 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3684 state.backward_stack.pop_front();
3685 }
3686 state.backward_stack.push_back(NavigationEntry {
3687 item,
3688 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3689 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3690 is_preview,
3691 });
3692 }
3693 NavigationMode::ClosingItem => {
3694 if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3695 state.closed_stack.pop_front();
3696 }
3697 state.closed_stack.push_back(NavigationEntry {
3698 item,
3699 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3700 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3701 is_preview,
3702 });
3703 }
3704 }
3705 state.did_update(cx);
3706 }
3707
3708 pub fn remove_item(&mut self, item_id: EntityId) {
3709 let mut state = self.0.lock();
3710 state.paths_by_item.remove(&item_id);
3711 state
3712 .backward_stack
3713 .retain(|entry| entry.item.id() != item_id);
3714 state
3715 .forward_stack
3716 .retain(|entry| entry.item.id() != item_id);
3717 state
3718 .closed_stack
3719 .retain(|entry| entry.item.id() != item_id);
3720 }
3721
3722 pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option<PathBuf>)> {
3723 self.0.lock().paths_by_item.get(&item_id).cloned()
3724 }
3725}
3726
3727impl NavHistoryState {
3728 pub fn did_update(&self, cx: &mut App) {
3729 if let Some(pane) = self.pane.upgrade() {
3730 cx.defer(move |cx| {
3731 pane.update(cx, |pane, cx| pane.history_updated(cx));
3732 });
3733 }
3734 }
3735}
3736
3737fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
3738 let path = buffer_path
3739 .as_ref()
3740 .and_then(|p| {
3741 p.path
3742 .to_str()
3743 .and_then(|s| if s.is_empty() { None } else { Some(s) })
3744 })
3745 .unwrap_or("This buffer");
3746 let path = truncate_and_remove_front(path, 80);
3747 format!("{path} contains unsaved edits. Do you want to save it?")
3748}
3749
3750pub fn tab_details(items: &[Box<dyn ItemHandle>], _window: &Window, cx: &App) -> Vec<usize> {
3751 let mut tab_details = items.iter().map(|_| 0).collect::<Vec<_>>();
3752 let mut tab_descriptions = HashMap::default();
3753 let mut done = false;
3754 while !done {
3755 done = true;
3756
3757 // Store item indices by their tab description.
3758 for (ix, (item, detail)) in items.iter().zip(&tab_details).enumerate() {
3759 let description = item.tab_content_text(*detail, cx);
3760 if *detail == 0 || description != item.tab_content_text(detail - 1, cx) {
3761 tab_descriptions
3762 .entry(description)
3763 .or_insert(Vec::new())
3764 .push(ix);
3765 }
3766 }
3767
3768 // If two or more items have the same tab description, increase their level
3769 // of detail and try again.
3770 for (_, item_ixs) in tab_descriptions.drain() {
3771 if item_ixs.len() > 1 {
3772 done = false;
3773 for ix in item_ixs {
3774 tab_details[ix] += 1;
3775 }
3776 }
3777 }
3778 }
3779
3780 tab_details
3781}
3782
3783pub fn render_item_indicator(item: Box<dyn ItemHandle>, cx: &App) -> Option<Indicator> {
3784 maybe!({
3785 let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
3786 (true, _) => Color::Warning,
3787 (_, true) => Color::Accent,
3788 (false, false) => return None,
3789 };
3790
3791 Some(Indicator::dot().color(indicator_color))
3792 })
3793}
3794
3795impl Render for DraggedTab {
3796 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3797 let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
3798 let label = self.item.tab_content(
3799 TabContentParams {
3800 detail: Some(self.detail),
3801 selected: false,
3802 preview: false,
3803 deemphasized: false,
3804 },
3805 window,
3806 cx,
3807 );
3808 Tab::new("")
3809 .toggle_state(self.is_active)
3810 .child(label)
3811 .render(window, cx)
3812 .font(ui_font)
3813 }
3814}
3815
3816#[cfg(test)]
3817mod tests {
3818 use std::num::NonZero;
3819
3820 use super::*;
3821 use crate::item::test::{TestItem, TestProjectItem};
3822 use gpui::{TestAppContext, VisualTestContext};
3823 use project::FakeFs;
3824 use settings::SettingsStore;
3825 use theme::LoadThemes;
3826 use util::TryFutureExt;
3827
3828 #[gpui::test]
3829 async fn test_add_item_capped_to_max_tabs(cx: &mut TestAppContext) {
3830 init_test(cx);
3831 let fs = FakeFs::new(cx.executor());
3832
3833 let project = Project::test(fs, None, cx).await;
3834 let (workspace, cx) =
3835 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3836 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3837
3838 for i in 0..7 {
3839 add_labeled_item(&pane, format!("{}", i).as_str(), false, cx);
3840 }
3841 set_max_tabs(cx, Some(5));
3842 add_labeled_item(&pane, "7", false, cx);
3843 // Remove items to respect the max tab cap.
3844 assert_item_labels(&pane, ["3", "4", "5", "6", "7*"], cx);
3845 pane.update_in(cx, |pane, window, cx| {
3846 pane.activate_item(0, false, false, window, cx);
3847 });
3848 add_labeled_item(&pane, "X", false, cx);
3849 // Respect activation order.
3850 assert_item_labels(&pane, ["3", "X*", "5", "6", "7"], cx);
3851
3852 for i in 0..7 {
3853 add_labeled_item(&pane, format!("D{}", i).as_str(), true, cx);
3854 }
3855 // Keeps dirty items, even over max tab cap.
3856 assert_item_labels(
3857 &pane,
3858 ["D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6*^"],
3859 cx,
3860 );
3861
3862 set_max_tabs(cx, None);
3863 for i in 0..7 {
3864 add_labeled_item(&pane, format!("N{}", i).as_str(), false, cx);
3865 }
3866 // No cap when max tabs is None.
3867 assert_item_labels(
3868 &pane,
3869 [
3870 "D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6^", "N0", "N1", "N2", "N3", "N4",
3871 "N5", "N6*",
3872 ],
3873 cx,
3874 );
3875 }
3876
3877 #[gpui::test]
3878 async fn test_allow_pinning_dirty_item_at_max_tabs(cx: &mut TestAppContext) {
3879 init_test(cx);
3880 let fs = FakeFs::new(cx.executor());
3881
3882 let project = Project::test(fs, None, cx).await;
3883 let (workspace, cx) =
3884 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3885 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3886
3887 set_max_tabs(cx, Some(1));
3888 let item_a = add_labeled_item(&pane, "A", true, cx);
3889
3890 pane.update_in(cx, |pane, window, cx| {
3891 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
3892 pane.pin_tab_at(ix, window, cx);
3893 });
3894 assert_item_labels(&pane, ["A*^!"], cx);
3895 }
3896
3897 #[gpui::test]
3898 async fn test_allow_pinning_non_dirty_item_at_max_tabs(cx: &mut TestAppContext) {
3899 init_test(cx);
3900 let fs = FakeFs::new(cx.executor());
3901
3902 let project = Project::test(fs, None, cx).await;
3903 let (workspace, cx) =
3904 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3905 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3906
3907 set_max_tabs(cx, Some(1));
3908 let item_a = add_labeled_item(&pane, "A", false, cx);
3909
3910 pane.update_in(cx, |pane, window, cx| {
3911 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
3912 pane.pin_tab_at(ix, window, cx);
3913 });
3914 assert_item_labels(&pane, ["A*!"], cx);
3915 }
3916
3917 #[gpui::test]
3918 async fn test_pin_tabs_incrementally_at_max_capacity(cx: &mut TestAppContext) {
3919 init_test(cx);
3920 let fs = FakeFs::new(cx.executor());
3921
3922 let project = Project::test(fs, None, cx).await;
3923 let (workspace, cx) =
3924 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3925 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3926
3927 set_max_tabs(cx, Some(3));
3928
3929 let item_a = add_labeled_item(&pane, "A", false, cx);
3930 assert_item_labels(&pane, ["A*"], cx);
3931
3932 pane.update_in(cx, |pane, window, cx| {
3933 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
3934 pane.pin_tab_at(ix, window, cx);
3935 });
3936 assert_item_labels(&pane, ["A*!"], cx);
3937
3938 let item_b = add_labeled_item(&pane, "B", false, cx);
3939 assert_item_labels(&pane, ["A!", "B*"], cx);
3940
3941 pane.update_in(cx, |pane, window, cx| {
3942 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
3943 pane.pin_tab_at(ix, window, cx);
3944 });
3945 assert_item_labels(&pane, ["A!", "B*!"], cx);
3946
3947 let item_c = add_labeled_item(&pane, "C", false, cx);
3948 assert_item_labels(&pane, ["A!", "B!", "C*"], cx);
3949
3950 pane.update_in(cx, |pane, window, cx| {
3951 let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
3952 pane.pin_tab_at(ix, window, cx);
3953 });
3954 assert_item_labels(&pane, ["A!", "B!", "C*!"], cx);
3955 }
3956
3957 #[gpui::test]
3958 async fn test_pin_tabs_left_to_right_after_opening_at_max_capacity(cx: &mut TestAppContext) {
3959 init_test(cx);
3960 let fs = FakeFs::new(cx.executor());
3961
3962 let project = Project::test(fs, None, cx).await;
3963 let (workspace, cx) =
3964 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3965 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3966
3967 set_max_tabs(cx, Some(3));
3968
3969 let item_a = add_labeled_item(&pane, "A", false, cx);
3970 assert_item_labels(&pane, ["A*"], cx);
3971
3972 let item_b = add_labeled_item(&pane, "B", false, cx);
3973 assert_item_labels(&pane, ["A", "B*"], cx);
3974
3975 let item_c = add_labeled_item(&pane, "C", false, cx);
3976 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3977
3978 pane.update_in(cx, |pane, window, cx| {
3979 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
3980 pane.pin_tab_at(ix, window, cx);
3981 });
3982 assert_item_labels(&pane, ["A!", "B", "C*"], cx);
3983
3984 pane.update_in(cx, |pane, window, cx| {
3985 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
3986 pane.pin_tab_at(ix, window, cx);
3987 });
3988 assert_item_labels(&pane, ["A!", "B!", "C*"], cx);
3989
3990 pane.update_in(cx, |pane, window, cx| {
3991 let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
3992 pane.pin_tab_at(ix, window, cx);
3993 });
3994 assert_item_labels(&pane, ["A!", "B!", "C*!"], cx);
3995 }
3996
3997 #[gpui::test]
3998 async fn test_pin_tabs_right_to_left_after_opening_at_max_capacity(cx: &mut TestAppContext) {
3999 init_test(cx);
4000 let fs = FakeFs::new(cx.executor());
4001
4002 let project = Project::test(fs, None, cx).await;
4003 let (workspace, cx) =
4004 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4005 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4006
4007 set_max_tabs(cx, Some(3));
4008
4009 let item_a = add_labeled_item(&pane, "A", false, cx);
4010 assert_item_labels(&pane, ["A*"], cx);
4011
4012 let item_b = add_labeled_item(&pane, "B", false, cx);
4013 assert_item_labels(&pane, ["A", "B*"], cx);
4014
4015 let item_c = add_labeled_item(&pane, "C", false, cx);
4016 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4017
4018 pane.update_in(cx, |pane, window, cx| {
4019 let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4020 pane.pin_tab_at(ix, window, cx);
4021 });
4022 assert_item_labels(&pane, ["C*!", "A", "B"], cx);
4023
4024 pane.update_in(cx, |pane, window, cx| {
4025 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4026 pane.pin_tab_at(ix, window, cx);
4027 });
4028 assert_item_labels(&pane, ["C*!", "B!", "A"], cx);
4029
4030 pane.update_in(cx, |pane, window, cx| {
4031 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4032 pane.pin_tab_at(ix, window, cx);
4033 });
4034 assert_item_labels(&pane, ["C*!", "B!", "A!"], cx);
4035 }
4036
4037 #[gpui::test]
4038 async fn test_pinned_tabs_never_closed_at_max_tabs(cx: &mut TestAppContext) {
4039 init_test(cx);
4040 let fs = FakeFs::new(cx.executor());
4041
4042 let project = Project::test(fs, None, cx).await;
4043 let (workspace, cx) =
4044 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4045 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4046
4047 let item_a = add_labeled_item(&pane, "A", false, cx);
4048 pane.update_in(cx, |pane, window, cx| {
4049 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4050 pane.pin_tab_at(ix, window, cx);
4051 });
4052
4053 let item_b = add_labeled_item(&pane, "B", false, cx);
4054 pane.update_in(cx, |pane, window, cx| {
4055 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4056 pane.pin_tab_at(ix, window, cx);
4057 });
4058
4059 add_labeled_item(&pane, "C", false, cx);
4060 add_labeled_item(&pane, "D", false, cx);
4061 add_labeled_item(&pane, "E", false, cx);
4062 assert_item_labels(&pane, ["A!", "B!", "C", "D", "E*"], cx);
4063
4064 set_max_tabs(cx, Some(3));
4065 add_labeled_item(&pane, "F", false, cx);
4066 assert_item_labels(&pane, ["A!", "B!", "F*"], cx);
4067
4068 add_labeled_item(&pane, "G", false, cx);
4069 assert_item_labels(&pane, ["A!", "B!", "G*"], cx);
4070
4071 add_labeled_item(&pane, "H", false, cx);
4072 assert_item_labels(&pane, ["A!", "B!", "H*"], cx);
4073 }
4074
4075 #[gpui::test]
4076 async fn test_always_allows_one_unpinned_item_over_max_tabs_regardless_of_pinned_count(
4077 cx: &mut TestAppContext,
4078 ) {
4079 init_test(cx);
4080 let fs = FakeFs::new(cx.executor());
4081
4082 let project = Project::test(fs, None, cx).await;
4083 let (workspace, cx) =
4084 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4085 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4086
4087 set_max_tabs(cx, Some(3));
4088
4089 let item_a = add_labeled_item(&pane, "A", false, cx);
4090 pane.update_in(cx, |pane, window, cx| {
4091 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4092 pane.pin_tab_at(ix, window, cx);
4093 });
4094
4095 let item_b = add_labeled_item(&pane, "B", false, cx);
4096 pane.update_in(cx, |pane, window, cx| {
4097 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4098 pane.pin_tab_at(ix, window, cx);
4099 });
4100
4101 let item_c = add_labeled_item(&pane, "C", false, cx);
4102 pane.update_in(cx, |pane, window, cx| {
4103 let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4104 pane.pin_tab_at(ix, window, cx);
4105 });
4106
4107 assert_item_labels(&pane, ["A!", "B!", "C*!"], cx);
4108
4109 let item_d = add_labeled_item(&pane, "D", false, cx);
4110 assert_item_labels(&pane, ["A!", "B!", "C!", "D*"], cx);
4111
4112 pane.update_in(cx, |pane, window, cx| {
4113 let ix = pane.index_for_item_id(item_d.item_id()).unwrap();
4114 pane.pin_tab_at(ix, window, cx);
4115 });
4116 assert_item_labels(&pane, ["A!", "B!", "C!", "D*!"], cx);
4117
4118 add_labeled_item(&pane, "E", false, cx);
4119 assert_item_labels(&pane, ["A!", "B!", "C!", "D!", "E*"], cx);
4120
4121 add_labeled_item(&pane, "F", false, cx);
4122 assert_item_labels(&pane, ["A!", "B!", "C!", "D!", "F*"], cx);
4123 }
4124
4125 #[gpui::test]
4126 async fn test_can_open_one_item_when_all_tabs_are_dirty_at_max(cx: &mut TestAppContext) {
4127 init_test(cx);
4128 let fs = FakeFs::new(cx.executor());
4129
4130 let project = Project::test(fs, None, cx).await;
4131 let (workspace, cx) =
4132 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4133 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4134
4135 set_max_tabs(cx, Some(3));
4136
4137 add_labeled_item(&pane, "A", true, cx);
4138 assert_item_labels(&pane, ["A*^"], cx);
4139
4140 add_labeled_item(&pane, "B", true, cx);
4141 assert_item_labels(&pane, ["A^", "B*^"], cx);
4142
4143 add_labeled_item(&pane, "C", true, cx);
4144 assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
4145
4146 add_labeled_item(&pane, "D", false, cx);
4147 assert_item_labels(&pane, ["A^", "B^", "C^", "D*"], cx);
4148
4149 add_labeled_item(&pane, "E", false, cx);
4150 assert_item_labels(&pane, ["A^", "B^", "C^", "E*"], cx);
4151
4152 add_labeled_item(&pane, "F", false, cx);
4153 assert_item_labels(&pane, ["A^", "B^", "C^", "F*"], cx);
4154
4155 add_labeled_item(&pane, "G", true, cx);
4156 assert_item_labels(&pane, ["A^", "B^", "C^", "G*^"], cx);
4157 }
4158
4159 #[gpui::test]
4160 async fn test_toggle_pin_tab(cx: &mut TestAppContext) {
4161 init_test(cx);
4162 let fs = FakeFs::new(cx.executor());
4163
4164 let project = Project::test(fs, None, cx).await;
4165 let (workspace, cx) =
4166 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4167 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4168
4169 set_labeled_items(&pane, ["A", "B*", "C"], cx);
4170 assert_item_labels(&pane, ["A", "B*", "C"], cx);
4171
4172 pane.update_in(cx, |pane, window, cx| {
4173 pane.toggle_pin_tab(&TogglePinTab, window, cx);
4174 });
4175 assert_item_labels(&pane, ["B*!", "A", "C"], cx);
4176
4177 pane.update_in(cx, |pane, window, cx| {
4178 pane.toggle_pin_tab(&TogglePinTab, window, cx);
4179 });
4180 assert_item_labels(&pane, ["B*", "A", "C"], cx);
4181 }
4182
4183 #[gpui::test]
4184 async fn test_pinning_active_tab_without_position_change_maintains_focus(
4185 cx: &mut TestAppContext,
4186 ) {
4187 init_test(cx);
4188 let fs = FakeFs::new(cx.executor());
4189
4190 let project = Project::test(fs, None, cx).await;
4191 let (workspace, cx) =
4192 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4193 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4194
4195 // Add A
4196 let item_a = add_labeled_item(&pane, "A", false, cx);
4197 assert_item_labels(&pane, ["A*"], cx);
4198
4199 // Add B
4200 add_labeled_item(&pane, "B", false, cx);
4201 assert_item_labels(&pane, ["A", "B*"], cx);
4202
4203 // Activate A again
4204 pane.update_in(cx, |pane, window, cx| {
4205 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4206 pane.activate_item(ix, true, true, window, cx);
4207 });
4208 assert_item_labels(&pane, ["A*", "B"], cx);
4209
4210 // Pin A - remains active
4211 pane.update_in(cx, |pane, window, cx| {
4212 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4213 pane.pin_tab_at(ix, window, cx);
4214 });
4215 assert_item_labels(&pane, ["A*!", "B"], cx);
4216
4217 // Unpin A - remain active
4218 pane.update_in(cx, |pane, window, cx| {
4219 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4220 pane.unpin_tab_at(ix, window, cx);
4221 });
4222 assert_item_labels(&pane, ["A*", "B"], cx);
4223 }
4224
4225 #[gpui::test]
4226 async fn test_pinning_active_tab_with_position_change_maintains_focus(cx: &mut TestAppContext) {
4227 init_test(cx);
4228 let fs = FakeFs::new(cx.executor());
4229
4230 let project = Project::test(fs, None, cx).await;
4231 let (workspace, cx) =
4232 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4233 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4234
4235 // Add A, B, C
4236 add_labeled_item(&pane, "A", false, cx);
4237 add_labeled_item(&pane, "B", false, cx);
4238 let item_c = add_labeled_item(&pane, "C", false, cx);
4239 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4240
4241 // Pin C - moves to pinned area, remains active
4242 pane.update_in(cx, |pane, window, cx| {
4243 let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4244 pane.pin_tab_at(ix, window, cx);
4245 });
4246 assert_item_labels(&pane, ["C*!", "A", "B"], cx);
4247
4248 // Unpin C - moves after pinned area, remains active
4249 pane.update_in(cx, |pane, window, cx| {
4250 let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4251 pane.unpin_tab_at(ix, window, cx);
4252 });
4253 assert_item_labels(&pane, ["C*", "A", "B"], cx);
4254 }
4255
4256 #[gpui::test]
4257 async fn test_pinning_inactive_tab_without_position_change_preserves_existing_focus(
4258 cx: &mut TestAppContext,
4259 ) {
4260 init_test(cx);
4261 let fs = FakeFs::new(cx.executor());
4262
4263 let project = Project::test(fs, None, cx).await;
4264 let (workspace, cx) =
4265 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4266 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4267
4268 // Add A, B
4269 let item_a = add_labeled_item(&pane, "A", false, cx);
4270 add_labeled_item(&pane, "B", false, cx);
4271 assert_item_labels(&pane, ["A", "B*"], cx);
4272
4273 // Pin A - already in pinned area, B remains active
4274 pane.update_in(cx, |pane, window, cx| {
4275 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4276 pane.pin_tab_at(ix, window, cx);
4277 });
4278 assert_item_labels(&pane, ["A!", "B*"], cx);
4279
4280 // Unpin A - stays in place, B remains active
4281 pane.update_in(cx, |pane, window, cx| {
4282 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4283 pane.unpin_tab_at(ix, window, cx);
4284 });
4285 assert_item_labels(&pane, ["A", "B*"], cx);
4286 }
4287
4288 #[gpui::test]
4289 async fn test_pinning_inactive_tab_with_position_change_preserves_existing_focus(
4290 cx: &mut TestAppContext,
4291 ) {
4292 init_test(cx);
4293 let fs = FakeFs::new(cx.executor());
4294
4295 let project = Project::test(fs, None, cx).await;
4296 let (workspace, cx) =
4297 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4298 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4299
4300 // Add A, B, C
4301 add_labeled_item(&pane, "A", false, cx);
4302 let item_b = add_labeled_item(&pane, "B", false, cx);
4303 let item_c = add_labeled_item(&pane, "C", false, cx);
4304 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4305
4306 // Activate B
4307 pane.update_in(cx, |pane, window, cx| {
4308 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4309 pane.activate_item(ix, true, true, window, cx);
4310 });
4311 assert_item_labels(&pane, ["A", "B*", "C"], cx);
4312
4313 // Pin C - moves to pinned area, B remains active
4314 pane.update_in(cx, |pane, window, cx| {
4315 let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4316 pane.pin_tab_at(ix, window, cx);
4317 });
4318 assert_item_labels(&pane, ["C!", "A", "B*"], cx);
4319
4320 // Unpin C - moves after pinned area, B remains active
4321 pane.update_in(cx, |pane, window, cx| {
4322 let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4323 pane.unpin_tab_at(ix, window, cx);
4324 });
4325 assert_item_labels(&pane, ["C", "A", "B*"], cx);
4326 }
4327
4328 #[gpui::test]
4329 async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
4330 init_test(cx);
4331 let fs = FakeFs::new(cx.executor());
4332
4333 let project = Project::test(fs, None, cx).await;
4334 let (workspace, cx) =
4335 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4336 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4337
4338 // 1. Add with a destination index
4339 // a. Add before the active item
4340 set_labeled_items(&pane, ["A", "B*", "C"], cx);
4341 pane.update_in(cx, |pane, window, cx| {
4342 pane.add_item(
4343 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
4344 false,
4345 false,
4346 Some(0),
4347 window,
4348 cx,
4349 );
4350 });
4351 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
4352
4353 // b. Add after the active item
4354 set_labeled_items(&pane, ["A", "B*", "C"], cx);
4355 pane.update_in(cx, |pane, window, cx| {
4356 pane.add_item(
4357 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
4358 false,
4359 false,
4360 Some(2),
4361 window,
4362 cx,
4363 );
4364 });
4365 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
4366
4367 // c. Add at the end of the item list (including off the length)
4368 set_labeled_items(&pane, ["A", "B*", "C"], cx);
4369 pane.update_in(cx, |pane, window, cx| {
4370 pane.add_item(
4371 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
4372 false,
4373 false,
4374 Some(5),
4375 window,
4376 cx,
4377 );
4378 });
4379 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4380
4381 // 2. Add without a destination index
4382 // a. Add with active item at the start of the item list
4383 set_labeled_items(&pane, ["A*", "B", "C"], cx);
4384 pane.update_in(cx, |pane, window, cx| {
4385 pane.add_item(
4386 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
4387 false,
4388 false,
4389 None,
4390 window,
4391 cx,
4392 );
4393 });
4394 set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
4395
4396 // b. Add with active item at the end of the item list
4397 set_labeled_items(&pane, ["A", "B", "C*"], cx);
4398 pane.update_in(cx, |pane, window, cx| {
4399 pane.add_item(
4400 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
4401 false,
4402 false,
4403 None,
4404 window,
4405 cx,
4406 );
4407 });
4408 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4409 }
4410
4411 #[gpui::test]
4412 async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
4413 init_test(cx);
4414 let fs = FakeFs::new(cx.executor());
4415
4416 let project = Project::test(fs, None, cx).await;
4417 let (workspace, cx) =
4418 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4419 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4420
4421 // 1. Add with a destination index
4422 // 1a. Add before the active item
4423 let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
4424 pane.update_in(cx, |pane, window, cx| {
4425 pane.add_item(d, false, false, Some(0), window, cx);
4426 });
4427 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
4428
4429 // 1b. Add after the active item
4430 let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
4431 pane.update_in(cx, |pane, window, cx| {
4432 pane.add_item(d, false, false, Some(2), window, cx);
4433 });
4434 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
4435
4436 // 1c. Add at the end of the item list (including off the length)
4437 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
4438 pane.update_in(cx, |pane, window, cx| {
4439 pane.add_item(a, false, false, Some(5), window, cx);
4440 });
4441 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
4442
4443 // 1d. Add same item to active index
4444 let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
4445 pane.update_in(cx, |pane, window, cx| {
4446 pane.add_item(b, false, false, Some(1), window, cx);
4447 });
4448 assert_item_labels(&pane, ["A", "B*", "C"], cx);
4449
4450 // 1e. Add item to index after same item in last position
4451 let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
4452 pane.update_in(cx, |pane, window, cx| {
4453 pane.add_item(c, false, false, Some(2), window, cx);
4454 });
4455 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4456
4457 // 2. Add without a destination index
4458 // 2a. Add with active item at the start of the item list
4459 let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
4460 pane.update_in(cx, |pane, window, cx| {
4461 pane.add_item(d, false, false, None, window, cx);
4462 });
4463 assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
4464
4465 // 2b. Add with active item at the end of the item list
4466 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
4467 pane.update_in(cx, |pane, window, cx| {
4468 pane.add_item(a, false, false, None, window, cx);
4469 });
4470 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
4471
4472 // 2c. Add active item to active item at end of list
4473 let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
4474 pane.update_in(cx, |pane, window, cx| {
4475 pane.add_item(c, false, false, None, window, cx);
4476 });
4477 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4478
4479 // 2d. Add active item to active item at start of list
4480 let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
4481 pane.update_in(cx, |pane, window, cx| {
4482 pane.add_item(a, false, false, None, window, cx);
4483 });
4484 assert_item_labels(&pane, ["A*", "B", "C"], cx);
4485 }
4486
4487 #[gpui::test]
4488 async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
4489 init_test(cx);
4490 let fs = FakeFs::new(cx.executor());
4491
4492 let project = Project::test(fs, None, cx).await;
4493 let (workspace, cx) =
4494 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4495 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4496
4497 // singleton view
4498 pane.update_in(cx, |pane, window, cx| {
4499 pane.add_item(
4500 Box::new(cx.new(|cx| {
4501 TestItem::new(cx)
4502 .with_singleton(true)
4503 .with_label("buffer 1")
4504 .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
4505 })),
4506 false,
4507 false,
4508 None,
4509 window,
4510 cx,
4511 );
4512 });
4513 assert_item_labels(&pane, ["buffer 1*"], cx);
4514
4515 // new singleton view with the same project entry
4516 pane.update_in(cx, |pane, window, cx| {
4517 pane.add_item(
4518 Box::new(cx.new(|cx| {
4519 TestItem::new(cx)
4520 .with_singleton(true)
4521 .with_label("buffer 1")
4522 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4523 })),
4524 false,
4525 false,
4526 None,
4527 window,
4528 cx,
4529 );
4530 });
4531 assert_item_labels(&pane, ["buffer 1*"], cx);
4532
4533 // new singleton view with different project entry
4534 pane.update_in(cx, |pane, window, cx| {
4535 pane.add_item(
4536 Box::new(cx.new(|cx| {
4537 TestItem::new(cx)
4538 .with_singleton(true)
4539 .with_label("buffer 2")
4540 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
4541 })),
4542 false,
4543 false,
4544 None,
4545 window,
4546 cx,
4547 );
4548 });
4549 assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
4550
4551 // new multibuffer view with the same project entry
4552 pane.update_in(cx, |pane, window, cx| {
4553 pane.add_item(
4554 Box::new(cx.new(|cx| {
4555 TestItem::new(cx)
4556 .with_singleton(false)
4557 .with_label("multibuffer 1")
4558 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4559 })),
4560 false,
4561 false,
4562 None,
4563 window,
4564 cx,
4565 );
4566 });
4567 assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
4568
4569 // another multibuffer view with the same project entry
4570 pane.update_in(cx, |pane, window, cx| {
4571 pane.add_item(
4572 Box::new(cx.new(|cx| {
4573 TestItem::new(cx)
4574 .with_singleton(false)
4575 .with_label("multibuffer 1b")
4576 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4577 })),
4578 false,
4579 false,
4580 None,
4581 window,
4582 cx,
4583 );
4584 });
4585 assert_item_labels(
4586 &pane,
4587 ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
4588 cx,
4589 );
4590 }
4591
4592 #[gpui::test]
4593 async fn test_remove_item_ordering_history(cx: &mut TestAppContext) {
4594 init_test(cx);
4595 let fs = FakeFs::new(cx.executor());
4596
4597 let project = Project::test(fs, None, cx).await;
4598 let (workspace, cx) =
4599 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4600 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4601
4602 add_labeled_item(&pane, "A", false, cx);
4603 add_labeled_item(&pane, "B", false, cx);
4604 add_labeled_item(&pane, "C", false, cx);
4605 add_labeled_item(&pane, "D", false, cx);
4606 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4607
4608 pane.update_in(cx, |pane, window, cx| {
4609 pane.activate_item(1, false, false, window, cx)
4610 });
4611 add_labeled_item(&pane, "1", false, cx);
4612 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
4613
4614 pane.update_in(cx, |pane, window, cx| {
4615 pane.close_active_item(
4616 &CloseActiveItem {
4617 save_intent: None,
4618 close_pinned: false,
4619 },
4620 window,
4621 cx,
4622 )
4623 })
4624 .await
4625 .unwrap();
4626 assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
4627
4628 pane.update_in(cx, |pane, window, cx| {
4629 pane.activate_item(3, false, false, window, cx)
4630 });
4631 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4632
4633 pane.update_in(cx, |pane, window, cx| {
4634 pane.close_active_item(
4635 &CloseActiveItem {
4636 save_intent: None,
4637 close_pinned: false,
4638 },
4639 window,
4640 cx,
4641 )
4642 })
4643 .await
4644 .unwrap();
4645 assert_item_labels(&pane, ["A", "B*", "C"], cx);
4646
4647 pane.update_in(cx, |pane, window, cx| {
4648 pane.close_active_item(
4649 &CloseActiveItem {
4650 save_intent: None,
4651 close_pinned: false,
4652 },
4653 window,
4654 cx,
4655 )
4656 })
4657 .await
4658 .unwrap();
4659 assert_item_labels(&pane, ["A", "C*"], cx);
4660
4661 pane.update_in(cx, |pane, window, cx| {
4662 pane.close_active_item(
4663 &CloseActiveItem {
4664 save_intent: None,
4665 close_pinned: false,
4666 },
4667 window,
4668 cx,
4669 )
4670 })
4671 .await
4672 .unwrap();
4673 assert_item_labels(&pane, ["A*"], cx);
4674 }
4675
4676 #[gpui::test]
4677 async fn test_remove_item_ordering_neighbour(cx: &mut TestAppContext) {
4678 init_test(cx);
4679 cx.update_global::<SettingsStore, ()>(|s, cx| {
4680 s.update_user_settings::<ItemSettings>(cx, |s| {
4681 s.activate_on_close = Some(ActivateOnClose::Neighbour);
4682 });
4683 });
4684 let fs = FakeFs::new(cx.executor());
4685
4686 let project = Project::test(fs, None, cx).await;
4687 let (workspace, cx) =
4688 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4689 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4690
4691 add_labeled_item(&pane, "A", false, cx);
4692 add_labeled_item(&pane, "B", false, cx);
4693 add_labeled_item(&pane, "C", false, cx);
4694 add_labeled_item(&pane, "D", false, cx);
4695 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4696
4697 pane.update_in(cx, |pane, window, cx| {
4698 pane.activate_item(1, false, false, window, cx)
4699 });
4700 add_labeled_item(&pane, "1", false, cx);
4701 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
4702
4703 pane.update_in(cx, |pane, window, cx| {
4704 pane.close_active_item(
4705 &CloseActiveItem {
4706 save_intent: None,
4707 close_pinned: false,
4708 },
4709 window,
4710 cx,
4711 )
4712 })
4713 .await
4714 .unwrap();
4715 assert_item_labels(&pane, ["A", "B", "C*", "D"], cx);
4716
4717 pane.update_in(cx, |pane, window, cx| {
4718 pane.activate_item(3, false, false, window, cx)
4719 });
4720 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4721
4722 pane.update_in(cx, |pane, window, cx| {
4723 pane.close_active_item(
4724 &CloseActiveItem {
4725 save_intent: None,
4726 close_pinned: false,
4727 },
4728 window,
4729 cx,
4730 )
4731 })
4732 .await
4733 .unwrap();
4734 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4735
4736 pane.update_in(cx, |pane, window, cx| {
4737 pane.close_active_item(
4738 &CloseActiveItem {
4739 save_intent: None,
4740 close_pinned: false,
4741 },
4742 window,
4743 cx,
4744 )
4745 })
4746 .await
4747 .unwrap();
4748 assert_item_labels(&pane, ["A", "B*"], cx);
4749
4750 pane.update_in(cx, |pane, window, cx| {
4751 pane.close_active_item(
4752 &CloseActiveItem {
4753 save_intent: None,
4754 close_pinned: false,
4755 },
4756 window,
4757 cx,
4758 )
4759 })
4760 .await
4761 .unwrap();
4762 assert_item_labels(&pane, ["A*"], cx);
4763 }
4764
4765 #[gpui::test]
4766 async fn test_remove_item_ordering_left_neighbour(cx: &mut TestAppContext) {
4767 init_test(cx);
4768 cx.update_global::<SettingsStore, ()>(|s, cx| {
4769 s.update_user_settings::<ItemSettings>(cx, |s| {
4770 s.activate_on_close = Some(ActivateOnClose::LeftNeighbour);
4771 });
4772 });
4773 let fs = FakeFs::new(cx.executor());
4774
4775 let project = Project::test(fs, None, cx).await;
4776 let (workspace, cx) =
4777 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4778 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4779
4780 add_labeled_item(&pane, "A", false, cx);
4781 add_labeled_item(&pane, "B", false, cx);
4782 add_labeled_item(&pane, "C", false, cx);
4783 add_labeled_item(&pane, "D", false, cx);
4784 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4785
4786 pane.update_in(cx, |pane, window, cx| {
4787 pane.activate_item(1, false, false, window, cx)
4788 });
4789 add_labeled_item(&pane, "1", false, cx);
4790 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
4791
4792 pane.update_in(cx, |pane, window, cx| {
4793 pane.close_active_item(
4794 &CloseActiveItem {
4795 save_intent: None,
4796 close_pinned: false,
4797 },
4798 window,
4799 cx,
4800 )
4801 })
4802 .await
4803 .unwrap();
4804 assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
4805
4806 pane.update_in(cx, |pane, window, cx| {
4807 pane.activate_item(3, false, false, window, cx)
4808 });
4809 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4810
4811 pane.update_in(cx, |pane, window, cx| {
4812 pane.close_active_item(
4813 &CloseActiveItem {
4814 save_intent: None,
4815 close_pinned: false,
4816 },
4817 window,
4818 cx,
4819 )
4820 })
4821 .await
4822 .unwrap();
4823 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4824
4825 pane.update_in(cx, |pane, window, cx| {
4826 pane.activate_item(0, false, false, window, cx)
4827 });
4828 assert_item_labels(&pane, ["A*", "B", "C"], cx);
4829
4830 pane.update_in(cx, |pane, window, cx| {
4831 pane.close_active_item(
4832 &CloseActiveItem {
4833 save_intent: None,
4834 close_pinned: false,
4835 },
4836 window,
4837 cx,
4838 )
4839 })
4840 .await
4841 .unwrap();
4842 assert_item_labels(&pane, ["B*", "C"], cx);
4843
4844 pane.update_in(cx, |pane, window, cx| {
4845 pane.close_active_item(
4846 &CloseActiveItem {
4847 save_intent: None,
4848 close_pinned: false,
4849 },
4850 window,
4851 cx,
4852 )
4853 })
4854 .await
4855 .unwrap();
4856 assert_item_labels(&pane, ["C*"], cx);
4857 }
4858
4859 #[gpui::test]
4860 async fn test_close_inactive_items(cx: &mut TestAppContext) {
4861 init_test(cx);
4862 let fs = FakeFs::new(cx.executor());
4863
4864 let project = Project::test(fs, None, cx).await;
4865 let (workspace, cx) =
4866 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4867 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4868
4869 let item_a = add_labeled_item(&pane, "A", false, cx);
4870 pane.update_in(cx, |pane, window, cx| {
4871 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4872 pane.pin_tab_at(ix, window, cx);
4873 });
4874 assert_item_labels(&pane, ["A*!"], cx);
4875
4876 let item_b = add_labeled_item(&pane, "B", false, cx);
4877 pane.update_in(cx, |pane, window, cx| {
4878 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4879 pane.pin_tab_at(ix, window, cx);
4880 });
4881 assert_item_labels(&pane, ["A!", "B*!"], cx);
4882
4883 add_labeled_item(&pane, "C", false, cx);
4884 assert_item_labels(&pane, ["A!", "B!", "C*"], cx);
4885
4886 add_labeled_item(&pane, "D", false, cx);
4887 add_labeled_item(&pane, "E", false, cx);
4888 assert_item_labels(&pane, ["A!", "B!", "C", "D", "E*"], cx);
4889
4890 pane.update_in(cx, |pane, window, cx| {
4891 pane.close_inactive_items(
4892 &CloseInactiveItems {
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!", "E*"], cx);
4903 }
4904
4905 #[gpui::test]
4906 async fn test_close_clean_items(cx: &mut TestAppContext) {
4907 init_test(cx);
4908 let fs = FakeFs::new(cx.executor());
4909
4910 let project = Project::test(fs, None, cx).await;
4911 let (workspace, cx) =
4912 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4913 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4914
4915 add_labeled_item(&pane, "A", true, cx);
4916 add_labeled_item(&pane, "B", false, cx);
4917 add_labeled_item(&pane, "C", true, cx);
4918 add_labeled_item(&pane, "D", false, cx);
4919 add_labeled_item(&pane, "E", false, cx);
4920 assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
4921
4922 pane.update_in(cx, |pane, window, cx| {
4923 pane.close_clean_items(
4924 &CloseCleanItems {
4925 close_pinned: false,
4926 },
4927 window,
4928 cx,
4929 )
4930 })
4931 .await
4932 .unwrap();
4933 assert_item_labels(&pane, ["A^", "C*^"], cx);
4934 }
4935
4936 #[gpui::test]
4937 async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
4938 init_test(cx);
4939 let fs = FakeFs::new(cx.executor());
4940
4941 let project = Project::test(fs, None, cx).await;
4942 let (workspace, cx) =
4943 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4944 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4945
4946 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
4947
4948 pane.update_in(cx, |pane, window, cx| {
4949 pane.close_items_to_the_left_by_id(
4950 None,
4951 &CloseItemsToTheLeft {
4952 close_pinned: false,
4953 },
4954 window,
4955 cx,
4956 )
4957 })
4958 .await
4959 .unwrap();
4960 assert_item_labels(&pane, ["C*", "D", "E"], cx);
4961 }
4962
4963 #[gpui::test]
4964 async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
4965 init_test(cx);
4966 let fs = FakeFs::new(cx.executor());
4967
4968 let project = Project::test(fs, None, cx).await;
4969 let (workspace, cx) =
4970 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4971 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4972
4973 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
4974
4975 pane.update_in(cx, |pane, window, cx| {
4976 pane.close_items_to_the_right_by_id(
4977 None,
4978 &CloseItemsToTheRight {
4979 close_pinned: false,
4980 },
4981 window,
4982 cx,
4983 )
4984 })
4985 .await
4986 .unwrap();
4987 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4988 }
4989
4990 #[gpui::test]
4991 async fn test_close_all_items(cx: &mut TestAppContext) {
4992 init_test(cx);
4993 let fs = FakeFs::new(cx.executor());
4994
4995 let project = Project::test(fs, None, cx).await;
4996 let (workspace, cx) =
4997 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4998 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4999
5000 let item_a = add_labeled_item(&pane, "A", false, cx);
5001 add_labeled_item(&pane, "B", false, cx);
5002 add_labeled_item(&pane, "C", false, cx);
5003 assert_item_labels(&pane, ["A", "B", "C*"], cx);
5004
5005 pane.update_in(cx, |pane, window, cx| {
5006 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
5007 pane.pin_tab_at(ix, window, cx);
5008 pane.close_all_items(
5009 &CloseAllItems {
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 pane.update_in(cx, |pane, window, cx| {
5022 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
5023 pane.unpin_tab_at(ix, window, cx);
5024 pane.close_all_items(
5025 &CloseAllItems {
5026 save_intent: None,
5027 close_pinned: false,
5028 },
5029 window,
5030 cx,
5031 )
5032 })
5033 .await
5034 .unwrap();
5035
5036 assert_item_labels(&pane, [], cx);
5037
5038 add_labeled_item(&pane, "A", true, cx).update(cx, |item, cx| {
5039 item.project_items
5040 .push(TestProjectItem::new_dirty(1, "A.txt", cx))
5041 });
5042 add_labeled_item(&pane, "B", true, cx).update(cx, |item, cx| {
5043 item.project_items
5044 .push(TestProjectItem::new_dirty(2, "B.txt", cx))
5045 });
5046 add_labeled_item(&pane, "C", true, cx).update(cx, |item, cx| {
5047 item.project_items
5048 .push(TestProjectItem::new_dirty(3, "C.txt", cx))
5049 });
5050 assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
5051
5052 let save = pane.update_in(cx, |pane, window, cx| {
5053 pane.close_all_items(
5054 &CloseAllItems {
5055 save_intent: None,
5056 close_pinned: false,
5057 },
5058 window,
5059 cx,
5060 )
5061 });
5062
5063 cx.executor().run_until_parked();
5064 cx.simulate_prompt_answer("Save all");
5065 save.await.unwrap();
5066 assert_item_labels(&pane, [], cx);
5067
5068 add_labeled_item(&pane, "A", true, cx);
5069 add_labeled_item(&pane, "B", true, cx);
5070 add_labeled_item(&pane, "C", true, cx);
5071 assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
5072 let save = pane.update_in(cx, |pane, window, cx| {
5073 pane.close_all_items(
5074 &CloseAllItems {
5075 save_intent: None,
5076 close_pinned: false,
5077 },
5078 window,
5079 cx,
5080 )
5081 });
5082
5083 cx.executor().run_until_parked();
5084 cx.simulate_prompt_answer("Discard all");
5085 save.await.unwrap();
5086 assert_item_labels(&pane, [], cx);
5087 }
5088
5089 #[gpui::test]
5090 async fn test_close_with_save_intent(cx: &mut TestAppContext) {
5091 init_test(cx);
5092 let fs = FakeFs::new(cx.executor());
5093
5094 let project = Project::test(fs, None, cx).await;
5095 let (workspace, cx) =
5096 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
5097 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5098
5099 let a = cx.update(|_, cx| TestProjectItem::new_dirty(1, "A.txt", cx));
5100 let b = cx.update(|_, cx| TestProjectItem::new_dirty(1, "B.txt", cx));
5101 let c = cx.update(|_, cx| TestProjectItem::new_dirty(1, "C.txt", cx));
5102
5103 add_labeled_item(&pane, "AB", true, cx).update(cx, |item, _| {
5104 item.project_items.push(a.clone());
5105 item.project_items.push(b.clone());
5106 });
5107 add_labeled_item(&pane, "C", true, cx)
5108 .update(cx, |item, _| item.project_items.push(c.clone()));
5109 assert_item_labels(&pane, ["AB^", "C*^"], cx);
5110
5111 pane.update_in(cx, |pane, window, cx| {
5112 pane.close_all_items(
5113 &CloseAllItems {
5114 save_intent: Some(SaveIntent::Save),
5115 close_pinned: false,
5116 },
5117 window,
5118 cx,
5119 )
5120 })
5121 .await
5122 .unwrap();
5123
5124 assert_item_labels(&pane, [], cx);
5125 cx.update(|_, cx| {
5126 assert!(!a.read(cx).is_dirty);
5127 assert!(!b.read(cx).is_dirty);
5128 assert!(!c.read(cx).is_dirty);
5129 });
5130 }
5131
5132 #[gpui::test]
5133 async fn test_close_all_items_including_pinned(cx: &mut TestAppContext) {
5134 init_test(cx);
5135 let fs = FakeFs::new(cx.executor());
5136
5137 let project = Project::test(fs, None, cx).await;
5138 let (workspace, cx) =
5139 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
5140 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5141
5142 let item_a = add_labeled_item(&pane, "A", false, cx);
5143 add_labeled_item(&pane, "B", false, cx);
5144 add_labeled_item(&pane, "C", false, cx);
5145 assert_item_labels(&pane, ["A", "B", "C*"], cx);
5146
5147 pane.update_in(cx, |pane, window, cx| {
5148 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
5149 pane.pin_tab_at(ix, window, cx);
5150 pane.close_all_items(
5151 &CloseAllItems {
5152 save_intent: None,
5153 close_pinned: true,
5154 },
5155 window,
5156 cx,
5157 )
5158 })
5159 .await
5160 .unwrap();
5161 assert_item_labels(&pane, [], cx);
5162 }
5163
5164 #[gpui::test]
5165 async fn test_close_pinned_tab_with_non_pinned_in_same_pane(cx: &mut TestAppContext) {
5166 init_test(cx);
5167 let fs = FakeFs::new(cx.executor());
5168 let project = Project::test(fs, None, cx).await;
5169 let (workspace, cx) =
5170 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
5171
5172 // Non-pinned tabs in same pane
5173 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5174 add_labeled_item(&pane, "A", false, cx);
5175 add_labeled_item(&pane, "B", false, cx);
5176 add_labeled_item(&pane, "C", false, cx);
5177 pane.update_in(cx, |pane, window, cx| {
5178 pane.pin_tab_at(0, window, cx);
5179 });
5180 set_labeled_items(&pane, ["A*", "B", "C"], cx);
5181 pane.update_in(cx, |pane, window, cx| {
5182 pane.close_active_item(
5183 &CloseActiveItem {
5184 save_intent: None,
5185 close_pinned: false,
5186 },
5187 window,
5188 cx,
5189 )
5190 .unwrap();
5191 });
5192 // Non-pinned tab should be active
5193 assert_item_labels(&pane, ["A!", "B*", "C"], cx);
5194 }
5195
5196 #[gpui::test]
5197 async fn test_close_pinned_tab_with_non_pinned_in_different_pane(cx: &mut TestAppContext) {
5198 init_test(cx);
5199 let fs = FakeFs::new(cx.executor());
5200 let project = Project::test(fs, None, cx).await;
5201 let (workspace, cx) =
5202 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
5203
5204 // No non-pinned tabs in same pane, non-pinned tabs in another pane
5205 let pane1 = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5206 let pane2 = workspace.update_in(cx, |workspace, window, cx| {
5207 workspace.split_pane(pane1.clone(), SplitDirection::Right, window, cx)
5208 });
5209 add_labeled_item(&pane1, "A", false, cx);
5210 pane1.update_in(cx, |pane, window, cx| {
5211 pane.pin_tab_at(0, window, cx);
5212 });
5213 set_labeled_items(&pane1, ["A*"], cx);
5214 add_labeled_item(&pane2, "B", false, cx);
5215 set_labeled_items(&pane2, ["B"], cx);
5216 pane1.update_in(cx, |pane, window, cx| {
5217 pane.close_active_item(
5218 &CloseActiveItem {
5219 save_intent: None,
5220 close_pinned: false,
5221 },
5222 window,
5223 cx,
5224 )
5225 .unwrap();
5226 });
5227 // Non-pinned tab of other pane should be active
5228 assert_item_labels(&pane2, ["B*"], cx);
5229 }
5230
5231 #[gpui::test]
5232 async fn ensure_item_closing_actions_do_not_panic_when_no_items_exist(cx: &mut TestAppContext) {
5233 init_test(cx);
5234 let fs = FakeFs::new(cx.executor());
5235 let project = Project::test(fs, None, cx).await;
5236 let (workspace, cx) =
5237 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
5238
5239 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5240 assert_item_labels(&pane, [], cx);
5241
5242 pane.update_in(cx, |pane, window, cx| {
5243 pane.close_active_item(
5244 &CloseActiveItem {
5245 save_intent: None,
5246 close_pinned: false,
5247 },
5248 window,
5249 cx,
5250 )
5251 })
5252 .await
5253 .unwrap();
5254
5255 pane.update_in(cx, |pane, window, cx| {
5256 pane.close_inactive_items(
5257 &CloseInactiveItems {
5258 save_intent: None,
5259 close_pinned: false,
5260 },
5261 window,
5262 cx,
5263 )
5264 })
5265 .await
5266 .unwrap();
5267
5268 pane.update_in(cx, |pane, window, cx| {
5269 pane.close_all_items(
5270 &CloseAllItems {
5271 save_intent: None,
5272 close_pinned: false,
5273 },
5274 window,
5275 cx,
5276 )
5277 })
5278 .await
5279 .unwrap();
5280
5281 pane.update_in(cx, |pane, window, cx| {
5282 pane.close_clean_items(
5283 &CloseCleanItems {
5284 close_pinned: false,
5285 },
5286 window,
5287 cx,
5288 )
5289 })
5290 .await
5291 .unwrap();
5292
5293 pane.update_in(cx, |pane, window, cx| {
5294 pane.close_items_to_the_right_by_id(
5295 None,
5296 &CloseItemsToTheRight {
5297 close_pinned: false,
5298 },
5299 window,
5300 cx,
5301 )
5302 })
5303 .await
5304 .unwrap();
5305
5306 pane.update_in(cx, |pane, window, cx| {
5307 pane.close_items_to_the_left_by_id(
5308 None,
5309 &CloseItemsToTheLeft {
5310 close_pinned: false,
5311 },
5312 window,
5313 cx,
5314 )
5315 })
5316 .await
5317 .unwrap();
5318 }
5319
5320 fn init_test(cx: &mut TestAppContext) {
5321 cx.update(|cx| {
5322 let settings_store = SettingsStore::test(cx);
5323 cx.set_global(settings_store);
5324 theme::init(LoadThemes::JustBase, cx);
5325 crate::init_settings(cx);
5326 Project::init_settings(cx);
5327 });
5328 }
5329
5330 fn set_max_tabs(cx: &mut TestAppContext, value: Option<usize>) {
5331 cx.update_global(|store: &mut SettingsStore, cx| {
5332 store.update_user_settings::<WorkspaceSettings>(cx, |settings| {
5333 settings.max_tabs = value.map(|v| NonZero::new(v).unwrap())
5334 });
5335 });
5336 }
5337
5338 fn add_labeled_item(
5339 pane: &Entity<Pane>,
5340 label: &str,
5341 is_dirty: bool,
5342 cx: &mut VisualTestContext,
5343 ) -> Box<Entity<TestItem>> {
5344 pane.update_in(cx, |pane, window, cx| {
5345 let labeled_item =
5346 Box::new(cx.new(|cx| TestItem::new(cx).with_label(label).with_dirty(is_dirty)));
5347 pane.add_item(labeled_item.clone(), false, false, None, window, cx);
5348 labeled_item
5349 })
5350 }
5351
5352 fn set_labeled_items<const COUNT: usize>(
5353 pane: &Entity<Pane>,
5354 labels: [&str; COUNT],
5355 cx: &mut VisualTestContext,
5356 ) -> [Box<Entity<TestItem>>; COUNT] {
5357 pane.update_in(cx, |pane, window, cx| {
5358 pane.items.clear();
5359 let mut active_item_index = 0;
5360
5361 let mut index = 0;
5362 let items = labels.map(|mut label| {
5363 if label.ends_with('*') {
5364 label = label.trim_end_matches('*');
5365 active_item_index = index;
5366 }
5367
5368 let labeled_item = Box::new(cx.new(|cx| TestItem::new(cx).with_label(label)));
5369 pane.add_item(labeled_item.clone(), false, false, None, window, cx);
5370 index += 1;
5371 labeled_item
5372 });
5373
5374 pane.activate_item(active_item_index, false, false, window, cx);
5375
5376 items
5377 })
5378 }
5379
5380 // Assert the item label, with the active item label suffixed with a '*'
5381 #[track_caller]
5382 fn assert_item_labels<const COUNT: usize>(
5383 pane: &Entity<Pane>,
5384 expected_states: [&str; COUNT],
5385 cx: &mut VisualTestContext,
5386 ) {
5387 let actual_states = pane.update(cx, |pane, cx| {
5388 pane.items
5389 .iter()
5390 .enumerate()
5391 .map(|(ix, item)| {
5392 let mut state = item
5393 .to_any()
5394 .downcast::<TestItem>()
5395 .unwrap()
5396 .read(cx)
5397 .label
5398 .clone();
5399 if ix == pane.active_item_index {
5400 state.push('*');
5401 }
5402 if item.is_dirty(cx) {
5403 state.push('^');
5404 }
5405 if pane.is_tab_pinned(ix) {
5406 state.push('!');
5407 }
5408 state
5409 })
5410 .collect::<Vec<_>>()
5411 });
5412 assert_eq!(
5413 actual_states, expected_states,
5414 "pane items do not match expectation"
5415 );
5416 }
5417}