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 .on_action(cx.listener(|_, _: &menu::Cancel, window, cx| {
3330 if cx.stop_active_drag(window) {
3331 return;
3332 } else {
3333 cx.propagate();
3334 }
3335 }))
3336 .when(self.active_item().is_some() && display_tab_bar, |pane| {
3337 pane.child((self.render_tab_bar.clone())(self, window, cx))
3338 })
3339 .child({
3340 let has_worktrees = project.read(cx).visible_worktrees(cx).next().is_some();
3341 // main content
3342 div()
3343 .flex_1()
3344 .relative()
3345 .group("")
3346 .overflow_hidden()
3347 .on_drag_move::<DraggedTab>(cx.listener(Self::handle_drag_move))
3348 .on_drag_move::<DraggedSelection>(cx.listener(Self::handle_drag_move))
3349 .when(is_local, |div| {
3350 div.on_drag_move::<ExternalPaths>(cx.listener(Self::handle_drag_move))
3351 })
3352 .map(|div| {
3353 if let Some(item) = self.active_item() {
3354 div.id("pane_placeholder")
3355 .v_flex()
3356 .size_full()
3357 .overflow_hidden()
3358 .child(self.toolbar.clone())
3359 .child(item.to_any())
3360 } else {
3361 let placeholder = div
3362 .id("pane_placeholder")
3363 .h_flex()
3364 .size_full()
3365 .justify_center()
3366 .on_click(cx.listener(
3367 move |this, event: &ClickEvent, window, cx| {
3368 if event.up.click_count == 2 {
3369 window.dispatch_action(
3370 this.double_click_dispatch_action.boxed_clone(),
3371 cx,
3372 );
3373 }
3374 },
3375 ));
3376 if has_worktrees {
3377 placeholder
3378 } else {
3379 placeholder.child(
3380 Label::new("Open a file or project to get started.")
3381 .color(Color::Muted),
3382 )
3383 }
3384 }
3385 })
3386 .child(
3387 // drag target
3388 div()
3389 .invisible()
3390 .absolute()
3391 .bg(cx.theme().colors().drop_target_background)
3392 .group_drag_over::<DraggedTab>("", |style| style.visible())
3393 .group_drag_over::<DraggedSelection>("", |style| style.visible())
3394 .when(is_local, |div| {
3395 div.group_drag_over::<ExternalPaths>("", |style| style.visible())
3396 })
3397 .when_some(self.can_drop_predicate.clone(), |this, p| {
3398 this.can_drop(move |a, window, cx| p(a, window, cx))
3399 })
3400 .on_drop(cx.listener(move |this, dragged_tab, window, cx| {
3401 this.handle_tab_drop(
3402 dragged_tab,
3403 this.active_item_index(),
3404 window,
3405 cx,
3406 )
3407 }))
3408 .on_drop(cx.listener(
3409 move |this, selection: &DraggedSelection, window, cx| {
3410 this.handle_dragged_selection_drop(selection, None, window, cx)
3411 },
3412 ))
3413 .on_drop(cx.listener(move |this, paths, window, cx| {
3414 this.handle_external_paths_drop(paths, window, cx)
3415 }))
3416 .map(|div| {
3417 let size = DefiniteLength::Fraction(0.5);
3418 match self.drag_split_direction {
3419 None => div.top_0().right_0().bottom_0().left_0(),
3420 Some(SplitDirection::Up) => {
3421 div.top_0().left_0().right_0().h(size)
3422 }
3423 Some(SplitDirection::Down) => {
3424 div.left_0().bottom_0().right_0().h(size)
3425 }
3426 Some(SplitDirection::Left) => {
3427 div.top_0().left_0().bottom_0().w(size)
3428 }
3429 Some(SplitDirection::Right) => {
3430 div.top_0().bottom_0().right_0().w(size)
3431 }
3432 }
3433 }),
3434 )
3435 })
3436 .on_mouse_down(
3437 MouseButton::Navigate(NavigationDirection::Back),
3438 cx.listener(|pane, _, window, cx| {
3439 if let Some(workspace) = pane.workspace.upgrade() {
3440 let pane = cx.entity().downgrade();
3441 window.defer(cx, move |window, cx| {
3442 workspace.update(cx, |workspace, cx| {
3443 workspace.go_back(pane, window, cx).detach_and_log_err(cx)
3444 })
3445 })
3446 }
3447 }),
3448 )
3449 .on_mouse_down(
3450 MouseButton::Navigate(NavigationDirection::Forward),
3451 cx.listener(|pane, _, window, cx| {
3452 if let Some(workspace) = pane.workspace.upgrade() {
3453 let pane = cx.entity().downgrade();
3454 window.defer(cx, move |window, cx| {
3455 workspace.update(cx, |workspace, cx| {
3456 workspace
3457 .go_forward(pane, window, cx)
3458 .detach_and_log_err(cx)
3459 })
3460 })
3461 }
3462 }),
3463 )
3464 }
3465}
3466
3467impl ItemNavHistory {
3468 pub fn push<D: 'static + Send + Any>(&mut self, data: Option<D>, cx: &mut App) {
3469 if self
3470 .item
3471 .upgrade()
3472 .is_some_and(|item| item.include_in_nav_history())
3473 {
3474 self.history
3475 .push(data, self.item.clone(), self.is_preview, cx);
3476 }
3477 }
3478
3479 pub fn pop_backward(&mut self, cx: &mut App) -> Option<NavigationEntry> {
3480 self.history.pop(NavigationMode::GoingBack, cx)
3481 }
3482
3483 pub fn pop_forward(&mut self, cx: &mut App) -> Option<NavigationEntry> {
3484 self.history.pop(NavigationMode::GoingForward, cx)
3485 }
3486}
3487
3488impl NavHistory {
3489 pub fn for_each_entry(
3490 &self,
3491 cx: &App,
3492 mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
3493 ) {
3494 let borrowed_history = self.0.lock();
3495 borrowed_history
3496 .forward_stack
3497 .iter()
3498 .chain(borrowed_history.backward_stack.iter())
3499 .chain(borrowed_history.closed_stack.iter())
3500 .for_each(|entry| {
3501 if let Some(project_and_abs_path) =
3502 borrowed_history.paths_by_item.get(&entry.item.id())
3503 {
3504 f(entry, project_and_abs_path.clone());
3505 } else if let Some(item) = entry.item.upgrade() {
3506 if let Some(path) = item.project_path(cx) {
3507 f(entry, (path, None));
3508 }
3509 }
3510 })
3511 }
3512
3513 pub fn set_mode(&mut self, mode: NavigationMode) {
3514 self.0.lock().mode = mode;
3515 }
3516
3517 pub fn mode(&self) -> NavigationMode {
3518 self.0.lock().mode
3519 }
3520
3521 pub fn disable(&mut self) {
3522 self.0.lock().mode = NavigationMode::Disabled;
3523 }
3524
3525 pub fn enable(&mut self) {
3526 self.0.lock().mode = NavigationMode::Normal;
3527 }
3528
3529 pub fn pop(&mut self, mode: NavigationMode, cx: &mut App) -> Option<NavigationEntry> {
3530 let mut state = self.0.lock();
3531 let entry = match mode {
3532 NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
3533 return None;
3534 }
3535 NavigationMode::GoingBack => &mut state.backward_stack,
3536 NavigationMode::GoingForward => &mut state.forward_stack,
3537 NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
3538 }
3539 .pop_back();
3540 if entry.is_some() {
3541 state.did_update(cx);
3542 }
3543 entry
3544 }
3545
3546 pub fn push<D: 'static + Send + Any>(
3547 &mut self,
3548 data: Option<D>,
3549 item: Arc<dyn WeakItemHandle>,
3550 is_preview: bool,
3551 cx: &mut App,
3552 ) {
3553 let state = &mut *self.0.lock();
3554 match state.mode {
3555 NavigationMode::Disabled => {}
3556 NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
3557 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3558 state.backward_stack.pop_front();
3559 }
3560 state.backward_stack.push_back(NavigationEntry {
3561 item,
3562 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3563 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3564 is_preview,
3565 });
3566 state.forward_stack.clear();
3567 }
3568 NavigationMode::GoingBack => {
3569 if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3570 state.forward_stack.pop_front();
3571 }
3572 state.forward_stack.push_back(NavigationEntry {
3573 item,
3574 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3575 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3576 is_preview,
3577 });
3578 }
3579 NavigationMode::GoingForward => {
3580 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3581 state.backward_stack.pop_front();
3582 }
3583 state.backward_stack.push_back(NavigationEntry {
3584 item,
3585 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3586 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3587 is_preview,
3588 });
3589 }
3590 NavigationMode::ClosingItem => {
3591 if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3592 state.closed_stack.pop_front();
3593 }
3594 state.closed_stack.push_back(NavigationEntry {
3595 item,
3596 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3597 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3598 is_preview,
3599 });
3600 }
3601 }
3602 state.did_update(cx);
3603 }
3604
3605 pub fn remove_item(&mut self, item_id: EntityId) {
3606 let mut state = self.0.lock();
3607 state.paths_by_item.remove(&item_id);
3608 state
3609 .backward_stack
3610 .retain(|entry| entry.item.id() != item_id);
3611 state
3612 .forward_stack
3613 .retain(|entry| entry.item.id() != item_id);
3614 state
3615 .closed_stack
3616 .retain(|entry| entry.item.id() != item_id);
3617 }
3618
3619 pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option<PathBuf>)> {
3620 self.0.lock().paths_by_item.get(&item_id).cloned()
3621 }
3622}
3623
3624impl NavHistoryState {
3625 pub fn did_update(&self, cx: &mut App) {
3626 if let Some(pane) = self.pane.upgrade() {
3627 cx.defer(move |cx| {
3628 pane.update(cx, |pane, cx| pane.history_updated(cx));
3629 });
3630 }
3631 }
3632}
3633
3634fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
3635 let path = buffer_path
3636 .as_ref()
3637 .and_then(|p| {
3638 p.path
3639 .to_str()
3640 .and_then(|s| if s.is_empty() { None } else { Some(s) })
3641 })
3642 .unwrap_or("This buffer");
3643 let path = truncate_and_remove_front(path, 80);
3644 format!("{path} contains unsaved edits. Do you want to save it?")
3645}
3646
3647pub fn tab_details(items: &[Box<dyn ItemHandle>], _window: &Window, cx: &App) -> Vec<usize> {
3648 let mut tab_details = items.iter().map(|_| 0).collect::<Vec<_>>();
3649 let mut tab_descriptions = HashMap::default();
3650 let mut done = false;
3651 while !done {
3652 done = true;
3653
3654 // Store item indices by their tab description.
3655 for (ix, (item, detail)) in items.iter().zip(&tab_details).enumerate() {
3656 let description = item.tab_content_text(*detail, cx);
3657 if *detail == 0 || description != item.tab_content_text(detail - 1, cx) {
3658 tab_descriptions
3659 .entry(description)
3660 .or_insert(Vec::new())
3661 .push(ix);
3662 }
3663 }
3664
3665 // If two or more items have the same tab description, increase their level
3666 // of detail and try again.
3667 for (_, item_ixs) in tab_descriptions.drain() {
3668 if item_ixs.len() > 1 {
3669 done = false;
3670 for ix in item_ixs {
3671 tab_details[ix] += 1;
3672 }
3673 }
3674 }
3675 }
3676
3677 tab_details
3678}
3679
3680pub fn render_item_indicator(item: Box<dyn ItemHandle>, cx: &App) -> Option<Indicator> {
3681 maybe!({
3682 let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
3683 (true, _) => Color::Warning,
3684 (_, true) => Color::Accent,
3685 (false, false) => return None,
3686 };
3687
3688 Some(Indicator::dot().color(indicator_color))
3689 })
3690}
3691
3692impl Render for DraggedTab {
3693 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3694 let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
3695 let label = self.item.tab_content(
3696 TabContentParams {
3697 detail: Some(self.detail),
3698 selected: false,
3699 preview: false,
3700 deemphasized: false,
3701 },
3702 window,
3703 cx,
3704 );
3705 Tab::new("")
3706 .toggle_state(self.is_active)
3707 .child(label)
3708 .render(window, cx)
3709 .font(ui_font)
3710 }
3711}
3712
3713#[cfg(test)]
3714mod tests {
3715 use std::num::NonZero;
3716
3717 use super::*;
3718 use crate::item::test::{TestItem, TestProjectItem};
3719 use gpui::{TestAppContext, VisualTestContext};
3720 use project::FakeFs;
3721 use settings::SettingsStore;
3722 use theme::LoadThemes;
3723
3724 #[gpui::test]
3725 async fn test_remove_active_empty(cx: &mut TestAppContext) {
3726 init_test(cx);
3727 let fs = FakeFs::new(cx.executor());
3728
3729 let project = Project::test(fs, None, cx).await;
3730 let (workspace, cx) =
3731 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3732 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3733
3734 pane.update_in(cx, |pane, window, cx| {
3735 assert!(
3736 pane.close_active_item(
3737 &CloseActiveItem {
3738 save_intent: None,
3739 close_pinned: false
3740 },
3741 window,
3742 cx
3743 )
3744 .is_none()
3745 )
3746 });
3747 }
3748
3749 #[gpui::test]
3750 async fn test_add_item_capped_to_max_tabs(cx: &mut TestAppContext) {
3751 init_test(cx);
3752 let fs = FakeFs::new(cx.executor());
3753
3754 let project = Project::test(fs, None, cx).await;
3755 let (workspace, cx) =
3756 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3757 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3758
3759 for i in 0..7 {
3760 add_labeled_item(&pane, format!("{}", i).as_str(), false, cx);
3761 }
3762 set_max_tabs(cx, Some(5));
3763 add_labeled_item(&pane, "7", false, cx);
3764 // Remove items to respect the max tab cap.
3765 assert_item_labels(&pane, ["3", "4", "5", "6", "7*"], cx);
3766 pane.update_in(cx, |pane, window, cx| {
3767 pane.activate_item(0, false, false, window, cx);
3768 });
3769 add_labeled_item(&pane, "X", false, cx);
3770 // Respect activation order.
3771 assert_item_labels(&pane, ["3", "X*", "5", "6", "7"], cx);
3772
3773 for i in 0..7 {
3774 add_labeled_item(&pane, format!("D{}", i).as_str(), true, cx);
3775 }
3776 // Keeps dirty items, even over max tab cap.
3777 assert_item_labels(
3778 &pane,
3779 ["D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6*^"],
3780 cx,
3781 );
3782
3783 set_max_tabs(cx, None);
3784 for i in 0..7 {
3785 add_labeled_item(&pane, format!("N{}", i).as_str(), false, cx);
3786 }
3787 // No cap when max tabs is None.
3788 assert_item_labels(
3789 &pane,
3790 [
3791 "D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6^", "N0", "N1", "N2", "N3", "N4",
3792 "N5", "N6*",
3793 ],
3794 cx,
3795 );
3796 }
3797
3798 #[gpui::test]
3799 async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
3800 init_test(cx);
3801 let fs = FakeFs::new(cx.executor());
3802
3803 let project = Project::test(fs, None, cx).await;
3804 let (workspace, cx) =
3805 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3806 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3807
3808 // 1. Add with a destination index
3809 // a. Add before the active item
3810 set_labeled_items(&pane, ["A", "B*", "C"], cx);
3811 pane.update_in(cx, |pane, window, cx| {
3812 pane.add_item(
3813 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3814 false,
3815 false,
3816 Some(0),
3817 window,
3818 cx,
3819 );
3820 });
3821 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
3822
3823 // b. Add after the active item
3824 set_labeled_items(&pane, ["A", "B*", "C"], cx);
3825 pane.update_in(cx, |pane, window, cx| {
3826 pane.add_item(
3827 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3828 false,
3829 false,
3830 Some(2),
3831 window,
3832 cx,
3833 );
3834 });
3835 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3836
3837 // c. Add at the end of the item list (including off the length)
3838 set_labeled_items(&pane, ["A", "B*", "C"], cx);
3839 pane.update_in(cx, |pane, window, cx| {
3840 pane.add_item(
3841 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3842 false,
3843 false,
3844 Some(5),
3845 window,
3846 cx,
3847 );
3848 });
3849 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3850
3851 // 2. Add without a destination index
3852 // a. Add with active item at the start of the item list
3853 set_labeled_items(&pane, ["A*", "B", "C"], cx);
3854 pane.update_in(cx, |pane, window, cx| {
3855 pane.add_item(
3856 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3857 false,
3858 false,
3859 None,
3860 window,
3861 cx,
3862 );
3863 });
3864 set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
3865
3866 // b. Add with active item at the end of the item list
3867 set_labeled_items(&pane, ["A", "B", "C*"], cx);
3868 pane.update_in(cx, |pane, window, cx| {
3869 pane.add_item(
3870 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3871 false,
3872 false,
3873 None,
3874 window,
3875 cx,
3876 );
3877 });
3878 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3879 }
3880
3881 #[gpui::test]
3882 async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
3883 init_test(cx);
3884 let fs = FakeFs::new(cx.executor());
3885
3886 let project = Project::test(fs, None, cx).await;
3887 let (workspace, cx) =
3888 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3889 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3890
3891 // 1. Add with a destination index
3892 // 1a. Add before 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(0), window, cx);
3896 });
3897 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
3898
3899 // 1b. Add after the active item
3900 let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3901 pane.update_in(cx, |pane, window, cx| {
3902 pane.add_item(d, false, false, Some(2), window, cx);
3903 });
3904 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3905
3906 // 1c. Add at the end of the item list (including off the length)
3907 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3908 pane.update_in(cx, |pane, window, cx| {
3909 pane.add_item(a, false, false, Some(5), window, cx);
3910 });
3911 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3912
3913 // 1d. Add same item to active index
3914 let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3915 pane.update_in(cx, |pane, window, cx| {
3916 pane.add_item(b, false, false, Some(1), window, cx);
3917 });
3918 assert_item_labels(&pane, ["A", "B*", "C"], cx);
3919
3920 // 1e. Add item to index after same item in last position
3921 let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3922 pane.update_in(cx, |pane, window, cx| {
3923 pane.add_item(c, false, false, Some(2), window, cx);
3924 });
3925 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3926
3927 // 2. Add without a destination index
3928 // 2a. Add with active item at the start of the item list
3929 let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
3930 pane.update_in(cx, |pane, window, cx| {
3931 pane.add_item(d, false, false, None, window, cx);
3932 });
3933 assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
3934
3935 // 2b. Add with active item at the end of the item list
3936 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
3937 pane.update_in(cx, |pane, window, cx| {
3938 pane.add_item(a, false, false, None, window, cx);
3939 });
3940 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3941
3942 // 2c. Add active item to active item at end of list
3943 let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
3944 pane.update_in(cx, |pane, window, cx| {
3945 pane.add_item(c, false, false, None, window, cx);
3946 });
3947 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3948
3949 // 2d. Add active item to active item at start of list
3950 let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
3951 pane.update_in(cx, |pane, window, cx| {
3952 pane.add_item(a, false, false, None, window, cx);
3953 });
3954 assert_item_labels(&pane, ["A*", "B", "C"], cx);
3955 }
3956
3957 #[gpui::test]
3958 async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
3959 init_test(cx);
3960 let fs = FakeFs::new(cx.executor());
3961
3962 let project = Project::test(fs, None, cx).await;
3963 let (workspace, cx) =
3964 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3965 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3966
3967 // singleton view
3968 pane.update_in(cx, |pane, window, cx| {
3969 pane.add_item(
3970 Box::new(cx.new(|cx| {
3971 TestItem::new(cx)
3972 .with_singleton(true)
3973 .with_label("buffer 1")
3974 .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
3975 })),
3976 false,
3977 false,
3978 None,
3979 window,
3980 cx,
3981 );
3982 });
3983 assert_item_labels(&pane, ["buffer 1*"], cx);
3984
3985 // new singleton view with the same project entry
3986 pane.update_in(cx, |pane, window, cx| {
3987 pane.add_item(
3988 Box::new(cx.new(|cx| {
3989 TestItem::new(cx)
3990 .with_singleton(true)
3991 .with_label("buffer 1")
3992 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3993 })),
3994 false,
3995 false,
3996 None,
3997 window,
3998 cx,
3999 );
4000 });
4001 assert_item_labels(&pane, ["buffer 1*"], cx);
4002
4003 // new singleton view with different project entry
4004 pane.update_in(cx, |pane, window, cx| {
4005 pane.add_item(
4006 Box::new(cx.new(|cx| {
4007 TestItem::new(cx)
4008 .with_singleton(true)
4009 .with_label("buffer 2")
4010 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
4011 })),
4012 false,
4013 false,
4014 None,
4015 window,
4016 cx,
4017 );
4018 });
4019 assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
4020
4021 // new multibuffer view with the same project entry
4022 pane.update_in(cx, |pane, window, cx| {
4023 pane.add_item(
4024 Box::new(cx.new(|cx| {
4025 TestItem::new(cx)
4026 .with_singleton(false)
4027 .with_label("multibuffer 1")
4028 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4029 })),
4030 false,
4031 false,
4032 None,
4033 window,
4034 cx,
4035 );
4036 });
4037 assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
4038
4039 // another multibuffer view with the same project entry
4040 pane.update_in(cx, |pane, window, cx| {
4041 pane.add_item(
4042 Box::new(cx.new(|cx| {
4043 TestItem::new(cx)
4044 .with_singleton(false)
4045 .with_label("multibuffer 1b")
4046 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4047 })),
4048 false,
4049 false,
4050 None,
4051 window,
4052 cx,
4053 );
4054 });
4055 assert_item_labels(
4056 &pane,
4057 ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
4058 cx,
4059 );
4060 }
4061
4062 #[gpui::test]
4063 async fn test_remove_item_ordering_history(cx: &mut TestAppContext) {
4064 init_test(cx);
4065 let fs = FakeFs::new(cx.executor());
4066
4067 let project = Project::test(fs, None, cx).await;
4068 let (workspace, cx) =
4069 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4070 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4071
4072 add_labeled_item(&pane, "A", false, cx);
4073 add_labeled_item(&pane, "B", false, cx);
4074 add_labeled_item(&pane, "C", false, cx);
4075 add_labeled_item(&pane, "D", false, cx);
4076 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4077
4078 pane.update_in(cx, |pane, window, cx| {
4079 pane.activate_item(1, false, false, window, cx)
4080 });
4081 add_labeled_item(&pane, "1", false, cx);
4082 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
4083
4084 pane.update_in(cx, |pane, window, cx| {
4085 pane.close_active_item(
4086 &CloseActiveItem {
4087 save_intent: None,
4088 close_pinned: false,
4089 },
4090 window,
4091 cx,
4092 )
4093 })
4094 .unwrap()
4095 .await
4096 .unwrap();
4097 assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
4098
4099 pane.update_in(cx, |pane, window, cx| {
4100 pane.activate_item(3, false, false, window, cx)
4101 });
4102 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4103
4104 pane.update_in(cx, |pane, window, cx| {
4105 pane.close_active_item(
4106 &CloseActiveItem {
4107 save_intent: None,
4108 close_pinned: false,
4109 },
4110 window,
4111 cx,
4112 )
4113 })
4114 .unwrap()
4115 .await
4116 .unwrap();
4117 assert_item_labels(&pane, ["A", "B*", "C"], cx);
4118
4119 pane.update_in(cx, |pane, window, cx| {
4120 pane.close_active_item(
4121 &CloseActiveItem {
4122 save_intent: None,
4123 close_pinned: false,
4124 },
4125 window,
4126 cx,
4127 )
4128 })
4129 .unwrap()
4130 .await
4131 .unwrap();
4132 assert_item_labels(&pane, ["A", "C*"], cx);
4133
4134 pane.update_in(cx, |pane, window, cx| {
4135 pane.close_active_item(
4136 &CloseActiveItem {
4137 save_intent: None,
4138 close_pinned: false,
4139 },
4140 window,
4141 cx,
4142 )
4143 })
4144 .unwrap()
4145 .await
4146 .unwrap();
4147 assert_item_labels(&pane, ["A*"], cx);
4148 }
4149
4150 #[gpui::test]
4151 async fn test_remove_item_ordering_neighbour(cx: &mut TestAppContext) {
4152 init_test(cx);
4153 cx.update_global::<SettingsStore, ()>(|s, cx| {
4154 s.update_user_settings::<ItemSettings>(cx, |s| {
4155 s.activate_on_close = Some(ActivateOnClose::Neighbour);
4156 });
4157 });
4158 let fs = FakeFs::new(cx.executor());
4159
4160 let project = Project::test(fs, None, cx).await;
4161 let (workspace, cx) =
4162 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4163 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4164
4165 add_labeled_item(&pane, "A", false, cx);
4166 add_labeled_item(&pane, "B", false, cx);
4167 add_labeled_item(&pane, "C", false, cx);
4168 add_labeled_item(&pane, "D", false, cx);
4169 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4170
4171 pane.update_in(cx, |pane, window, cx| {
4172 pane.activate_item(1, false, false, window, cx)
4173 });
4174 add_labeled_item(&pane, "1", false, cx);
4175 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
4176
4177 pane.update_in(cx, |pane, window, cx| {
4178 pane.close_active_item(
4179 &CloseActiveItem {
4180 save_intent: None,
4181 close_pinned: false,
4182 },
4183 window,
4184 cx,
4185 )
4186 })
4187 .unwrap()
4188 .await
4189 .unwrap();
4190 assert_item_labels(&pane, ["A", "B", "C*", "D"], cx);
4191
4192 pane.update_in(cx, |pane, window, cx| {
4193 pane.activate_item(3, false, false, window, cx)
4194 });
4195 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4196
4197 pane.update_in(cx, |pane, window, cx| {
4198 pane.close_active_item(
4199 &CloseActiveItem {
4200 save_intent: None,
4201 close_pinned: false,
4202 },
4203 window,
4204 cx,
4205 )
4206 })
4207 .unwrap()
4208 .await
4209 .unwrap();
4210 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4211
4212 pane.update_in(cx, |pane, window, cx| {
4213 pane.close_active_item(
4214 &CloseActiveItem {
4215 save_intent: None,
4216 close_pinned: false,
4217 },
4218 window,
4219 cx,
4220 )
4221 })
4222 .unwrap()
4223 .await
4224 .unwrap();
4225 assert_item_labels(&pane, ["A", "B*"], cx);
4226
4227 pane.update_in(cx, |pane, window, cx| {
4228 pane.close_active_item(
4229 &CloseActiveItem {
4230 save_intent: None,
4231 close_pinned: false,
4232 },
4233 window,
4234 cx,
4235 )
4236 })
4237 .unwrap()
4238 .await
4239 .unwrap();
4240 assert_item_labels(&pane, ["A*"], cx);
4241 }
4242
4243 #[gpui::test]
4244 async fn test_remove_item_ordering_left_neighbour(cx: &mut TestAppContext) {
4245 init_test(cx);
4246 cx.update_global::<SettingsStore, ()>(|s, cx| {
4247 s.update_user_settings::<ItemSettings>(cx, |s| {
4248 s.activate_on_close = Some(ActivateOnClose::LeftNeighbour);
4249 });
4250 });
4251 let fs = FakeFs::new(cx.executor());
4252
4253 let project = Project::test(fs, None, cx).await;
4254 let (workspace, cx) =
4255 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4256 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4257
4258 add_labeled_item(&pane, "A", false, cx);
4259 add_labeled_item(&pane, "B", false, cx);
4260 add_labeled_item(&pane, "C", false, cx);
4261 add_labeled_item(&pane, "D", false, cx);
4262 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4263
4264 pane.update_in(cx, |pane, window, cx| {
4265 pane.activate_item(1, false, false, window, cx)
4266 });
4267 add_labeled_item(&pane, "1", false, cx);
4268 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
4269
4270 pane.update_in(cx, |pane, window, cx| {
4271 pane.close_active_item(
4272 &CloseActiveItem {
4273 save_intent: None,
4274 close_pinned: false,
4275 },
4276 window,
4277 cx,
4278 )
4279 })
4280 .unwrap()
4281 .await
4282 .unwrap();
4283 assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
4284
4285 pane.update_in(cx, |pane, window, cx| {
4286 pane.activate_item(3, false, false, window, cx)
4287 });
4288 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4289
4290 pane.update_in(cx, |pane, window, cx| {
4291 pane.close_active_item(
4292 &CloseActiveItem {
4293 save_intent: None,
4294 close_pinned: false,
4295 },
4296 window,
4297 cx,
4298 )
4299 })
4300 .unwrap()
4301 .await
4302 .unwrap();
4303 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4304
4305 pane.update_in(cx, |pane, window, cx| {
4306 pane.activate_item(0, false, false, window, cx)
4307 });
4308 assert_item_labels(&pane, ["A*", "B", "C"], cx);
4309
4310 pane.update_in(cx, |pane, window, cx| {
4311 pane.close_active_item(
4312 &CloseActiveItem {
4313 save_intent: None,
4314 close_pinned: false,
4315 },
4316 window,
4317 cx,
4318 )
4319 })
4320 .unwrap()
4321 .await
4322 .unwrap();
4323 assert_item_labels(&pane, ["B*", "C"], cx);
4324
4325 pane.update_in(cx, |pane, window, cx| {
4326 pane.close_active_item(
4327 &CloseActiveItem {
4328 save_intent: None,
4329 close_pinned: false,
4330 },
4331 window,
4332 cx,
4333 )
4334 })
4335 .unwrap()
4336 .await
4337 .unwrap();
4338 assert_item_labels(&pane, ["C*"], cx);
4339 }
4340
4341 #[gpui::test]
4342 async fn test_close_inactive_items(cx: &mut TestAppContext) {
4343 init_test(cx);
4344 let fs = FakeFs::new(cx.executor());
4345
4346 let project = Project::test(fs, None, cx).await;
4347 let (workspace, cx) =
4348 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4349 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4350
4351 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
4352
4353 pane.update_in(cx, |pane, window, cx| {
4354 pane.close_inactive_items(
4355 &CloseInactiveItems {
4356 save_intent: None,
4357 close_pinned: false,
4358 },
4359 window,
4360 cx,
4361 )
4362 })
4363 .unwrap()
4364 .await
4365 .unwrap();
4366 assert_item_labels(&pane, ["C*"], cx);
4367 }
4368
4369 #[gpui::test]
4370 async fn test_close_clean_items(cx: &mut TestAppContext) {
4371 init_test(cx);
4372 let fs = FakeFs::new(cx.executor());
4373
4374 let project = Project::test(fs, None, cx).await;
4375 let (workspace, cx) =
4376 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4377 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4378
4379 add_labeled_item(&pane, "A", true, cx);
4380 add_labeled_item(&pane, "B", false, cx);
4381 add_labeled_item(&pane, "C", true, cx);
4382 add_labeled_item(&pane, "D", false, cx);
4383 add_labeled_item(&pane, "E", false, cx);
4384 assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
4385
4386 pane.update_in(cx, |pane, window, cx| {
4387 pane.close_clean_items(
4388 &CloseCleanItems {
4389 close_pinned: false,
4390 },
4391 window,
4392 cx,
4393 )
4394 })
4395 .unwrap()
4396 .await
4397 .unwrap();
4398 assert_item_labels(&pane, ["A^", "C*^"], cx);
4399 }
4400
4401 #[gpui::test]
4402 async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
4403 init_test(cx);
4404 let fs = FakeFs::new(cx.executor());
4405
4406 let project = Project::test(fs, None, cx).await;
4407 let (workspace, cx) =
4408 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4409 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4410
4411 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
4412
4413 pane.update_in(cx, |pane, window, cx| {
4414 pane.close_items_to_the_left(
4415 &CloseItemsToTheLeft {
4416 close_pinned: false,
4417 },
4418 window,
4419 cx,
4420 )
4421 })
4422 .unwrap()
4423 .await
4424 .unwrap();
4425 assert_item_labels(&pane, ["C*", "D", "E"], cx);
4426 }
4427
4428 #[gpui::test]
4429 async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
4430 init_test(cx);
4431 let fs = FakeFs::new(cx.executor());
4432
4433 let project = Project::test(fs, None, cx).await;
4434 let (workspace, cx) =
4435 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4436 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4437
4438 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
4439
4440 pane.update_in(cx, |pane, window, cx| {
4441 pane.close_items_to_the_right(
4442 &CloseItemsToTheRight {
4443 close_pinned: false,
4444 },
4445 window,
4446 cx,
4447 )
4448 })
4449 .unwrap()
4450 .await
4451 .unwrap();
4452 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4453 }
4454
4455 #[gpui::test]
4456 async fn test_close_all_items(cx: &mut TestAppContext) {
4457 init_test(cx);
4458 let fs = FakeFs::new(cx.executor());
4459
4460 let project = Project::test(fs, None, cx).await;
4461 let (workspace, cx) =
4462 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4463 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4464
4465 let item_a = add_labeled_item(&pane, "A", false, cx);
4466 add_labeled_item(&pane, "B", false, cx);
4467 add_labeled_item(&pane, "C", false, cx);
4468 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4469
4470 pane.update_in(cx, |pane, window, cx| {
4471 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4472 pane.pin_tab_at(ix, window, cx);
4473 pane.close_all_items(
4474 &CloseAllItems {
4475 save_intent: None,
4476 close_pinned: false,
4477 },
4478 window,
4479 cx,
4480 )
4481 })
4482 .unwrap()
4483 .await
4484 .unwrap();
4485 assert_item_labels(&pane, ["A*"], cx);
4486
4487 pane.update_in(cx, |pane, window, cx| {
4488 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4489 pane.unpin_tab_at(ix, window, cx);
4490 pane.close_all_items(
4491 &CloseAllItems {
4492 save_intent: None,
4493 close_pinned: false,
4494 },
4495 window,
4496 cx,
4497 )
4498 })
4499 .unwrap()
4500 .await
4501 .unwrap();
4502
4503 assert_item_labels(&pane, [], cx);
4504
4505 add_labeled_item(&pane, "A", true, cx).update(cx, |item, cx| {
4506 item.project_items
4507 .push(TestProjectItem::new_dirty(1, "A.txt", cx))
4508 });
4509 add_labeled_item(&pane, "B", true, cx).update(cx, |item, cx| {
4510 item.project_items
4511 .push(TestProjectItem::new_dirty(2, "B.txt", cx))
4512 });
4513 add_labeled_item(&pane, "C", true, cx).update(cx, |item, cx| {
4514 item.project_items
4515 .push(TestProjectItem::new_dirty(3, "C.txt", cx))
4516 });
4517 assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
4518
4519 let save = pane
4520 .update_in(cx, |pane, window, cx| {
4521 pane.close_all_items(
4522 &CloseAllItems {
4523 save_intent: None,
4524 close_pinned: false,
4525 },
4526 window,
4527 cx,
4528 )
4529 })
4530 .unwrap();
4531
4532 cx.executor().run_until_parked();
4533 cx.simulate_prompt_answer("Save all");
4534 save.await.unwrap();
4535 assert_item_labels(&pane, [], cx);
4536
4537 add_labeled_item(&pane, "A", true, cx);
4538 add_labeled_item(&pane, "B", true, cx);
4539 add_labeled_item(&pane, "C", true, cx);
4540 assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
4541 let save = pane
4542 .update_in(cx, |pane, window, cx| {
4543 pane.close_all_items(
4544 &CloseAllItems {
4545 save_intent: None,
4546 close_pinned: false,
4547 },
4548 window,
4549 cx,
4550 )
4551 })
4552 .unwrap();
4553
4554 cx.executor().run_until_parked();
4555 cx.simulate_prompt_answer("Discard all");
4556 save.await.unwrap();
4557 assert_item_labels(&pane, [], cx);
4558 }
4559
4560 #[gpui::test]
4561 async fn test_close_with_save_intent(cx: &mut TestAppContext) {
4562 init_test(cx);
4563 let fs = FakeFs::new(cx.executor());
4564
4565 let project = Project::test(fs, None, cx).await;
4566 let (workspace, cx) =
4567 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
4568 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4569
4570 let a = cx.update(|_, cx| TestProjectItem::new_dirty(1, "A.txt", cx));
4571 let b = cx.update(|_, cx| TestProjectItem::new_dirty(1, "B.txt", cx));
4572 let c = cx.update(|_, cx| TestProjectItem::new_dirty(1, "C.txt", cx));
4573
4574 add_labeled_item(&pane, "AB", true, cx).update(cx, |item, _| {
4575 item.project_items.push(a.clone());
4576 item.project_items.push(b.clone());
4577 });
4578 add_labeled_item(&pane, "C", true, cx)
4579 .update(cx, |item, _| item.project_items.push(c.clone()));
4580 assert_item_labels(&pane, ["AB^", "C*^"], cx);
4581
4582 pane.update_in(cx, |pane, window, cx| {
4583 pane.close_all_items(
4584 &CloseAllItems {
4585 save_intent: Some(SaveIntent::Save),
4586 close_pinned: false,
4587 },
4588 window,
4589 cx,
4590 )
4591 })
4592 .unwrap()
4593 .await
4594 .unwrap();
4595
4596 assert_item_labels(&pane, [], cx);
4597 cx.update(|_, cx| {
4598 assert!(!a.read(cx).is_dirty);
4599 assert!(!b.read(cx).is_dirty);
4600 assert!(!c.read(cx).is_dirty);
4601 });
4602 }
4603
4604 #[gpui::test]
4605 async fn test_close_all_items_including_pinned(cx: &mut TestAppContext) {
4606 init_test(cx);
4607 let fs = FakeFs::new(cx.executor());
4608
4609 let project = Project::test(fs, None, cx).await;
4610 let (workspace, cx) =
4611 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
4612 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4613
4614 let item_a = add_labeled_item(&pane, "A", false, cx);
4615 add_labeled_item(&pane, "B", false, cx);
4616 add_labeled_item(&pane, "C", false, cx);
4617 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4618
4619 pane.update_in(cx, |pane, window, cx| {
4620 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4621 pane.pin_tab_at(ix, window, cx);
4622 pane.close_all_items(
4623 &CloseAllItems {
4624 save_intent: None,
4625 close_pinned: true,
4626 },
4627 window,
4628 cx,
4629 )
4630 })
4631 .unwrap()
4632 .await
4633 .unwrap();
4634 assert_item_labels(&pane, [], cx);
4635 }
4636
4637 #[gpui::test]
4638 async fn test_close_pinned_tab_with_non_pinned_in_same_pane(cx: &mut TestAppContext) {
4639 init_test(cx);
4640 let fs = FakeFs::new(cx.executor());
4641 let project = Project::test(fs, None, cx).await;
4642 let (workspace, cx) =
4643 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
4644
4645 // Non-pinned tabs in same pane
4646 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4647 add_labeled_item(&pane, "A", false, cx);
4648 add_labeled_item(&pane, "B", false, cx);
4649 add_labeled_item(&pane, "C", false, cx);
4650 pane.update_in(cx, |pane, window, cx| {
4651 pane.pin_tab_at(0, window, cx);
4652 });
4653 set_labeled_items(&pane, ["A*", "B", "C"], cx);
4654 pane.update_in(cx, |pane, window, cx| {
4655 pane.close_active_item(
4656 &CloseActiveItem {
4657 save_intent: None,
4658 close_pinned: false,
4659 },
4660 window,
4661 cx,
4662 );
4663 });
4664 // Non-pinned tab should be active
4665 assert_item_labels(&pane, ["A", "B*", "C"], cx);
4666 }
4667
4668 #[gpui::test]
4669 async fn test_close_pinned_tab_with_non_pinned_in_different_pane(cx: &mut TestAppContext) {
4670 init_test(cx);
4671 let fs = FakeFs::new(cx.executor());
4672 let project = Project::test(fs, None, cx).await;
4673 let (workspace, cx) =
4674 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
4675
4676 // No non-pinned tabs in same pane, non-pinned tabs in another pane
4677 let pane1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4678 let pane2 = workspace.update_in(cx, |workspace, window, cx| {
4679 workspace.split_pane(pane1.clone(), SplitDirection::Right, window, cx)
4680 });
4681 add_labeled_item(&pane1, "A", false, cx);
4682 pane1.update_in(cx, |pane, window, cx| {
4683 pane.pin_tab_at(0, window, cx);
4684 });
4685 set_labeled_items(&pane1, ["A*"], cx);
4686 add_labeled_item(&pane2, "B", false, cx);
4687 set_labeled_items(&pane2, ["B"], cx);
4688 pane1.update_in(cx, |pane, window, cx| {
4689 pane.close_active_item(
4690 &CloseActiveItem {
4691 save_intent: None,
4692 close_pinned: false,
4693 },
4694 window,
4695 cx,
4696 );
4697 });
4698 // Non-pinned tab of other pane should be active
4699 assert_item_labels(&pane2, ["B*"], cx);
4700 }
4701
4702 fn init_test(cx: &mut TestAppContext) {
4703 cx.update(|cx| {
4704 let settings_store = SettingsStore::test(cx);
4705 cx.set_global(settings_store);
4706 theme::init(LoadThemes::JustBase, cx);
4707 crate::init_settings(cx);
4708 Project::init_settings(cx);
4709 });
4710 }
4711
4712 fn set_max_tabs(cx: &mut TestAppContext, value: Option<usize>) {
4713 cx.update_global(|store: &mut SettingsStore, cx| {
4714 store.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4715 settings.max_tabs = value.map(|v| NonZero::new(v).unwrap())
4716 });
4717 });
4718 }
4719
4720 fn add_labeled_item(
4721 pane: &Entity<Pane>,
4722 label: &str,
4723 is_dirty: bool,
4724 cx: &mut VisualTestContext,
4725 ) -> Box<Entity<TestItem>> {
4726 pane.update_in(cx, |pane, window, cx| {
4727 let labeled_item =
4728 Box::new(cx.new(|cx| TestItem::new(cx).with_label(label).with_dirty(is_dirty)));
4729 pane.add_item(labeled_item.clone(), false, false, None, window, cx);
4730 labeled_item
4731 })
4732 }
4733
4734 fn set_labeled_items<const COUNT: usize>(
4735 pane: &Entity<Pane>,
4736 labels: [&str; COUNT],
4737 cx: &mut VisualTestContext,
4738 ) -> [Box<Entity<TestItem>>; COUNT] {
4739 pane.update_in(cx, |pane, window, cx| {
4740 pane.items.clear();
4741 let mut active_item_index = 0;
4742
4743 let mut index = 0;
4744 let items = labels.map(|mut label| {
4745 if label.ends_with('*') {
4746 label = label.trim_end_matches('*');
4747 active_item_index = index;
4748 }
4749
4750 let labeled_item = Box::new(cx.new(|cx| TestItem::new(cx).with_label(label)));
4751 pane.add_item(labeled_item.clone(), false, false, None, window, cx);
4752 index += 1;
4753 labeled_item
4754 });
4755
4756 pane.activate_item(active_item_index, false, false, window, cx);
4757
4758 items
4759 })
4760 }
4761
4762 // Assert the item label, with the active item label suffixed with a '*'
4763 #[track_caller]
4764 fn assert_item_labels<const COUNT: usize>(
4765 pane: &Entity<Pane>,
4766 expected_states: [&str; COUNT],
4767 cx: &mut VisualTestContext,
4768 ) {
4769 let actual_states = pane.update(cx, |pane, cx| {
4770 pane.items
4771 .iter()
4772 .enumerate()
4773 .map(|(ix, item)| {
4774 let mut state = item
4775 .to_any()
4776 .downcast::<TestItem>()
4777 .unwrap()
4778 .read(cx)
4779 .label
4780 .clone();
4781 if ix == pane.active_item_index {
4782 state.push('*');
4783 }
4784 if item.is_dirty(cx) {
4785 state.push('^');
4786 }
4787 state
4788 })
4789 .collect::<Vec<_>>()
4790 });
4791 assert_eq!(
4792 actual_states, expected_states,
4793 "pane items do not match expectation"
4794 );
4795 }
4796}