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