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