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