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