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