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 let has_active_unpinned_tab = self.active_item_index >= self.pinned_tab_count;
2681 h_flex()
2682 .children(pinned_tabs)
2683 .when(is_scrollable && is_scrolled, |this| {
2684 this.when(has_active_unpinned_tab, |this| this.border_r_2())
2685 .when(!has_active_unpinned_tab, |this| this.border_r_1())
2686 .border_color(cx.theme().colors().border)
2687 })
2688 }))
2689 .child(
2690 h_flex()
2691 .id("unpinned tabs")
2692 .overflow_x_scroll()
2693 .w_full()
2694 .track_scroll(&self.tab_bar_scroll_handle)
2695 .children(unpinned_tabs)
2696 .child(
2697 div()
2698 .id("tab_bar_drop_target")
2699 .min_w_6()
2700 // HACK: This empty child is currently necessary to force the drop target to appear
2701 // despite us setting a min width above.
2702 .child("")
2703 .h_full()
2704 .flex_grow()
2705 .drag_over::<DraggedTab>(|bar, _, _, cx| {
2706 bar.bg(cx.theme().colors().drop_target_background)
2707 })
2708 .drag_over::<DraggedSelection>(|bar, _, _, cx| {
2709 bar.bg(cx.theme().colors().drop_target_background)
2710 })
2711 .on_drop(cx.listener(
2712 move |this, dragged_tab: &DraggedTab, window, cx| {
2713 this.drag_split_direction = None;
2714 this.handle_tab_drop(dragged_tab, this.items.len(), window, cx)
2715 },
2716 ))
2717 .on_drop(cx.listener(
2718 move |this, selection: &DraggedSelection, window, cx| {
2719 this.drag_split_direction = None;
2720 this.handle_project_entry_drop(
2721 &selection.active_selection.entry_id,
2722 Some(tab_count),
2723 window,
2724 cx,
2725 )
2726 },
2727 ))
2728 .on_drop(cx.listener(move |this, paths, window, cx| {
2729 this.drag_split_direction = None;
2730 this.handle_external_paths_drop(paths, window, cx)
2731 }))
2732 .on_click(cx.listener(move |this, event: &ClickEvent, window, cx| {
2733 if event.up.click_count == 2 {
2734 window.dispatch_action(
2735 this.double_click_dispatch_action.boxed_clone(),
2736 cx,
2737 );
2738 }
2739 })),
2740 ),
2741 )
2742 .into_any_element()
2743 }
2744
2745 pub fn render_menu_overlay(menu: &Entity<ContextMenu>) -> Div {
2746 div().absolute().bottom_0().right_0().size_0().child(
2747 deferred(anchored().anchor(Corner::TopRight).child(menu.clone())).with_priority(1),
2748 )
2749 }
2750
2751 pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut Context<Self>) {
2752 self.zoomed = zoomed;
2753 cx.notify();
2754 }
2755
2756 pub fn is_zoomed(&self) -> bool {
2757 self.zoomed
2758 }
2759
2760 fn handle_drag_move<T: 'static>(
2761 &mut self,
2762 event: &DragMoveEvent<T>,
2763 window: &mut Window,
2764 cx: &mut Context<Self>,
2765 ) {
2766 let can_split_predicate = self.can_split_predicate.take();
2767 let can_split = match &can_split_predicate {
2768 Some(can_split_predicate) => {
2769 can_split_predicate(self, event.dragged_item(), window, cx)
2770 }
2771 None => false,
2772 };
2773 self.can_split_predicate = can_split_predicate;
2774 if !can_split {
2775 return;
2776 }
2777
2778 let rect = event.bounds.size;
2779
2780 let size = event.bounds.size.width.min(event.bounds.size.height)
2781 * WorkspaceSettings::get_global(cx).drop_target_size;
2782
2783 let relative_cursor = Point::new(
2784 event.event.position.x - event.bounds.left(),
2785 event.event.position.y - event.bounds.top(),
2786 );
2787
2788 let direction = if relative_cursor.x < size
2789 || relative_cursor.x > rect.width - size
2790 || relative_cursor.y < size
2791 || relative_cursor.y > rect.height - size
2792 {
2793 [
2794 SplitDirection::Up,
2795 SplitDirection::Right,
2796 SplitDirection::Down,
2797 SplitDirection::Left,
2798 ]
2799 .iter()
2800 .min_by_key(|side| match side {
2801 SplitDirection::Up => relative_cursor.y,
2802 SplitDirection::Right => rect.width - relative_cursor.x,
2803 SplitDirection::Down => rect.height - relative_cursor.y,
2804 SplitDirection::Left => relative_cursor.x,
2805 })
2806 .cloned()
2807 } else {
2808 None
2809 };
2810
2811 if direction != self.drag_split_direction {
2812 self.drag_split_direction = direction;
2813 }
2814 }
2815
2816 pub fn handle_tab_drop(
2817 &mut self,
2818 dragged_tab: &DraggedTab,
2819 ix: usize,
2820 window: &mut Window,
2821 cx: &mut Context<Self>,
2822 ) {
2823 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2824 if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_tab, window, cx) {
2825 return;
2826 }
2827 }
2828 let mut to_pane = cx.entity().clone();
2829 let split_direction = self.drag_split_direction;
2830 let item_id = dragged_tab.item.item_id();
2831 if let Some(preview_item_id) = self.preview_item_id {
2832 if item_id == preview_item_id {
2833 self.set_preview_item_id(None, cx);
2834 }
2835 }
2836
2837 let from_pane = dragged_tab.pane.clone();
2838 self.workspace
2839 .update(cx, |_, cx| {
2840 cx.defer_in(window, move |workspace, window, cx| {
2841 if let Some(split_direction) = split_direction {
2842 to_pane = workspace.split_pane(to_pane, split_direction, window, cx);
2843 }
2844 let old_ix = from_pane.read(cx).index_for_item_id(item_id);
2845 let old_len = to_pane.read(cx).items.len();
2846 move_item(&from_pane, &to_pane, item_id, ix, window, cx);
2847 if to_pane == from_pane {
2848 if let Some(old_index) = old_ix {
2849 to_pane.update(cx, |this, _| {
2850 if old_index < this.pinned_tab_count
2851 && (ix == this.items.len() || ix > this.pinned_tab_count)
2852 {
2853 this.pinned_tab_count -= 1;
2854 } else if this.has_pinned_tabs()
2855 && old_index >= this.pinned_tab_count
2856 && ix < this.pinned_tab_count
2857 {
2858 this.pinned_tab_count += 1;
2859 }
2860 });
2861 }
2862 } else {
2863 to_pane.update(cx, |this, _| {
2864 if this.items.len() > old_len // Did we not deduplicate on drag?
2865 && this.has_pinned_tabs()
2866 && ix < this.pinned_tab_count
2867 {
2868 this.pinned_tab_count += 1;
2869 }
2870 });
2871 from_pane.update(cx, |this, _| {
2872 if let Some(index) = old_ix {
2873 if this.pinned_tab_count > index {
2874 this.pinned_tab_count -= 1;
2875 }
2876 }
2877 })
2878 }
2879 });
2880 })
2881 .log_err();
2882 }
2883
2884 fn handle_dragged_selection_drop(
2885 &mut self,
2886 dragged_selection: &DraggedSelection,
2887 dragged_onto: Option<usize>,
2888 window: &mut Window,
2889 cx: &mut Context<Self>,
2890 ) {
2891 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2892 if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_selection, window, cx)
2893 {
2894 return;
2895 }
2896 }
2897 self.handle_project_entry_drop(
2898 &dragged_selection.active_selection.entry_id,
2899 dragged_onto,
2900 window,
2901 cx,
2902 );
2903 }
2904
2905 fn handle_project_entry_drop(
2906 &mut self,
2907 project_entry_id: &ProjectEntryId,
2908 target: Option<usize>,
2909 window: &mut Window,
2910 cx: &mut Context<Self>,
2911 ) {
2912 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2913 if let ControlFlow::Break(()) = custom_drop_handle(self, project_entry_id, window, cx) {
2914 return;
2915 }
2916 }
2917 let mut to_pane = cx.entity().clone();
2918 let split_direction = self.drag_split_direction;
2919 let project_entry_id = *project_entry_id;
2920 self.workspace
2921 .update(cx, |_, cx| {
2922 cx.defer_in(window, move |workspace, window, cx| {
2923 if let Some(path) = workspace
2924 .project()
2925 .read(cx)
2926 .path_for_entry(project_entry_id, cx)
2927 {
2928 let load_path_task = workspace.load_path(path, window, cx);
2929 cx.spawn_in(window, async move |workspace, cx| {
2930 if let Some((project_entry_id, build_item)) =
2931 load_path_task.await.notify_async_err(cx)
2932 {
2933 let (to_pane, new_item_handle) = workspace
2934 .update_in(cx, |workspace, window, cx| {
2935 if let Some(split_direction) = split_direction {
2936 to_pane = workspace.split_pane(
2937 to_pane,
2938 split_direction,
2939 window,
2940 cx,
2941 );
2942 }
2943 let new_item_handle = to_pane.update(cx, |pane, cx| {
2944 pane.open_item(
2945 project_entry_id,
2946 true,
2947 false,
2948 true,
2949 target,
2950 window,
2951 cx,
2952 build_item,
2953 )
2954 });
2955 (to_pane, new_item_handle)
2956 })
2957 .log_err()?;
2958 to_pane
2959 .update_in(cx, |this, window, cx| {
2960 let Some(index) = this.index_for_item(&*new_item_handle)
2961 else {
2962 return;
2963 };
2964
2965 if target.map_or(false, |target| this.is_tab_pinned(target))
2966 {
2967 this.pin_tab_at(index, window, cx);
2968 }
2969 })
2970 .ok()?
2971 }
2972 Some(())
2973 })
2974 .detach();
2975 };
2976 });
2977 })
2978 .log_err();
2979 }
2980
2981 fn handle_external_paths_drop(
2982 &mut self,
2983 paths: &ExternalPaths,
2984 window: &mut Window,
2985 cx: &mut Context<Self>,
2986 ) {
2987 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2988 if let ControlFlow::Break(()) = custom_drop_handle(self, paths, window, cx) {
2989 return;
2990 }
2991 }
2992 let mut to_pane = cx.entity().clone();
2993 let mut split_direction = self.drag_split_direction;
2994 let paths = paths.paths().to_vec();
2995 let is_remote = self
2996 .workspace
2997 .update(cx, |workspace, cx| {
2998 if workspace.project().read(cx).is_via_collab() {
2999 workspace.show_error(
3000 &anyhow::anyhow!("Cannot drop files on a remote project"),
3001 cx,
3002 );
3003 true
3004 } else {
3005 false
3006 }
3007 })
3008 .unwrap_or(true);
3009 if is_remote {
3010 return;
3011 }
3012
3013 self.workspace
3014 .update(cx, |workspace, cx| {
3015 let fs = Arc::clone(workspace.project().read(cx).fs());
3016 cx.spawn_in(window, async move |workspace, cx| {
3017 let mut is_file_checks = FuturesUnordered::new();
3018 for path in &paths {
3019 is_file_checks.push(fs.is_file(path))
3020 }
3021 let mut has_files_to_open = false;
3022 while let Some(is_file) = is_file_checks.next().await {
3023 if is_file {
3024 has_files_to_open = true;
3025 break;
3026 }
3027 }
3028 drop(is_file_checks);
3029 if !has_files_to_open {
3030 split_direction = None;
3031 }
3032
3033 if let Ok(open_task) = workspace.update_in(cx, |workspace, window, cx| {
3034 if let Some(split_direction) = split_direction {
3035 to_pane = workspace.split_pane(to_pane, split_direction, window, cx);
3036 }
3037 workspace.open_paths(
3038 paths,
3039 OpenOptions {
3040 visible: Some(OpenVisible::OnlyDirectories),
3041 ..Default::default()
3042 },
3043 Some(to_pane.downgrade()),
3044 window,
3045 cx,
3046 )
3047 }) {
3048 let opened_items: Vec<_> = open_task.await;
3049 _ = workspace.update(cx, |workspace, cx| {
3050 for item in opened_items.into_iter().flatten() {
3051 if let Err(e) = item {
3052 workspace.show_error(&e, cx);
3053 }
3054 }
3055 });
3056 }
3057 })
3058 .detach();
3059 })
3060 .log_err();
3061 }
3062
3063 pub fn display_nav_history_buttons(&mut self, display: Option<bool>) {
3064 self.display_nav_history_buttons = display;
3065 }
3066
3067 fn get_non_closeable_item_ids(&self, close_pinned: bool) -> Vec<EntityId> {
3068 if close_pinned {
3069 return vec![];
3070 }
3071
3072 self.items
3073 .iter()
3074 .enumerate()
3075 .filter(|(index, _item)| self.is_tab_pinned(*index))
3076 .map(|(_, item)| item.item_id())
3077 .collect()
3078 }
3079
3080 pub fn drag_split_direction(&self) -> Option<SplitDirection> {
3081 self.drag_split_direction
3082 }
3083
3084 pub fn set_zoom_out_on_close(&mut self, zoom_out_on_close: bool) {
3085 self.zoom_out_on_close = zoom_out_on_close;
3086 }
3087}
3088
3089fn default_render_tab_bar_buttons(
3090 pane: &mut Pane,
3091 window: &mut Window,
3092 cx: &mut Context<Pane>,
3093) -> (Option<AnyElement>, Option<AnyElement>) {
3094 if !pane.has_focus(window, cx) && !pane.context_menu_focused(window, cx) {
3095 return (None, None);
3096 }
3097 // Ideally we would return a vec of elements here to pass directly to the [TabBar]'s
3098 // `end_slot`, but due to needing a view here that isn't possible.
3099 let right_children = h_flex()
3100 // Instead we need to replicate the spacing from the [TabBar]'s `end_slot` here.
3101 .gap(DynamicSpacing::Base04.rems(cx))
3102 .child(
3103 PopoverMenu::new("pane-tab-bar-popover-menu")
3104 .trigger_with_tooltip(
3105 IconButton::new("plus", IconName::Plus).icon_size(IconSize::Small),
3106 Tooltip::text("New..."),
3107 )
3108 .anchor(Corner::TopRight)
3109 .with_handle(pane.new_item_context_menu_handle.clone())
3110 .menu(move |window, cx| {
3111 Some(ContextMenu::build(window, cx, |menu, _, _| {
3112 menu.action("New File", NewFile.boxed_clone())
3113 .action("Open File", ToggleFileFinder::default().boxed_clone())
3114 .separator()
3115 .action(
3116 "Search Project",
3117 DeploySearch {
3118 replace_enabled: false,
3119 }
3120 .boxed_clone(),
3121 )
3122 .action("Search Symbols", ToggleProjectSymbols.boxed_clone())
3123 .separator()
3124 .action("New Terminal", NewTerminal.boxed_clone())
3125 }))
3126 }),
3127 )
3128 .child(
3129 PopoverMenu::new("pane-tab-bar-split")
3130 .trigger_with_tooltip(
3131 IconButton::new("split", IconName::Split).icon_size(IconSize::Small),
3132 Tooltip::text("Split Pane"),
3133 )
3134 .anchor(Corner::TopRight)
3135 .with_handle(pane.split_item_context_menu_handle.clone())
3136 .menu(move |window, cx| {
3137 ContextMenu::build(window, cx, |menu, _, _| {
3138 menu.action("Split Right", SplitRight.boxed_clone())
3139 .action("Split Left", SplitLeft.boxed_clone())
3140 .action("Split Up", SplitUp.boxed_clone())
3141 .action("Split Down", SplitDown.boxed_clone())
3142 })
3143 .into()
3144 }),
3145 )
3146 .child({
3147 let zoomed = pane.is_zoomed();
3148 IconButton::new("toggle_zoom", IconName::Maximize)
3149 .icon_size(IconSize::Small)
3150 .toggle_state(zoomed)
3151 .selected_icon(IconName::Minimize)
3152 .on_click(cx.listener(|pane, _, window, cx| {
3153 pane.toggle_zoom(&crate::ToggleZoom, window, cx);
3154 }))
3155 .tooltip(move |window, cx| {
3156 Tooltip::for_action(
3157 if zoomed { "Zoom Out" } else { "Zoom In" },
3158 &ToggleZoom,
3159 window,
3160 cx,
3161 )
3162 })
3163 })
3164 .into_any_element()
3165 .into();
3166 (None, right_children)
3167}
3168
3169impl Focusable for Pane {
3170 fn focus_handle(&self, _cx: &App) -> FocusHandle {
3171 self.focus_handle.clone()
3172 }
3173}
3174
3175impl Render for Pane {
3176 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3177 let mut key_context = KeyContext::new_with_defaults();
3178 key_context.add("Pane");
3179 if self.active_item().is_none() {
3180 key_context.add("EmptyPane");
3181 }
3182
3183 let should_display_tab_bar = self.should_display_tab_bar.clone();
3184 let display_tab_bar = should_display_tab_bar(window, cx);
3185 let Some(project) = self.project.upgrade() else {
3186 return div().track_focus(&self.focus_handle(cx));
3187 };
3188 let is_local = project.read(cx).is_local();
3189
3190 v_flex()
3191 .key_context(key_context)
3192 .track_focus(&self.focus_handle(cx))
3193 .size_full()
3194 .flex_none()
3195 .overflow_hidden()
3196 .on_action(cx.listener(|pane, _: &AlternateFile, window, cx| {
3197 pane.alternate_file(window, cx);
3198 }))
3199 .on_action(
3200 cx.listener(|pane, _: &SplitLeft, _, cx| pane.split(SplitDirection::Left, cx)),
3201 )
3202 .on_action(cx.listener(|pane, _: &SplitUp, _, cx| pane.split(SplitDirection::Up, cx)))
3203 .on_action(cx.listener(|pane, _: &SplitHorizontal, _, cx| {
3204 pane.split(SplitDirection::horizontal(cx), cx)
3205 }))
3206 .on_action(cx.listener(|pane, _: &SplitVertical, _, cx| {
3207 pane.split(SplitDirection::vertical(cx), cx)
3208 }))
3209 .on_action(
3210 cx.listener(|pane, _: &SplitRight, _, cx| pane.split(SplitDirection::Right, cx)),
3211 )
3212 .on_action(
3213 cx.listener(|pane, _: &SplitDown, _, cx| pane.split(SplitDirection::Down, cx)),
3214 )
3215 .on_action(
3216 cx.listener(|pane, _: &GoBack, window, cx| pane.navigate_backward(window, cx)),
3217 )
3218 .on_action(
3219 cx.listener(|pane, _: &GoForward, window, cx| pane.navigate_forward(window, cx)),
3220 )
3221 .on_action(cx.listener(|_, _: &JoinIntoNext, _, cx| {
3222 cx.emit(Event::JoinIntoNext);
3223 }))
3224 .on_action(cx.listener(|_, _: &JoinAll, _, cx| {
3225 cx.emit(Event::JoinAll);
3226 }))
3227 .on_action(cx.listener(Pane::toggle_zoom))
3228 .on_action(
3229 cx.listener(|pane: &mut Pane, action: &ActivateItem, window, cx| {
3230 pane.activate_item(action.0, true, true, window, cx);
3231 }),
3232 )
3233 .on_action(
3234 cx.listener(|pane: &mut Pane, _: &ActivateLastItem, window, cx| {
3235 pane.activate_item(pane.items.len() - 1, true, true, window, cx);
3236 }),
3237 )
3238 .on_action(
3239 cx.listener(|pane: &mut Pane, _: &ActivatePreviousItem, window, cx| {
3240 pane.activate_prev_item(true, window, cx);
3241 }),
3242 )
3243 .on_action(
3244 cx.listener(|pane: &mut Pane, _: &ActivateNextItem, window, cx| {
3245 pane.activate_next_item(true, window, cx);
3246 }),
3247 )
3248 .on_action(
3249 cx.listener(|pane, _: &SwapItemLeft, window, cx| pane.swap_item_left(window, cx)),
3250 )
3251 .on_action(
3252 cx.listener(|pane, _: &SwapItemRight, window, cx| pane.swap_item_right(window, cx)),
3253 )
3254 .on_action(cx.listener(|pane, action, window, cx| {
3255 pane.toggle_pin_tab(action, window, cx);
3256 }))
3257 .when(PreviewTabsSettings::get_global(cx).enabled, |this| {
3258 this.on_action(cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, _, cx| {
3259 if let Some(active_item_id) = pane.active_item().map(|i| i.item_id()) {
3260 if pane.is_active_preview_item(active_item_id) {
3261 pane.set_preview_item_id(None, cx);
3262 } else {
3263 pane.set_preview_item_id(Some(active_item_id), cx);
3264 }
3265 }
3266 }))
3267 })
3268 .on_action(
3269 cx.listener(|pane: &mut Self, action: &CloseActiveItem, window, cx| {
3270 if let Some(task) = pane.close_active_item(action, window, cx) {
3271 task.detach_and_log_err(cx)
3272 }
3273 }),
3274 )
3275 .on_action(
3276 cx.listener(|pane: &mut Self, action: &CloseInactiveItems, window, cx| {
3277 if let Some(task) = pane.close_inactive_items(action, window, cx) {
3278 task.detach_and_log_err(cx)
3279 }
3280 }),
3281 )
3282 .on_action(
3283 cx.listener(|pane: &mut Self, action: &CloseCleanItems, window, cx| {
3284 if let Some(task) = pane.close_clean_items(action, window, cx) {
3285 task.detach_and_log_err(cx)
3286 }
3287 }),
3288 )
3289 .on_action(cx.listener(
3290 |pane: &mut Self, action: &CloseItemsToTheLeft, window, cx| {
3291 if let Some(task) = pane.close_items_to_the_left(action, window, cx) {
3292 task.detach_and_log_err(cx)
3293 }
3294 },
3295 ))
3296 .on_action(cx.listener(
3297 |pane: &mut Self, action: &CloseItemsToTheRight, window, cx| {
3298 if let Some(task) = pane.close_items_to_the_right(action, window, cx) {
3299 task.detach_and_log_err(cx)
3300 }
3301 },
3302 ))
3303 .on_action(
3304 cx.listener(|pane: &mut Self, action: &CloseAllItems, window, cx| {
3305 if let Some(task) = pane.close_all_items(action, window, cx) {
3306 task.detach_and_log_err(cx)
3307 }
3308 }),
3309 )
3310 .on_action(
3311 cx.listener(|pane: &mut Self, action: &CloseActiveItem, window, cx| {
3312 if let Some(task) = pane.close_active_item(action, window, cx) {
3313 task.detach_and_log_err(cx)
3314 }
3315 }),
3316 )
3317 .on_action(
3318 cx.listener(|pane: &mut Self, action: &RevealInProjectPanel, _, cx| {
3319 let entry_id = action
3320 .entry_id
3321 .map(ProjectEntryId::from_proto)
3322 .or_else(|| pane.active_item()?.project_entry_ids(cx).first().copied());
3323 if let Some(entry_id) = entry_id {
3324 pane.project
3325 .update(cx, |_, cx| {
3326 cx.emit(project::Event::RevealInProjectPanel(entry_id))
3327 })
3328 .ok();
3329 }
3330 }),
3331 )
3332 .on_action(cx.listener(|_, _: &menu::Cancel, window, cx| {
3333 if cx.stop_active_drag(window) {
3334 return;
3335 } else {
3336 cx.propagate();
3337 }
3338 }))
3339 .when(self.active_item().is_some() && display_tab_bar, |pane| {
3340 pane.child((self.render_tab_bar.clone())(self, window, cx))
3341 })
3342 .child({
3343 let has_worktrees = project.read(cx).visible_worktrees(cx).next().is_some();
3344 // main content
3345 div()
3346 .flex_1()
3347 .relative()
3348 .group("")
3349 .overflow_hidden()
3350 .on_drag_move::<DraggedTab>(cx.listener(Self::handle_drag_move))
3351 .on_drag_move::<DraggedSelection>(cx.listener(Self::handle_drag_move))
3352 .when(is_local, |div| {
3353 div.on_drag_move::<ExternalPaths>(cx.listener(Self::handle_drag_move))
3354 })
3355 .map(|div| {
3356 if let Some(item) = self.active_item() {
3357 div.id("pane_placeholder")
3358 .v_flex()
3359 .size_full()
3360 .overflow_hidden()
3361 .child(self.toolbar.clone())
3362 .child(item.to_any())
3363 } else {
3364 let placeholder = div
3365 .id("pane_placeholder")
3366 .h_flex()
3367 .size_full()
3368 .justify_center()
3369 .on_click(cx.listener(
3370 move |this, event: &ClickEvent, window, cx| {
3371 if event.up.click_count == 2 {
3372 window.dispatch_action(
3373 this.double_click_dispatch_action.boxed_clone(),
3374 cx,
3375 );
3376 }
3377 },
3378 ));
3379 if has_worktrees {
3380 placeholder
3381 } else {
3382 placeholder.child(
3383 Label::new("Open a file or project to get started.")
3384 .color(Color::Muted),
3385 )
3386 }
3387 }
3388 })
3389 .child(
3390 // drag target
3391 div()
3392 .invisible()
3393 .absolute()
3394 .bg(cx.theme().colors().drop_target_background)
3395 .group_drag_over::<DraggedTab>("", |style| style.visible())
3396 .group_drag_over::<DraggedSelection>("", |style| style.visible())
3397 .when(is_local, |div| {
3398 div.group_drag_over::<ExternalPaths>("", |style| style.visible())
3399 })
3400 .when_some(self.can_drop_predicate.clone(), |this, p| {
3401 this.can_drop(move |a, window, cx| p(a, window, cx))
3402 })
3403 .on_drop(cx.listener(move |this, dragged_tab, window, cx| {
3404 this.handle_tab_drop(
3405 dragged_tab,
3406 this.active_item_index(),
3407 window,
3408 cx,
3409 )
3410 }))
3411 .on_drop(cx.listener(
3412 move |this, selection: &DraggedSelection, window, cx| {
3413 this.handle_dragged_selection_drop(selection, None, window, cx)
3414 },
3415 ))
3416 .on_drop(cx.listener(move |this, paths, window, cx| {
3417 this.handle_external_paths_drop(paths, window, cx)
3418 }))
3419 .map(|div| {
3420 let size = DefiniteLength::Fraction(0.5);
3421 match self.drag_split_direction {
3422 None => div.top_0().right_0().bottom_0().left_0(),
3423 Some(SplitDirection::Up) => {
3424 div.top_0().left_0().right_0().h(size)
3425 }
3426 Some(SplitDirection::Down) => {
3427 div.left_0().bottom_0().right_0().h(size)
3428 }
3429 Some(SplitDirection::Left) => {
3430 div.top_0().left_0().bottom_0().w(size)
3431 }
3432 Some(SplitDirection::Right) => {
3433 div.top_0().bottom_0().right_0().w(size)
3434 }
3435 }
3436 }),
3437 )
3438 })
3439 .on_mouse_down(
3440 MouseButton::Navigate(NavigationDirection::Back),
3441 cx.listener(|pane, _, window, cx| {
3442 if let Some(workspace) = pane.workspace.upgrade() {
3443 let pane = cx.entity().downgrade();
3444 window.defer(cx, move |window, cx| {
3445 workspace.update(cx, |workspace, cx| {
3446 workspace.go_back(pane, window, cx).detach_and_log_err(cx)
3447 })
3448 })
3449 }
3450 }),
3451 )
3452 .on_mouse_down(
3453 MouseButton::Navigate(NavigationDirection::Forward),
3454 cx.listener(|pane, _, window, cx| {
3455 if let Some(workspace) = pane.workspace.upgrade() {
3456 let pane = cx.entity().downgrade();
3457 window.defer(cx, move |window, cx| {
3458 workspace.update(cx, |workspace, cx| {
3459 workspace
3460 .go_forward(pane, window, cx)
3461 .detach_and_log_err(cx)
3462 })
3463 })
3464 }
3465 }),
3466 )
3467 }
3468}
3469
3470impl ItemNavHistory {
3471 pub fn push<D: 'static + Send + Any>(&mut self, data: Option<D>, cx: &mut App) {
3472 if self
3473 .item
3474 .upgrade()
3475 .is_some_and(|item| item.include_in_nav_history())
3476 {
3477 self.history
3478 .push(data, self.item.clone(), self.is_preview, cx);
3479 }
3480 }
3481
3482 pub fn pop_backward(&mut self, cx: &mut App) -> Option<NavigationEntry> {
3483 self.history.pop(NavigationMode::GoingBack, cx)
3484 }
3485
3486 pub fn pop_forward(&mut self, cx: &mut App) -> Option<NavigationEntry> {
3487 self.history.pop(NavigationMode::GoingForward, cx)
3488 }
3489}
3490
3491impl NavHistory {
3492 pub fn for_each_entry(
3493 &self,
3494 cx: &App,
3495 mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
3496 ) {
3497 let borrowed_history = self.0.lock();
3498 borrowed_history
3499 .forward_stack
3500 .iter()
3501 .chain(borrowed_history.backward_stack.iter())
3502 .chain(borrowed_history.closed_stack.iter())
3503 .for_each(|entry| {
3504 if let Some(project_and_abs_path) =
3505 borrowed_history.paths_by_item.get(&entry.item.id())
3506 {
3507 f(entry, project_and_abs_path.clone());
3508 } else if let Some(item) = entry.item.upgrade() {
3509 if let Some(path) = item.project_path(cx) {
3510 f(entry, (path, None));
3511 }
3512 }
3513 })
3514 }
3515
3516 pub fn set_mode(&mut self, mode: NavigationMode) {
3517 self.0.lock().mode = mode;
3518 }
3519
3520 pub fn mode(&self) -> NavigationMode {
3521 self.0.lock().mode
3522 }
3523
3524 pub fn disable(&mut self) {
3525 self.0.lock().mode = NavigationMode::Disabled;
3526 }
3527
3528 pub fn enable(&mut self) {
3529 self.0.lock().mode = NavigationMode::Normal;
3530 }
3531
3532 pub fn pop(&mut self, mode: NavigationMode, cx: &mut App) -> Option<NavigationEntry> {
3533 let mut state = self.0.lock();
3534 let entry = match mode {
3535 NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
3536 return None;
3537 }
3538 NavigationMode::GoingBack => &mut state.backward_stack,
3539 NavigationMode::GoingForward => &mut state.forward_stack,
3540 NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
3541 }
3542 .pop_back();
3543 if entry.is_some() {
3544 state.did_update(cx);
3545 }
3546 entry
3547 }
3548
3549 pub fn push<D: 'static + Send + Any>(
3550 &mut self,
3551 data: Option<D>,
3552 item: Arc<dyn WeakItemHandle>,
3553 is_preview: bool,
3554 cx: &mut App,
3555 ) {
3556 let state = &mut *self.0.lock();
3557 match state.mode {
3558 NavigationMode::Disabled => {}
3559 NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
3560 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3561 state.backward_stack.pop_front();
3562 }
3563 state.backward_stack.push_back(NavigationEntry {
3564 item,
3565 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3566 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3567 is_preview,
3568 });
3569 state.forward_stack.clear();
3570 }
3571 NavigationMode::GoingBack => {
3572 if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3573 state.forward_stack.pop_front();
3574 }
3575 state.forward_stack.push_back(NavigationEntry {
3576 item,
3577 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3578 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3579 is_preview,
3580 });
3581 }
3582 NavigationMode::GoingForward => {
3583 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3584 state.backward_stack.pop_front();
3585 }
3586 state.backward_stack.push_back(NavigationEntry {
3587 item,
3588 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3589 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3590 is_preview,
3591 });
3592 }
3593 NavigationMode::ClosingItem => {
3594 if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3595 state.closed_stack.pop_front();
3596 }
3597 state.closed_stack.push_back(NavigationEntry {
3598 item,
3599 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3600 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3601 is_preview,
3602 });
3603 }
3604 }
3605 state.did_update(cx);
3606 }
3607
3608 pub fn remove_item(&mut self, item_id: EntityId) {
3609 let mut state = self.0.lock();
3610 state.paths_by_item.remove(&item_id);
3611 state
3612 .backward_stack
3613 .retain(|entry| entry.item.id() != item_id);
3614 state
3615 .forward_stack
3616 .retain(|entry| entry.item.id() != item_id);
3617 state
3618 .closed_stack
3619 .retain(|entry| entry.item.id() != item_id);
3620 }
3621
3622 pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option<PathBuf>)> {
3623 self.0.lock().paths_by_item.get(&item_id).cloned()
3624 }
3625}
3626
3627impl NavHistoryState {
3628 pub fn did_update(&self, cx: &mut App) {
3629 if let Some(pane) = self.pane.upgrade() {
3630 cx.defer(move |cx| {
3631 pane.update(cx, |pane, cx| pane.history_updated(cx));
3632 });
3633 }
3634 }
3635}
3636
3637fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
3638 let path = buffer_path
3639 .as_ref()
3640 .and_then(|p| {
3641 p.path
3642 .to_str()
3643 .and_then(|s| if s.is_empty() { None } else { Some(s) })
3644 })
3645 .unwrap_or("This buffer");
3646 let path = truncate_and_remove_front(path, 80);
3647 format!("{path} contains unsaved edits. Do you want to save it?")
3648}
3649
3650pub fn tab_details(items: &[Box<dyn ItemHandle>], _window: &Window, cx: &App) -> Vec<usize> {
3651 let mut tab_details = items.iter().map(|_| 0).collect::<Vec<_>>();
3652 let mut tab_descriptions = HashMap::default();
3653 let mut done = false;
3654 while !done {
3655 done = true;
3656
3657 // Store item indices by their tab description.
3658 for (ix, (item, detail)) in items.iter().zip(&tab_details).enumerate() {
3659 let description = item.tab_content_text(*detail, cx);
3660 if *detail == 0 || description != item.tab_content_text(detail - 1, cx) {
3661 tab_descriptions
3662 .entry(description)
3663 .or_insert(Vec::new())
3664 .push(ix);
3665 }
3666 }
3667
3668 // If two or more items have the same tab description, increase their level
3669 // of detail and try again.
3670 for (_, item_ixs) in tab_descriptions.drain() {
3671 if item_ixs.len() > 1 {
3672 done = false;
3673 for ix in item_ixs {
3674 tab_details[ix] += 1;
3675 }
3676 }
3677 }
3678 }
3679
3680 tab_details
3681}
3682
3683pub fn render_item_indicator(item: Box<dyn ItemHandle>, cx: &App) -> Option<Indicator> {
3684 maybe!({
3685 let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
3686 (true, _) => Color::Warning,
3687 (_, true) => Color::Accent,
3688 (false, false) => return None,
3689 };
3690
3691 Some(Indicator::dot().color(indicator_color))
3692 })
3693}
3694
3695impl Render for DraggedTab {
3696 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3697 let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
3698 let label = self.item.tab_content(
3699 TabContentParams {
3700 detail: Some(self.detail),
3701 selected: false,
3702 preview: false,
3703 deemphasized: false,
3704 },
3705 window,
3706 cx,
3707 );
3708 Tab::new("")
3709 .toggle_state(self.is_active)
3710 .child(label)
3711 .render(window, cx)
3712 .font(ui_font)
3713 }
3714}
3715
3716#[cfg(test)]
3717mod tests {
3718 use std::num::NonZero;
3719
3720 use super::*;
3721 use crate::item::test::{TestItem, TestProjectItem};
3722 use gpui::{TestAppContext, VisualTestContext};
3723 use project::FakeFs;
3724 use settings::SettingsStore;
3725 use theme::LoadThemes;
3726
3727 #[gpui::test]
3728 async fn test_remove_active_empty(cx: &mut TestAppContext) {
3729 init_test(cx);
3730 let fs = FakeFs::new(cx.executor());
3731
3732 let project = Project::test(fs, None, cx).await;
3733 let (workspace, cx) =
3734 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3735 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3736
3737 pane.update_in(cx, |pane, window, cx| {
3738 assert!(
3739 pane.close_active_item(
3740 &CloseActiveItem {
3741 save_intent: None,
3742 close_pinned: false
3743 },
3744 window,
3745 cx
3746 )
3747 .is_none()
3748 )
3749 });
3750 }
3751
3752 #[gpui::test]
3753 async fn test_add_item_capped_to_max_tabs(cx: &mut TestAppContext) {
3754 init_test(cx);
3755 let fs = FakeFs::new(cx.executor());
3756
3757 let project = Project::test(fs, None, cx).await;
3758 let (workspace, cx) =
3759 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3760 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3761
3762 for i in 0..7 {
3763 add_labeled_item(&pane, format!("{}", i).as_str(), false, cx);
3764 }
3765 set_max_tabs(cx, Some(5));
3766 add_labeled_item(&pane, "7", false, cx);
3767 // Remove items to respect the max tab cap.
3768 assert_item_labels(&pane, ["3", "4", "5", "6", "7*"], cx);
3769 pane.update_in(cx, |pane, window, cx| {
3770 pane.activate_item(0, false, false, window, cx);
3771 });
3772 add_labeled_item(&pane, "X", false, cx);
3773 // Respect activation order.
3774 assert_item_labels(&pane, ["3", "X*", "5", "6", "7"], cx);
3775
3776 for i in 0..7 {
3777 add_labeled_item(&pane, format!("D{}", i).as_str(), true, cx);
3778 }
3779 // Keeps dirty items, even over max tab cap.
3780 assert_item_labels(
3781 &pane,
3782 ["D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6*^"],
3783 cx,
3784 );
3785
3786 set_max_tabs(cx, None);
3787 for i in 0..7 {
3788 add_labeled_item(&pane, format!("N{}", i).as_str(), false, cx);
3789 }
3790 // No cap when max tabs is None.
3791 assert_item_labels(
3792 &pane,
3793 [
3794 "D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6^", "N0", "N1", "N2", "N3", "N4",
3795 "N5", "N6*",
3796 ],
3797 cx,
3798 );
3799 }
3800
3801 #[gpui::test]
3802 async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
3803 init_test(cx);
3804 let fs = FakeFs::new(cx.executor());
3805
3806 let project = Project::test(fs, None, cx).await;
3807 let (workspace, cx) =
3808 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3809 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3810
3811 // 1. Add with a destination index
3812 // a. Add before the active item
3813 set_labeled_items(&pane, ["A", "B*", "C"], cx);
3814 pane.update_in(cx, |pane, window, cx| {
3815 pane.add_item(
3816 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3817 false,
3818 false,
3819 Some(0),
3820 window,
3821 cx,
3822 );
3823 });
3824 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
3825
3826 // b. Add after the active item
3827 set_labeled_items(&pane, ["A", "B*", "C"], cx);
3828 pane.update_in(cx, |pane, window, cx| {
3829 pane.add_item(
3830 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3831 false,
3832 false,
3833 Some(2),
3834 window,
3835 cx,
3836 );
3837 });
3838 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3839
3840 // c. Add at the end of the item list (including off the length)
3841 set_labeled_items(&pane, ["A", "B*", "C"], cx);
3842 pane.update_in(cx, |pane, window, cx| {
3843 pane.add_item(
3844 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3845 false,
3846 false,
3847 Some(5),
3848 window,
3849 cx,
3850 );
3851 });
3852 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3853
3854 // 2. Add without a destination index
3855 // a. Add with active item at the start of the item list
3856 set_labeled_items(&pane, ["A*", "B", "C"], cx);
3857 pane.update_in(cx, |pane, window, cx| {
3858 pane.add_item(
3859 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3860 false,
3861 false,
3862 None,
3863 window,
3864 cx,
3865 );
3866 });
3867 set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
3868
3869 // b. Add with active item at the end of the item list
3870 set_labeled_items(&pane, ["A", "B", "C*"], cx);
3871 pane.update_in(cx, |pane, window, cx| {
3872 pane.add_item(
3873 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3874 false,
3875 false,
3876 None,
3877 window,
3878 cx,
3879 );
3880 });
3881 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3882 }
3883
3884 #[gpui::test]
3885 async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
3886 init_test(cx);
3887 let fs = FakeFs::new(cx.executor());
3888
3889 let project = Project::test(fs, None, cx).await;
3890 let (workspace, cx) =
3891 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3892 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3893
3894 // 1. Add with a destination index
3895 // 1a. Add before the active item
3896 let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3897 pane.update_in(cx, |pane, window, cx| {
3898 pane.add_item(d, false, false, Some(0), window, cx);
3899 });
3900 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
3901
3902 // 1b. Add after the active item
3903 let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3904 pane.update_in(cx, |pane, window, cx| {
3905 pane.add_item(d, false, false, Some(2), window, cx);
3906 });
3907 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3908
3909 // 1c. Add at the end of the item list (including off the length)
3910 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3911 pane.update_in(cx, |pane, window, cx| {
3912 pane.add_item(a, false, false, Some(5), window, cx);
3913 });
3914 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3915
3916 // 1d. Add same item to active index
3917 let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3918 pane.update_in(cx, |pane, window, cx| {
3919 pane.add_item(b, false, false, Some(1), window, cx);
3920 });
3921 assert_item_labels(&pane, ["A", "B*", "C"], cx);
3922
3923 // 1e. Add item to index after same item in last position
3924 let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3925 pane.update_in(cx, |pane, window, cx| {
3926 pane.add_item(c, false, false, Some(2), window, cx);
3927 });
3928 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3929
3930 // 2. Add without a destination index
3931 // 2a. Add with active item at the start of the item list
3932 let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
3933 pane.update_in(cx, |pane, window, cx| {
3934 pane.add_item(d, false, false, None, window, cx);
3935 });
3936 assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
3937
3938 // 2b. Add with active item at the end of the item list
3939 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
3940 pane.update_in(cx, |pane, window, cx| {
3941 pane.add_item(a, false, false, None, window, cx);
3942 });
3943 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3944
3945 // 2c. Add active item to active item at end of list
3946 let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
3947 pane.update_in(cx, |pane, window, cx| {
3948 pane.add_item(c, false, false, None, window, cx);
3949 });
3950 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3951
3952 // 2d. Add active item to active item at start of list
3953 let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
3954 pane.update_in(cx, |pane, window, cx| {
3955 pane.add_item(a, false, false, None, window, cx);
3956 });
3957 assert_item_labels(&pane, ["A*", "B", "C"], cx);
3958 }
3959
3960 #[gpui::test]
3961 async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
3962 init_test(cx);
3963 let fs = FakeFs::new(cx.executor());
3964
3965 let project = Project::test(fs, None, cx).await;
3966 let (workspace, cx) =
3967 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3968 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3969
3970 // singleton view
3971 pane.update_in(cx, |pane, window, cx| {
3972 pane.add_item(
3973 Box::new(cx.new(|cx| {
3974 TestItem::new(cx)
3975 .with_singleton(true)
3976 .with_label("buffer 1")
3977 .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
3978 })),
3979 false,
3980 false,
3981 None,
3982 window,
3983 cx,
3984 );
3985 });
3986 assert_item_labels(&pane, ["buffer 1*"], cx);
3987
3988 // new singleton view with the same project entry
3989 pane.update_in(cx, |pane, window, cx| {
3990 pane.add_item(
3991 Box::new(cx.new(|cx| {
3992 TestItem::new(cx)
3993 .with_singleton(true)
3994 .with_label("buffer 1")
3995 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3996 })),
3997 false,
3998 false,
3999 None,
4000 window,
4001 cx,
4002 );
4003 });
4004 assert_item_labels(&pane, ["buffer 1*"], cx);
4005
4006 // new singleton view with different project entry
4007 pane.update_in(cx, |pane, window, cx| {
4008 pane.add_item(
4009 Box::new(cx.new(|cx| {
4010 TestItem::new(cx)
4011 .with_singleton(true)
4012 .with_label("buffer 2")
4013 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
4014 })),
4015 false,
4016 false,
4017 None,
4018 window,
4019 cx,
4020 );
4021 });
4022 assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
4023
4024 // new multibuffer view with the same project entry
4025 pane.update_in(cx, |pane, window, cx| {
4026 pane.add_item(
4027 Box::new(cx.new(|cx| {
4028 TestItem::new(cx)
4029 .with_singleton(false)
4030 .with_label("multibuffer 1")
4031 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4032 })),
4033 false,
4034 false,
4035 None,
4036 window,
4037 cx,
4038 );
4039 });
4040 assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
4041
4042 // another multibuffer view with the same project entry
4043 pane.update_in(cx, |pane, window, cx| {
4044 pane.add_item(
4045 Box::new(cx.new(|cx| {
4046 TestItem::new(cx)
4047 .with_singleton(false)
4048 .with_label("multibuffer 1b")
4049 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4050 })),
4051 false,
4052 false,
4053 None,
4054 window,
4055 cx,
4056 );
4057 });
4058 assert_item_labels(
4059 &pane,
4060 ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
4061 cx,
4062 );
4063 }
4064
4065 #[gpui::test]
4066 async fn test_remove_item_ordering_history(cx: &mut TestAppContext) {
4067 init_test(cx);
4068 let fs = FakeFs::new(cx.executor());
4069
4070 let project = Project::test(fs, None, cx).await;
4071 let (workspace, cx) =
4072 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4073 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4074
4075 add_labeled_item(&pane, "A", false, cx);
4076 add_labeled_item(&pane, "B", false, cx);
4077 add_labeled_item(&pane, "C", false, cx);
4078 add_labeled_item(&pane, "D", false, cx);
4079 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4080
4081 pane.update_in(cx, |pane, window, cx| {
4082 pane.activate_item(1, false, false, window, cx)
4083 });
4084 add_labeled_item(&pane, "1", false, cx);
4085 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
4086
4087 pane.update_in(cx, |pane, window, cx| {
4088 pane.close_active_item(
4089 &CloseActiveItem {
4090 save_intent: None,
4091 close_pinned: false,
4092 },
4093 window,
4094 cx,
4095 )
4096 })
4097 .unwrap()
4098 .await
4099 .unwrap();
4100 assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
4101
4102 pane.update_in(cx, |pane, window, cx| {
4103 pane.activate_item(3, false, false, window, cx)
4104 });
4105 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4106
4107 pane.update_in(cx, |pane, window, cx| {
4108 pane.close_active_item(
4109 &CloseActiveItem {
4110 save_intent: None,
4111 close_pinned: false,
4112 },
4113 window,
4114 cx,
4115 )
4116 })
4117 .unwrap()
4118 .await
4119 .unwrap();
4120 assert_item_labels(&pane, ["A", "B*", "C"], cx);
4121
4122 pane.update_in(cx, |pane, window, cx| {
4123 pane.close_active_item(
4124 &CloseActiveItem {
4125 save_intent: None,
4126 close_pinned: false,
4127 },
4128 window,
4129 cx,
4130 )
4131 })
4132 .unwrap()
4133 .await
4134 .unwrap();
4135 assert_item_labels(&pane, ["A", "C*"], cx);
4136
4137 pane.update_in(cx, |pane, window, cx| {
4138 pane.close_active_item(
4139 &CloseActiveItem {
4140 save_intent: None,
4141 close_pinned: false,
4142 },
4143 window,
4144 cx,
4145 )
4146 })
4147 .unwrap()
4148 .await
4149 .unwrap();
4150 assert_item_labels(&pane, ["A*"], cx);
4151 }
4152
4153 #[gpui::test]
4154 async fn test_remove_item_ordering_neighbour(cx: &mut TestAppContext) {
4155 init_test(cx);
4156 cx.update_global::<SettingsStore, ()>(|s, cx| {
4157 s.update_user_settings::<ItemSettings>(cx, |s| {
4158 s.activate_on_close = Some(ActivateOnClose::Neighbour);
4159 });
4160 });
4161 let fs = FakeFs::new(cx.executor());
4162
4163 let project = Project::test(fs, None, cx).await;
4164 let (workspace, cx) =
4165 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4166 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4167
4168 add_labeled_item(&pane, "A", false, cx);
4169 add_labeled_item(&pane, "B", false, cx);
4170 add_labeled_item(&pane, "C", false, cx);
4171 add_labeled_item(&pane, "D", false, cx);
4172 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4173
4174 pane.update_in(cx, |pane, window, cx| {
4175 pane.activate_item(1, false, false, window, cx)
4176 });
4177 add_labeled_item(&pane, "1", false, cx);
4178 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
4179
4180 pane.update_in(cx, |pane, window, cx| {
4181 pane.close_active_item(
4182 &CloseActiveItem {
4183 save_intent: None,
4184 close_pinned: false,
4185 },
4186 window,
4187 cx,
4188 )
4189 })
4190 .unwrap()
4191 .await
4192 .unwrap();
4193 assert_item_labels(&pane, ["A", "B", "C*", "D"], cx);
4194
4195 pane.update_in(cx, |pane, window, cx| {
4196 pane.activate_item(3, false, false, window, cx)
4197 });
4198 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4199
4200 pane.update_in(cx, |pane, window, cx| {
4201 pane.close_active_item(
4202 &CloseActiveItem {
4203 save_intent: None,
4204 close_pinned: false,
4205 },
4206 window,
4207 cx,
4208 )
4209 })
4210 .unwrap()
4211 .await
4212 .unwrap();
4213 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4214
4215 pane.update_in(cx, |pane, window, cx| {
4216 pane.close_active_item(
4217 &CloseActiveItem {
4218 save_intent: None,
4219 close_pinned: false,
4220 },
4221 window,
4222 cx,
4223 )
4224 })
4225 .unwrap()
4226 .await
4227 .unwrap();
4228 assert_item_labels(&pane, ["A", "B*"], cx);
4229
4230 pane.update_in(cx, |pane, window, cx| {
4231 pane.close_active_item(
4232 &CloseActiveItem {
4233 save_intent: None,
4234 close_pinned: false,
4235 },
4236 window,
4237 cx,
4238 )
4239 })
4240 .unwrap()
4241 .await
4242 .unwrap();
4243 assert_item_labels(&pane, ["A*"], cx);
4244 }
4245
4246 #[gpui::test]
4247 async fn test_remove_item_ordering_left_neighbour(cx: &mut TestAppContext) {
4248 init_test(cx);
4249 cx.update_global::<SettingsStore, ()>(|s, cx| {
4250 s.update_user_settings::<ItemSettings>(cx, |s| {
4251 s.activate_on_close = Some(ActivateOnClose::LeftNeighbour);
4252 });
4253 });
4254 let fs = FakeFs::new(cx.executor());
4255
4256 let project = Project::test(fs, None, cx).await;
4257 let (workspace, cx) =
4258 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4259 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4260
4261 add_labeled_item(&pane, "A", false, cx);
4262 add_labeled_item(&pane, "B", false, cx);
4263 add_labeled_item(&pane, "C", false, cx);
4264 add_labeled_item(&pane, "D", false, cx);
4265 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4266
4267 pane.update_in(cx, |pane, window, cx| {
4268 pane.activate_item(1, false, false, window, cx)
4269 });
4270 add_labeled_item(&pane, "1", false, cx);
4271 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
4272
4273 pane.update_in(cx, |pane, window, cx| {
4274 pane.close_active_item(
4275 &CloseActiveItem {
4276 save_intent: None,
4277 close_pinned: false,
4278 },
4279 window,
4280 cx,
4281 )
4282 })
4283 .unwrap()
4284 .await
4285 .unwrap();
4286 assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
4287
4288 pane.update_in(cx, |pane, window, cx| {
4289 pane.activate_item(3, false, false, window, cx)
4290 });
4291 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4292
4293 pane.update_in(cx, |pane, window, cx| {
4294 pane.close_active_item(
4295 &CloseActiveItem {
4296 save_intent: None,
4297 close_pinned: false,
4298 },
4299 window,
4300 cx,
4301 )
4302 })
4303 .unwrap()
4304 .await
4305 .unwrap();
4306 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4307
4308 pane.update_in(cx, |pane, window, cx| {
4309 pane.activate_item(0, false, false, window, cx)
4310 });
4311 assert_item_labels(&pane, ["A*", "B", "C"], cx);
4312
4313 pane.update_in(cx, |pane, window, cx| {
4314 pane.close_active_item(
4315 &CloseActiveItem {
4316 save_intent: None,
4317 close_pinned: false,
4318 },
4319 window,
4320 cx,
4321 )
4322 })
4323 .unwrap()
4324 .await
4325 .unwrap();
4326 assert_item_labels(&pane, ["B*", "C"], cx);
4327
4328 pane.update_in(cx, |pane, window, cx| {
4329 pane.close_active_item(
4330 &CloseActiveItem {
4331 save_intent: None,
4332 close_pinned: false,
4333 },
4334 window,
4335 cx,
4336 )
4337 })
4338 .unwrap()
4339 .await
4340 .unwrap();
4341 assert_item_labels(&pane, ["C*"], cx);
4342 }
4343
4344 #[gpui::test]
4345 async fn test_close_inactive_items(cx: &mut TestAppContext) {
4346 init_test(cx);
4347 let fs = FakeFs::new(cx.executor());
4348
4349 let project = Project::test(fs, None, cx).await;
4350 let (workspace, cx) =
4351 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4352 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4353
4354 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
4355
4356 pane.update_in(cx, |pane, window, cx| {
4357 pane.close_inactive_items(
4358 &CloseInactiveItems {
4359 save_intent: None,
4360 close_pinned: false,
4361 },
4362 window,
4363 cx,
4364 )
4365 })
4366 .unwrap()
4367 .await
4368 .unwrap();
4369 assert_item_labels(&pane, ["C*"], cx);
4370 }
4371
4372 #[gpui::test]
4373 async fn test_close_clean_items(cx: &mut TestAppContext) {
4374 init_test(cx);
4375 let fs = FakeFs::new(cx.executor());
4376
4377 let project = Project::test(fs, None, cx).await;
4378 let (workspace, cx) =
4379 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4380 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4381
4382 add_labeled_item(&pane, "A", true, cx);
4383 add_labeled_item(&pane, "B", false, cx);
4384 add_labeled_item(&pane, "C", true, cx);
4385 add_labeled_item(&pane, "D", false, cx);
4386 add_labeled_item(&pane, "E", false, cx);
4387 assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
4388
4389 pane.update_in(cx, |pane, window, cx| {
4390 pane.close_clean_items(
4391 &CloseCleanItems {
4392 close_pinned: false,
4393 },
4394 window,
4395 cx,
4396 )
4397 })
4398 .unwrap()
4399 .await
4400 .unwrap();
4401 assert_item_labels(&pane, ["A^", "C*^"], cx);
4402 }
4403
4404 #[gpui::test]
4405 async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
4406 init_test(cx);
4407 let fs = FakeFs::new(cx.executor());
4408
4409 let project = Project::test(fs, None, cx).await;
4410 let (workspace, cx) =
4411 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4412 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4413
4414 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
4415
4416 pane.update_in(cx, |pane, window, cx| {
4417 pane.close_items_to_the_left(
4418 &CloseItemsToTheLeft {
4419 close_pinned: false,
4420 },
4421 window,
4422 cx,
4423 )
4424 })
4425 .unwrap()
4426 .await
4427 .unwrap();
4428 assert_item_labels(&pane, ["C*", "D", "E"], cx);
4429 }
4430
4431 #[gpui::test]
4432 async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
4433 init_test(cx);
4434 let fs = FakeFs::new(cx.executor());
4435
4436 let project = Project::test(fs, None, cx).await;
4437 let (workspace, cx) =
4438 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4439 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4440
4441 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
4442
4443 pane.update_in(cx, |pane, window, cx| {
4444 pane.close_items_to_the_right(
4445 &CloseItemsToTheRight {
4446 close_pinned: false,
4447 },
4448 window,
4449 cx,
4450 )
4451 })
4452 .unwrap()
4453 .await
4454 .unwrap();
4455 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4456 }
4457
4458 #[gpui::test]
4459 async fn test_close_all_items(cx: &mut TestAppContext) {
4460 init_test(cx);
4461 let fs = FakeFs::new(cx.executor());
4462
4463 let project = Project::test(fs, None, cx).await;
4464 let (workspace, cx) =
4465 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4466 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4467
4468 let item_a = add_labeled_item(&pane, "A", false, cx);
4469 add_labeled_item(&pane, "B", false, cx);
4470 add_labeled_item(&pane, "C", false, cx);
4471 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4472
4473 pane.update_in(cx, |pane, window, cx| {
4474 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4475 pane.pin_tab_at(ix, window, cx);
4476 pane.close_all_items(
4477 &CloseAllItems {
4478 save_intent: None,
4479 close_pinned: false,
4480 },
4481 window,
4482 cx,
4483 )
4484 })
4485 .unwrap()
4486 .await
4487 .unwrap();
4488 assert_item_labels(&pane, ["A*"], cx);
4489
4490 pane.update_in(cx, |pane, window, cx| {
4491 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4492 pane.unpin_tab_at(ix, window, cx);
4493 pane.close_all_items(
4494 &CloseAllItems {
4495 save_intent: None,
4496 close_pinned: false,
4497 },
4498 window,
4499 cx,
4500 )
4501 })
4502 .unwrap()
4503 .await
4504 .unwrap();
4505
4506 assert_item_labels(&pane, [], cx);
4507
4508 add_labeled_item(&pane, "A", true, cx).update(cx, |item, cx| {
4509 item.project_items
4510 .push(TestProjectItem::new_dirty(1, "A.txt", cx))
4511 });
4512 add_labeled_item(&pane, "B", true, cx).update(cx, |item, cx| {
4513 item.project_items
4514 .push(TestProjectItem::new_dirty(2, "B.txt", cx))
4515 });
4516 add_labeled_item(&pane, "C", true, cx).update(cx, |item, cx| {
4517 item.project_items
4518 .push(TestProjectItem::new_dirty(3, "C.txt", cx))
4519 });
4520 assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
4521
4522 let save = pane
4523 .update_in(cx, |pane, window, cx| {
4524 pane.close_all_items(
4525 &CloseAllItems {
4526 save_intent: None,
4527 close_pinned: false,
4528 },
4529 window,
4530 cx,
4531 )
4532 })
4533 .unwrap();
4534
4535 cx.executor().run_until_parked();
4536 cx.simulate_prompt_answer("Save all");
4537 save.await.unwrap();
4538 assert_item_labels(&pane, [], cx);
4539
4540 add_labeled_item(&pane, "A", true, cx);
4541 add_labeled_item(&pane, "B", true, cx);
4542 add_labeled_item(&pane, "C", true, cx);
4543 assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
4544 let save = pane
4545 .update_in(cx, |pane, window, cx| {
4546 pane.close_all_items(
4547 &CloseAllItems {
4548 save_intent: None,
4549 close_pinned: false,
4550 },
4551 window,
4552 cx,
4553 )
4554 })
4555 .unwrap();
4556
4557 cx.executor().run_until_parked();
4558 cx.simulate_prompt_answer("Discard all");
4559 save.await.unwrap();
4560 assert_item_labels(&pane, [], cx);
4561 }
4562
4563 #[gpui::test]
4564 async fn test_close_with_save_intent(cx: &mut TestAppContext) {
4565 init_test(cx);
4566 let fs = FakeFs::new(cx.executor());
4567
4568 let project = Project::test(fs, None, cx).await;
4569 let (workspace, cx) =
4570 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
4571 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4572
4573 let a = cx.update(|_, cx| TestProjectItem::new_dirty(1, "A.txt", cx));
4574 let b = cx.update(|_, cx| TestProjectItem::new_dirty(1, "B.txt", cx));
4575 let c = cx.update(|_, cx| TestProjectItem::new_dirty(1, "C.txt", cx));
4576
4577 add_labeled_item(&pane, "AB", true, cx).update(cx, |item, _| {
4578 item.project_items.push(a.clone());
4579 item.project_items.push(b.clone());
4580 });
4581 add_labeled_item(&pane, "C", true, cx)
4582 .update(cx, |item, _| item.project_items.push(c.clone()));
4583 assert_item_labels(&pane, ["AB^", "C*^"], cx);
4584
4585 pane.update_in(cx, |pane, window, cx| {
4586 pane.close_all_items(
4587 &CloseAllItems {
4588 save_intent: Some(SaveIntent::Save),
4589 close_pinned: false,
4590 },
4591 window,
4592 cx,
4593 )
4594 })
4595 .unwrap()
4596 .await
4597 .unwrap();
4598
4599 assert_item_labels(&pane, [], cx);
4600 cx.update(|_, cx| {
4601 assert!(!a.read(cx).is_dirty);
4602 assert!(!b.read(cx).is_dirty);
4603 assert!(!c.read(cx).is_dirty);
4604 });
4605 }
4606
4607 #[gpui::test]
4608 async fn test_close_all_items_including_pinned(cx: &mut TestAppContext) {
4609 init_test(cx);
4610 let fs = FakeFs::new(cx.executor());
4611
4612 let project = Project::test(fs, None, cx).await;
4613 let (workspace, cx) =
4614 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
4615 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4616
4617 let item_a = add_labeled_item(&pane, "A", false, cx);
4618 add_labeled_item(&pane, "B", false, cx);
4619 add_labeled_item(&pane, "C", false, cx);
4620 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4621
4622 pane.update_in(cx, |pane, window, cx| {
4623 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4624 pane.pin_tab_at(ix, window, cx);
4625 pane.close_all_items(
4626 &CloseAllItems {
4627 save_intent: None,
4628 close_pinned: true,
4629 },
4630 window,
4631 cx,
4632 )
4633 })
4634 .unwrap()
4635 .await
4636 .unwrap();
4637 assert_item_labels(&pane, [], cx);
4638 }
4639
4640 #[gpui::test]
4641 async fn test_close_pinned_tab_with_non_pinned_in_same_pane(cx: &mut TestAppContext) {
4642 init_test(cx);
4643 let fs = FakeFs::new(cx.executor());
4644 let project = Project::test(fs, None, cx).await;
4645 let (workspace, cx) =
4646 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
4647
4648 // Non-pinned tabs in same pane
4649 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4650 add_labeled_item(&pane, "A", false, cx);
4651 add_labeled_item(&pane, "B", false, cx);
4652 add_labeled_item(&pane, "C", false, cx);
4653 pane.update_in(cx, |pane, window, cx| {
4654 pane.pin_tab_at(0, window, cx);
4655 });
4656 set_labeled_items(&pane, ["A*", "B", "C"], cx);
4657 pane.update_in(cx, |pane, window, cx| {
4658 pane.close_active_item(
4659 &CloseActiveItem {
4660 save_intent: None,
4661 close_pinned: false,
4662 },
4663 window,
4664 cx,
4665 );
4666 });
4667 // Non-pinned tab should be active
4668 assert_item_labels(&pane, ["A", "B*", "C"], cx);
4669 }
4670
4671 #[gpui::test]
4672 async fn test_close_pinned_tab_with_non_pinned_in_different_pane(cx: &mut TestAppContext) {
4673 init_test(cx);
4674 let fs = FakeFs::new(cx.executor());
4675 let project = Project::test(fs, None, cx).await;
4676 let (workspace, cx) =
4677 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
4678
4679 // No non-pinned tabs in same pane, non-pinned tabs in another pane
4680 let pane1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4681 let pane2 = workspace.update_in(cx, |workspace, window, cx| {
4682 workspace.split_pane(pane1.clone(), SplitDirection::Right, window, cx)
4683 });
4684 add_labeled_item(&pane1, "A", false, cx);
4685 pane1.update_in(cx, |pane, window, cx| {
4686 pane.pin_tab_at(0, window, cx);
4687 });
4688 set_labeled_items(&pane1, ["A*"], cx);
4689 add_labeled_item(&pane2, "B", false, cx);
4690 set_labeled_items(&pane2, ["B"], cx);
4691 pane1.update_in(cx, |pane, window, cx| {
4692 pane.close_active_item(
4693 &CloseActiveItem {
4694 save_intent: None,
4695 close_pinned: false,
4696 },
4697 window,
4698 cx,
4699 );
4700 });
4701 // Non-pinned tab of other pane should be active
4702 assert_item_labels(&pane2, ["B*"], cx);
4703 }
4704
4705 fn init_test(cx: &mut TestAppContext) {
4706 cx.update(|cx| {
4707 let settings_store = SettingsStore::test(cx);
4708 cx.set_global(settings_store);
4709 theme::init(LoadThemes::JustBase, cx);
4710 crate::init_settings(cx);
4711 Project::init_settings(cx);
4712 });
4713 }
4714
4715 fn set_max_tabs(cx: &mut TestAppContext, value: Option<usize>) {
4716 cx.update_global(|store: &mut SettingsStore, cx| {
4717 store.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4718 settings.max_tabs = value.map(|v| NonZero::new(v).unwrap())
4719 });
4720 });
4721 }
4722
4723 fn add_labeled_item(
4724 pane: &Entity<Pane>,
4725 label: &str,
4726 is_dirty: bool,
4727 cx: &mut VisualTestContext,
4728 ) -> Box<Entity<TestItem>> {
4729 pane.update_in(cx, |pane, window, cx| {
4730 let labeled_item =
4731 Box::new(cx.new(|cx| TestItem::new(cx).with_label(label).with_dirty(is_dirty)));
4732 pane.add_item(labeled_item.clone(), false, false, None, window, cx);
4733 labeled_item
4734 })
4735 }
4736
4737 fn set_labeled_items<const COUNT: usize>(
4738 pane: &Entity<Pane>,
4739 labels: [&str; COUNT],
4740 cx: &mut VisualTestContext,
4741 ) -> [Box<Entity<TestItem>>; COUNT] {
4742 pane.update_in(cx, |pane, window, cx| {
4743 pane.items.clear();
4744 let mut active_item_index = 0;
4745
4746 let mut index = 0;
4747 let items = labels.map(|mut label| {
4748 if label.ends_with('*') {
4749 label = label.trim_end_matches('*');
4750 active_item_index = index;
4751 }
4752
4753 let labeled_item = Box::new(cx.new(|cx| TestItem::new(cx).with_label(label)));
4754 pane.add_item(labeled_item.clone(), false, false, None, window, cx);
4755 index += 1;
4756 labeled_item
4757 });
4758
4759 pane.activate_item(active_item_index, false, false, window, cx);
4760
4761 items
4762 })
4763 }
4764
4765 // Assert the item label, with the active item label suffixed with a '*'
4766 #[track_caller]
4767 fn assert_item_labels<const COUNT: usize>(
4768 pane: &Entity<Pane>,
4769 expected_states: [&str; COUNT],
4770 cx: &mut VisualTestContext,
4771 ) {
4772 let actual_states = pane.update(cx, |pane, cx| {
4773 pane.items
4774 .iter()
4775 .enumerate()
4776 .map(|(ix, item)| {
4777 let mut state = item
4778 .to_any()
4779 .downcast::<TestItem>()
4780 .unwrap()
4781 .read(cx)
4782 .label
4783 .clone();
4784 if ix == pane.active_item_index {
4785 state.push('*');
4786 }
4787 if item.is_dirty(cx) {
4788 state.push('^');
4789 }
4790 state
4791 })
4792 .collect::<Vec<_>>()
4793 });
4794 assert_eq!(
4795 actual_states, expected_states,
4796 "pane items do not match expectation"
4797 );
4798 }
4799}