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