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