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 pane.remove_item(item_to_close.item_id(), false, true, window, cx);
1582 })
1583 .ok();
1584 }
1585
1586 pane.update(cx, |_, cx| cx.notify()).ok();
1587 Ok(())
1588 })
1589 }
1590
1591 pub fn remove_item(
1592 &mut self,
1593 item_id: EntityId,
1594 activate_pane: bool,
1595 close_pane_if_empty: bool,
1596 window: &mut Window,
1597 cx: &mut Context<Self>,
1598 ) {
1599 let Some(item_index) = self.index_for_item_id(item_id) else {
1600 return;
1601 };
1602 self._remove_item(
1603 item_index,
1604 activate_pane,
1605 close_pane_if_empty,
1606 None,
1607 window,
1608 cx,
1609 )
1610 }
1611
1612 pub fn remove_item_and_focus_on_pane(
1613 &mut self,
1614 item_index: usize,
1615 activate_pane: bool,
1616 focus_on_pane_if_closed: Entity<Pane>,
1617 window: &mut Window,
1618 cx: &mut Context<Self>,
1619 ) {
1620 self._remove_item(
1621 item_index,
1622 activate_pane,
1623 true,
1624 Some(focus_on_pane_if_closed),
1625 window,
1626 cx,
1627 )
1628 }
1629
1630 fn _remove_item(
1631 &mut self,
1632 item_index: usize,
1633 activate_pane: bool,
1634 close_pane_if_empty: bool,
1635 focus_on_pane_if_closed: Option<Entity<Pane>>,
1636 window: &mut Window,
1637 cx: &mut Context<Self>,
1638 ) {
1639 let activate_on_close = &ItemSettings::get_global(cx).activate_on_close;
1640 self.activation_history
1641 .retain(|entry| entry.entity_id != self.items[item_index].item_id());
1642
1643 if self.is_tab_pinned(item_index) {
1644 self.pinned_tab_count -= 1;
1645 }
1646 if item_index == self.active_item_index {
1647 let left_neighbour_index = || item_index.min(self.items.len()).saturating_sub(1);
1648 let index_to_activate = match activate_on_close {
1649 ActivateOnClose::History => self
1650 .activation_history
1651 .pop()
1652 .and_then(|last_activated_item| {
1653 self.items.iter().enumerate().find_map(|(index, item)| {
1654 (item.item_id() == last_activated_item.entity_id).then_some(index)
1655 })
1656 })
1657 // We didn't have a valid activation history entry, so fallback
1658 // to activating the item to the left
1659 .unwrap_or_else(left_neighbour_index),
1660 ActivateOnClose::Neighbour => {
1661 self.activation_history.pop();
1662 if item_index + 1 < self.items.len() {
1663 item_index + 1
1664 } else {
1665 item_index.saturating_sub(1)
1666 }
1667 }
1668 ActivateOnClose::LeftNeighbour => {
1669 self.activation_history.pop();
1670 left_neighbour_index()
1671 }
1672 };
1673
1674 let should_activate = activate_pane || self.has_focus(window, cx);
1675 if self.items.len() == 1 && should_activate {
1676 self.focus_handle.focus(window);
1677 } else {
1678 self.activate_item(
1679 index_to_activate,
1680 should_activate,
1681 should_activate,
1682 window,
1683 cx,
1684 );
1685 }
1686 }
1687
1688 let item = self.items.remove(item_index);
1689
1690 cx.emit(Event::RemovedItem { item: item.clone() });
1691 if self.items.is_empty() {
1692 item.deactivated(window, cx);
1693 if close_pane_if_empty {
1694 self.update_toolbar(window, cx);
1695 cx.emit(Event::Remove {
1696 focus_on_pane: focus_on_pane_if_closed,
1697 });
1698 }
1699 }
1700
1701 if item_index < self.active_item_index {
1702 self.active_item_index -= 1;
1703 }
1704
1705 let mode = self.nav_history.mode();
1706 self.nav_history.set_mode(NavigationMode::ClosingItem);
1707 item.deactivated(window, cx);
1708 self.nav_history.set_mode(mode);
1709
1710 if self.is_active_preview_item(item.item_id()) {
1711 self.set_preview_item_id(None, cx);
1712 }
1713
1714 if let Some(path) = item.project_path(cx) {
1715 let abs_path = self
1716 .nav_history
1717 .0
1718 .lock()
1719 .paths_by_item
1720 .get(&item.item_id())
1721 .and_then(|(_, abs_path)| abs_path.clone());
1722
1723 self.nav_history
1724 .0
1725 .lock()
1726 .paths_by_item
1727 .insert(item.item_id(), (path, abs_path));
1728 } else {
1729 self.nav_history
1730 .0
1731 .lock()
1732 .paths_by_item
1733 .remove(&item.item_id());
1734 }
1735
1736 if self.zoom_out_on_close && self.items.is_empty() && close_pane_if_empty && self.zoomed {
1737 cx.emit(Event::ZoomOut);
1738 }
1739
1740 cx.notify();
1741 }
1742
1743 pub async fn save_item(
1744 project: Entity<Project>,
1745 pane: &WeakEntity<Pane>,
1746 item: &dyn ItemHandle,
1747 save_intent: SaveIntent,
1748 cx: &mut AsyncWindowContext,
1749 ) -> Result<bool> {
1750 const CONFLICT_MESSAGE: &str = "This file has changed on disk since you started editing it. Do you want to overwrite it?";
1751
1752 const DELETED_MESSAGE: &str = "This file has been deleted on disk since you started editing it. Do you want to recreate it?";
1753
1754 if save_intent == SaveIntent::Skip {
1755 return Ok(true);
1756 }
1757 let Some(item_ix) = pane
1758 .update(cx, |pane, _| pane.index_for_item(item))
1759 .ok()
1760 .flatten()
1761 else {
1762 return Ok(true);
1763 };
1764
1765 let (
1766 mut has_conflict,
1767 mut is_dirty,
1768 mut can_save,
1769 can_save_as,
1770 is_singleton,
1771 has_deleted_file,
1772 ) = cx.update(|_window, cx| {
1773 (
1774 item.has_conflict(cx),
1775 item.is_dirty(cx),
1776 item.can_save(cx),
1777 item.can_save_as(cx),
1778 item.is_singleton(cx),
1779 item.has_deleted_file(cx),
1780 )
1781 })?;
1782
1783 // when saving a single buffer, we ignore whether or not it's dirty.
1784 if save_intent == SaveIntent::Save || save_intent == SaveIntent::SaveWithoutFormat {
1785 is_dirty = true;
1786 }
1787
1788 if save_intent == SaveIntent::SaveAs {
1789 is_dirty = true;
1790 has_conflict = false;
1791 can_save = false;
1792 }
1793
1794 if save_intent == SaveIntent::Overwrite {
1795 has_conflict = false;
1796 }
1797
1798 let should_format = save_intent != SaveIntent::SaveWithoutFormat;
1799
1800 if has_conflict && can_save {
1801 if has_deleted_file && is_singleton {
1802 let answer = pane.update_in(cx, |pane, window, cx| {
1803 pane.activate_item(item_ix, true, true, window, cx);
1804 window.prompt(
1805 PromptLevel::Warning,
1806 DELETED_MESSAGE,
1807 None,
1808 &["Save", "Close", "Cancel"],
1809 cx,
1810 )
1811 })?;
1812 match answer.await {
1813 Ok(0) => {
1814 pane.update_in(cx, |_, window, cx| {
1815 item.save(should_format, project, window, cx)
1816 })?
1817 .await?
1818 }
1819 Ok(1) => {
1820 pane.update_in(cx, |pane, window, cx| {
1821 pane.remove_item(item.item_id(), false, true, window, cx)
1822 })?;
1823 }
1824 _ => return Ok(false),
1825 }
1826 return Ok(true);
1827 } else {
1828 let answer = pane.update_in(cx, |pane, window, cx| {
1829 pane.activate_item(item_ix, true, true, window, cx);
1830 window.prompt(
1831 PromptLevel::Warning,
1832 CONFLICT_MESSAGE,
1833 None,
1834 &["Overwrite", "Discard", "Cancel"],
1835 cx,
1836 )
1837 })?;
1838 match answer.await {
1839 Ok(0) => {
1840 pane.update_in(cx, |_, window, cx| {
1841 item.save(should_format, project, window, cx)
1842 })?
1843 .await?
1844 }
1845 Ok(1) => {
1846 pane.update_in(cx, |_, window, cx| item.reload(project, window, cx))?
1847 .await?
1848 }
1849 _ => return Ok(false),
1850 }
1851 }
1852 } else if is_dirty && (can_save || can_save_as) {
1853 if save_intent == SaveIntent::Close {
1854 let will_autosave = cx.update(|_window, cx| {
1855 matches!(
1856 item.workspace_settings(cx).autosave,
1857 AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange
1858 ) && Self::can_autosave_item(item, cx)
1859 })?;
1860 if !will_autosave {
1861 let item_id = item.item_id();
1862 let answer_task = pane.update_in(cx, |pane, window, cx| {
1863 if pane.save_modals_spawned.insert(item_id) {
1864 pane.activate_item(item_ix, true, true, window, cx);
1865 let prompt = dirty_message_for(item.project_path(cx));
1866 Some(window.prompt(
1867 PromptLevel::Warning,
1868 &prompt,
1869 None,
1870 &["Save", "Don't Save", "Cancel"],
1871 cx,
1872 ))
1873 } else {
1874 None
1875 }
1876 })?;
1877 if let Some(answer_task) = answer_task {
1878 let answer = answer_task.await;
1879 pane.update(cx, |pane, _| {
1880 if !pane.save_modals_spawned.remove(&item_id) {
1881 debug_panic!(
1882 "save modal was not present in spawned modals after awaiting for its answer"
1883 )
1884 }
1885 })?;
1886 match answer {
1887 Ok(0) => {}
1888 Ok(1) => {
1889 // Don't save this file
1890 pane.update_in(cx, |pane, window, cx| {
1891 if pane.is_tab_pinned(item_ix) && !item.can_save(cx) {
1892 pane.pinned_tab_count -= 1;
1893 }
1894 item.discarded(project, window, cx)
1895 })
1896 .log_err();
1897 return Ok(true);
1898 }
1899 _ => return Ok(false), // Cancel
1900 }
1901 } else {
1902 return Ok(false);
1903 }
1904 }
1905 }
1906
1907 if can_save {
1908 pane.update_in(cx, |pane, window, cx| {
1909 if pane.is_active_preview_item(item.item_id()) {
1910 pane.set_preview_item_id(None, cx);
1911 }
1912 item.save(should_format, project, window, cx)
1913 })?
1914 .await?;
1915 } else if can_save_as && is_singleton {
1916 let abs_path = pane.update_in(cx, |pane, window, cx| {
1917 pane.activate_item(item_ix, true, true, window, cx);
1918 pane.workspace.update(cx, |workspace, cx| {
1919 workspace.prompt_for_new_path(window, cx)
1920 })
1921 })??;
1922 if let Some(abs_path) = abs_path.await.ok().flatten() {
1923 pane.update_in(cx, |pane, window, cx| {
1924 if let Some(item) = pane.item_for_path(abs_path.clone(), cx) {
1925 pane.remove_item(item.item_id(), false, false, window, cx);
1926 }
1927
1928 item.save_as(project, abs_path, window, cx)
1929 })?
1930 .await?;
1931 } else {
1932 return Ok(false);
1933 }
1934 }
1935 }
1936
1937 pane.update(cx, |_, cx| {
1938 cx.emit(Event::UserSavedItem {
1939 item: item.downgrade_item(),
1940 save_intent,
1941 });
1942 true
1943 })
1944 }
1945
1946 fn can_autosave_item(item: &dyn ItemHandle, cx: &App) -> bool {
1947 let is_deleted = item.project_entry_ids(cx).is_empty();
1948 item.is_dirty(cx) && !item.has_conflict(cx) && item.can_save(cx) && !is_deleted
1949 }
1950
1951 pub fn autosave_item(
1952 item: &dyn ItemHandle,
1953 project: Entity<Project>,
1954 window: &mut Window,
1955 cx: &mut App,
1956 ) -> Task<Result<()>> {
1957 let format = !matches!(
1958 item.workspace_settings(cx).autosave,
1959 AutosaveSetting::AfterDelay { .. }
1960 );
1961 if Self::can_autosave_item(item, cx) {
1962 item.save(format, project, window, cx)
1963 } else {
1964 Task::ready(Ok(()))
1965 }
1966 }
1967
1968 pub fn focus_active_item(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1969 if let Some(active_item) = self.active_item() {
1970 let focus_handle = active_item.item_focus_handle(cx);
1971 window.focus(&focus_handle);
1972 }
1973 }
1974
1975 pub fn split(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
1976 cx.emit(Event::Split(direction));
1977 }
1978
1979 pub fn toolbar(&self) -> &Entity<Toolbar> {
1980 &self.toolbar
1981 }
1982
1983 pub fn handle_deleted_project_item(
1984 &mut self,
1985 entry_id: ProjectEntryId,
1986 window: &mut Window,
1987 cx: &mut Context<Pane>,
1988 ) -> Option<()> {
1989 let item_id = self.items().find_map(|item| {
1990 if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
1991 Some(item.item_id())
1992 } else {
1993 None
1994 }
1995 })?;
1996
1997 self.remove_item(item_id, false, true, window, cx);
1998 self.nav_history.remove_item(item_id);
1999
2000 Some(())
2001 }
2002
2003 fn update_toolbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2004 let active_item = self
2005 .items
2006 .get(self.active_item_index)
2007 .map(|item| item.as_ref());
2008 self.toolbar.update(cx, |toolbar, cx| {
2009 toolbar.set_active_item(active_item, window, cx);
2010 });
2011 }
2012
2013 fn update_status_bar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2014 let workspace = self.workspace.clone();
2015 let pane = cx.entity().clone();
2016
2017 window.defer(cx, move |window, cx| {
2018 let Ok(status_bar) = workspace.update(cx, |workspace, _| workspace.status_bar.clone())
2019 else {
2020 return;
2021 };
2022
2023 status_bar.update(cx, move |status_bar, cx| {
2024 status_bar.set_active_pane(&pane, window, cx);
2025 });
2026 });
2027 }
2028
2029 fn entry_abs_path(&self, entry: ProjectEntryId, cx: &App) -> Option<PathBuf> {
2030 let worktree = self
2031 .workspace
2032 .upgrade()?
2033 .read(cx)
2034 .project()
2035 .read(cx)
2036 .worktree_for_entry(entry, cx)?
2037 .read(cx);
2038 let entry = worktree.entry_for_id(entry)?;
2039 match &entry.canonical_path {
2040 Some(canonical_path) => Some(canonical_path.to_path_buf()),
2041 None => worktree.absolutize(&entry.path).ok(),
2042 }
2043 }
2044
2045 pub fn icon_color(selected: bool) -> Color {
2046 if selected {
2047 Color::Default
2048 } else {
2049 Color::Muted
2050 }
2051 }
2052
2053 fn toggle_pin_tab(&mut self, _: &TogglePinTab, window: &mut Window, cx: &mut Context<Self>) {
2054 if self.items.is_empty() {
2055 return;
2056 }
2057 let active_tab_ix = self.active_item_index();
2058 if self.is_tab_pinned(active_tab_ix) {
2059 self.unpin_tab_at(active_tab_ix, window, cx);
2060 } else {
2061 self.pin_tab_at(active_tab_ix, window, cx);
2062 }
2063 }
2064
2065 fn pin_tab_at(&mut self, ix: usize, window: &mut Window, cx: &mut Context<Self>) {
2066 maybe!({
2067 let pane = cx.entity().clone();
2068 let destination_index = self.pinned_tab_count.min(ix);
2069 self.pinned_tab_count += 1;
2070 let id = self.item_for_index(ix)?.item_id();
2071
2072 if self.is_active_preview_item(id) {
2073 self.set_preview_item_id(None, cx);
2074 }
2075
2076 self.workspace
2077 .update(cx, |_, cx| {
2078 cx.defer_in(window, move |_, window, cx| {
2079 move_item(&pane, &pane, id, destination_index, window, cx)
2080 });
2081 })
2082 .ok()?;
2083
2084 Some(())
2085 });
2086 }
2087
2088 fn unpin_tab_at(&mut self, ix: usize, window: &mut Window, cx: &mut Context<Self>) {
2089 maybe!({
2090 let pane = cx.entity().clone();
2091 self.pinned_tab_count = self.pinned_tab_count.checked_sub(1)?;
2092 let destination_index = self.pinned_tab_count;
2093
2094 let id = self.item_for_index(ix)?.item_id();
2095
2096 self.workspace
2097 .update(cx, |_, cx| {
2098 cx.defer_in(window, move |_, window, cx| {
2099 move_item(&pane, &pane, id, destination_index, window, cx)
2100 });
2101 })
2102 .ok()?;
2103
2104 Some(())
2105 });
2106 }
2107
2108 fn is_tab_pinned(&self, ix: usize) -> bool {
2109 self.pinned_tab_count > ix
2110 }
2111
2112 fn has_pinned_tabs(&self) -> bool {
2113 self.pinned_tab_count != 0
2114 }
2115
2116 fn has_unpinned_tabs(&self) -> bool {
2117 self.pinned_tab_count < self.items.len()
2118 }
2119
2120 fn activate_unpinned_tab(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2121 if self.items.is_empty() {
2122 return;
2123 }
2124 let Some(index) = self
2125 .items()
2126 .enumerate()
2127 .find_map(|(index, _item)| (!self.is_tab_pinned(index)).then_some(index))
2128 else {
2129 return;
2130 };
2131 self.activate_item(index, true, true, window, cx);
2132 }
2133
2134 fn render_tab(
2135 &self,
2136 ix: usize,
2137 item: &dyn ItemHandle,
2138 detail: usize,
2139 focus_handle: &FocusHandle,
2140 window: &mut Window,
2141 cx: &mut Context<Pane>,
2142 ) -> impl IntoElement + use<> {
2143 let is_active = ix == self.active_item_index;
2144 let is_preview = self
2145 .preview_item_id
2146 .map(|id| id == item.item_id())
2147 .unwrap_or(false);
2148
2149 let label = item.tab_content(
2150 TabContentParams {
2151 detail: Some(detail),
2152 selected: is_active,
2153 preview: is_preview,
2154 deemphasized: !self.has_focus(window, cx),
2155 },
2156 window,
2157 cx,
2158 );
2159
2160 let item_diagnostic = item
2161 .project_path(cx)
2162 .map_or(None, |project_path| self.diagnostics.get(&project_path));
2163
2164 let decorated_icon = item_diagnostic.map_or(None, |diagnostic| {
2165 let icon = match item.tab_icon(window, cx) {
2166 Some(icon) => icon,
2167 None => return None,
2168 };
2169
2170 let knockout_item_color = if is_active {
2171 cx.theme().colors().tab_active_background
2172 } else {
2173 cx.theme().colors().tab_bar_background
2174 };
2175
2176 let (icon_decoration, icon_color) = if matches!(diagnostic, &DiagnosticSeverity::ERROR)
2177 {
2178 (IconDecorationKind::X, Color::Error)
2179 } else {
2180 (IconDecorationKind::Triangle, Color::Warning)
2181 };
2182
2183 Some(DecoratedIcon::new(
2184 icon.size(IconSize::Small).color(Color::Muted),
2185 Some(
2186 IconDecoration::new(icon_decoration, knockout_item_color, cx)
2187 .color(icon_color.color(cx))
2188 .position(Point {
2189 x: px(-2.),
2190 y: px(-2.),
2191 }),
2192 ),
2193 ))
2194 });
2195
2196 let icon = if decorated_icon.is_none() {
2197 match item_diagnostic {
2198 Some(&DiagnosticSeverity::ERROR) => None,
2199 Some(&DiagnosticSeverity::WARNING) => None,
2200 _ => item
2201 .tab_icon(window, cx)
2202 .map(|icon| icon.color(Color::Muted)),
2203 }
2204 .map(|icon| icon.size(IconSize::Small))
2205 } else {
2206 None
2207 };
2208
2209 let settings = ItemSettings::get_global(cx);
2210 let close_side = &settings.close_position;
2211 let show_close_button = &settings.show_close_button;
2212 let indicator = render_item_indicator(item.boxed_clone(), cx);
2213 let item_id = item.item_id();
2214 let is_first_item = ix == 0;
2215 let is_last_item = ix == self.items.len() - 1;
2216 let is_pinned = self.is_tab_pinned(ix);
2217 let position_relative_to_active_item = ix.cmp(&self.active_item_index);
2218
2219 let tab = Tab::new(ix)
2220 .position(if is_first_item {
2221 TabPosition::First
2222 } else if is_last_item {
2223 TabPosition::Last
2224 } else {
2225 TabPosition::Middle(position_relative_to_active_item)
2226 })
2227 .close_side(match close_side {
2228 ClosePosition::Left => ui::TabCloseSide::Start,
2229 ClosePosition::Right => ui::TabCloseSide::End,
2230 })
2231 .toggle_state(is_active)
2232 .on_click(cx.listener(move |pane: &mut Self, _, window, cx| {
2233 pane.activate_item(ix, true, true, window, cx)
2234 }))
2235 // TODO: This should be a click listener with the middle mouse button instead of a mouse down listener.
2236 .on_mouse_down(
2237 MouseButton::Middle,
2238 cx.listener(move |pane, _event, window, cx| {
2239 pane.close_item_by_id(item_id, SaveIntent::Close, window, cx)
2240 .detach_and_log_err(cx);
2241 }),
2242 )
2243 .on_mouse_down(
2244 MouseButton::Left,
2245 cx.listener(move |pane, event: &MouseDownEvent, _, cx| {
2246 if let Some(id) = pane.preview_item_id {
2247 if id == item_id && event.click_count > 1 {
2248 pane.set_preview_item_id(None, cx);
2249 }
2250 }
2251 }),
2252 )
2253 .on_drag(
2254 DraggedTab {
2255 item: item.boxed_clone(),
2256 pane: cx.entity().clone(),
2257 detail,
2258 is_active,
2259 ix,
2260 },
2261 |tab, _, _, cx| cx.new(|_| tab.clone()),
2262 )
2263 .drag_over::<DraggedTab>(|tab, _, _, cx| {
2264 tab.bg(cx.theme().colors().drop_target_background)
2265 })
2266 .drag_over::<DraggedSelection>(|tab, _, _, cx| {
2267 tab.bg(cx.theme().colors().drop_target_background)
2268 })
2269 .when_some(self.can_drop_predicate.clone(), |this, p| {
2270 this.can_drop(move |a, window, cx| p(a, window, cx))
2271 })
2272 .on_drop(
2273 cx.listener(move |this, dragged_tab: &DraggedTab, window, cx| {
2274 this.drag_split_direction = None;
2275 this.handle_tab_drop(dragged_tab, ix, window, cx)
2276 }),
2277 )
2278 .on_drop(
2279 cx.listener(move |this, selection: &DraggedSelection, window, cx| {
2280 this.drag_split_direction = None;
2281 this.handle_dragged_selection_drop(selection, Some(ix), window, cx)
2282 }),
2283 )
2284 .on_drop(cx.listener(move |this, paths, window, cx| {
2285 this.drag_split_direction = None;
2286 this.handle_external_paths_drop(paths, window, cx)
2287 }))
2288 .when_some(item.tab_tooltip_content(cx), |tab, content| match content {
2289 TabTooltipContent::Text(text) => tab.tooltip(Tooltip::text(text.clone())),
2290 TabTooltipContent::Custom(element_fn) => {
2291 tab.tooltip(move |window, cx| element_fn(window, cx))
2292 }
2293 })
2294 .start_slot::<Indicator>(indicator)
2295 .map(|this| {
2296 let end_slot_action: &'static dyn Action;
2297 let end_slot_tooltip_text: &'static str;
2298 let end_slot = if is_pinned {
2299 end_slot_action = &TogglePinTab;
2300 end_slot_tooltip_text = "Unpin Tab";
2301 IconButton::new("unpin tab", IconName::Pin)
2302 .shape(IconButtonShape::Square)
2303 .icon_color(Color::Muted)
2304 .size(ButtonSize::None)
2305 .icon_size(IconSize::XSmall)
2306 .on_click(cx.listener(move |pane, _, window, cx| {
2307 pane.unpin_tab_at(ix, window, cx);
2308 }))
2309 } else {
2310 end_slot_action = &CloseActiveItem {
2311 save_intent: None,
2312 close_pinned: false,
2313 };
2314 end_slot_tooltip_text = "Close Tab";
2315 match show_close_button {
2316 ShowCloseButton::Always => IconButton::new("close tab", IconName::Close),
2317 ShowCloseButton::Hover => {
2318 IconButton::new("close tab", IconName::Close).visible_on_hover("")
2319 }
2320 ShowCloseButton::Hidden => return this,
2321 }
2322 .shape(IconButtonShape::Square)
2323 .icon_color(Color::Muted)
2324 .size(ButtonSize::None)
2325 .icon_size(IconSize::XSmall)
2326 .on_click(cx.listener(move |pane, _, window, cx| {
2327 pane.close_item_by_id(item_id, SaveIntent::Close, window, cx)
2328 .detach_and_log_err(cx);
2329 }))
2330 }
2331 .map(|this| {
2332 if is_active {
2333 let focus_handle = focus_handle.clone();
2334 this.tooltip(move |window, cx| {
2335 Tooltip::for_action_in(
2336 end_slot_tooltip_text,
2337 end_slot_action,
2338 &focus_handle,
2339 window,
2340 cx,
2341 )
2342 })
2343 } else {
2344 this.tooltip(Tooltip::text(end_slot_tooltip_text))
2345 }
2346 });
2347 this.end_slot(end_slot)
2348 })
2349 .child(
2350 h_flex()
2351 .gap_1()
2352 .items_center()
2353 .children(
2354 std::iter::once(if let Some(decorated_icon) = decorated_icon {
2355 Some(div().child(decorated_icon.into_any_element()))
2356 } else if let Some(icon) = icon {
2357 Some(div().child(icon.into_any_element()))
2358 } else {
2359 None
2360 })
2361 .flatten(),
2362 )
2363 .child(label),
2364 );
2365
2366 let single_entry_to_resolve = self.items[ix]
2367 .is_singleton(cx)
2368 .then(|| self.items[ix].project_entry_ids(cx).get(0).copied())
2369 .flatten();
2370
2371 let total_items = self.items.len();
2372 let has_items_to_left = ix > 0;
2373 let has_items_to_right = ix < total_items - 1;
2374 let is_pinned = self.is_tab_pinned(ix);
2375 let pane = cx.entity().downgrade();
2376 let menu_context = item.item_focus_handle(cx);
2377 right_click_menu(ix).trigger(tab).menu(move |window, cx| {
2378 let pane = pane.clone();
2379 let menu_context = menu_context.clone();
2380 ContextMenu::build(window, cx, move |mut menu, window, cx| {
2381 if let Some(pane) = pane.upgrade() {
2382 menu = menu
2383 .entry(
2384 "Close",
2385 Some(Box::new(CloseActiveItem {
2386 save_intent: None,
2387 close_pinned: true,
2388 })),
2389 window.handler_for(&pane, move |pane, window, cx| {
2390 pane.close_item_by_id(item_id, SaveIntent::Close, window, cx)
2391 .detach_and_log_err(cx);
2392 }),
2393 )
2394 .item(ContextMenuItem::Entry(
2395 ContextMenuEntry::new("Close Others")
2396 .action(Box::new(CloseInactiveItems {
2397 save_intent: None,
2398 close_pinned: false,
2399 }))
2400 .disabled(total_items == 1)
2401 .handler(window.handler_for(&pane, move |pane, window, cx| {
2402 pane.close_items(window, cx, SaveIntent::Close, |id| {
2403 id != item_id
2404 })
2405 .detach_and_log_err(cx);
2406 })),
2407 ))
2408 .separator()
2409 .item(ContextMenuItem::Entry(
2410 ContextMenuEntry::new("Close Left")
2411 .action(Box::new(CloseItemsToTheLeft {
2412 close_pinned: false,
2413 }))
2414 .disabled(!has_items_to_left)
2415 .handler(window.handler_for(&pane, move |pane, window, cx| {
2416 pane.close_items_to_the_left_by_id(
2417 item_id,
2418 &CloseItemsToTheLeft {
2419 close_pinned: false,
2420 },
2421 pane.get_non_closeable_item_ids(false),
2422 window,
2423 cx,
2424 )
2425 .detach_and_log_err(cx);
2426 })),
2427 ))
2428 .item(ContextMenuItem::Entry(
2429 ContextMenuEntry::new("Close Right")
2430 .action(Box::new(CloseItemsToTheRight {
2431 close_pinned: false,
2432 }))
2433 .disabled(!has_items_to_right)
2434 .handler(window.handler_for(&pane, move |pane, window, cx| {
2435 pane.close_items_to_the_right_by_id(
2436 item_id,
2437 &CloseItemsToTheRight {
2438 close_pinned: false,
2439 },
2440 pane.get_non_closeable_item_ids(false),
2441 window,
2442 cx,
2443 )
2444 .detach_and_log_err(cx);
2445 })),
2446 ))
2447 .separator()
2448 .entry(
2449 "Close Clean",
2450 Some(Box::new(CloseCleanItems {
2451 close_pinned: false,
2452 })),
2453 window.handler_for(&pane, move |pane, window, cx| {
2454 if let Some(task) = pane.close_clean_items(
2455 &CloseCleanItems {
2456 close_pinned: false,
2457 },
2458 window,
2459 cx,
2460 ) {
2461 task.detach_and_log_err(cx)
2462 }
2463 }),
2464 )
2465 .entry(
2466 "Close All",
2467 Some(Box::new(CloseAllItems {
2468 save_intent: None,
2469 close_pinned: false,
2470 })),
2471 window.handler_for(&pane, |pane, window, cx| {
2472 if let Some(task) = pane.close_all_items(
2473 &CloseAllItems {
2474 save_intent: None,
2475 close_pinned: false,
2476 },
2477 window,
2478 cx,
2479 ) {
2480 task.detach_and_log_err(cx)
2481 }
2482 }),
2483 );
2484
2485 let pin_tab_entries = |menu: ContextMenu| {
2486 menu.separator().map(|this| {
2487 if is_pinned {
2488 this.entry(
2489 "Unpin Tab",
2490 Some(TogglePinTab.boxed_clone()),
2491 window.handler_for(&pane, move |pane, window, cx| {
2492 pane.unpin_tab_at(ix, window, cx);
2493 }),
2494 )
2495 } else {
2496 this.entry(
2497 "Pin Tab",
2498 Some(TogglePinTab.boxed_clone()),
2499 window.handler_for(&pane, move |pane, window, cx| {
2500 pane.pin_tab_at(ix, window, cx);
2501 }),
2502 )
2503 }
2504 })
2505 };
2506 if let Some(entry) = single_entry_to_resolve {
2507 let project_path = pane
2508 .read(cx)
2509 .item_for_entry(entry, cx)
2510 .and_then(|item| item.project_path(cx));
2511 let worktree = project_path.as_ref().and_then(|project_path| {
2512 pane.read(cx)
2513 .project
2514 .upgrade()?
2515 .read(cx)
2516 .worktree_for_id(project_path.worktree_id, cx)
2517 });
2518 let has_relative_path = worktree.as_ref().is_some_and(|worktree| {
2519 worktree
2520 .read(cx)
2521 .root_entry()
2522 .map_or(false, |entry| entry.is_dir())
2523 });
2524
2525 let entry_abs_path = pane.read(cx).entry_abs_path(entry, cx);
2526 let parent_abs_path = entry_abs_path
2527 .as_deref()
2528 .and_then(|abs_path| Some(abs_path.parent()?.to_path_buf()));
2529 let relative_path = project_path
2530 .map(|project_path| project_path.path)
2531 .filter(|_| has_relative_path);
2532
2533 let visible_in_project_panel = relative_path.is_some()
2534 && worktree.is_some_and(|worktree| worktree.read(cx).is_visible());
2535
2536 let entry_id = entry.to_proto();
2537 menu = menu
2538 .separator()
2539 .when_some(entry_abs_path, |menu, abs_path| {
2540 menu.entry(
2541 "Copy Path",
2542 Some(Box::new(zed_actions::workspace::CopyPath)),
2543 window.handler_for(&pane, move |_, _, cx| {
2544 cx.write_to_clipboard(ClipboardItem::new_string(
2545 abs_path.to_string_lossy().to_string(),
2546 ));
2547 }),
2548 )
2549 })
2550 .when_some(relative_path, |menu, relative_path| {
2551 menu.entry(
2552 "Copy Relative Path",
2553 Some(Box::new(zed_actions::workspace::CopyRelativePath)),
2554 window.handler_for(&pane, move |_, _, cx| {
2555 cx.write_to_clipboard(ClipboardItem::new_string(
2556 relative_path.to_string_lossy().to_string(),
2557 ));
2558 }),
2559 )
2560 })
2561 .map(pin_tab_entries)
2562 .separator()
2563 .when(visible_in_project_panel, |menu| {
2564 menu.entry(
2565 "Reveal In Project Panel",
2566 Some(Box::new(RevealInProjectPanel {
2567 entry_id: Some(entry_id),
2568 })),
2569 window.handler_for(&pane, move |pane, _, cx| {
2570 pane.project
2571 .update(cx, |_, cx| {
2572 cx.emit(project::Event::RevealInProjectPanel(
2573 ProjectEntryId::from_proto(entry_id),
2574 ))
2575 })
2576 .ok();
2577 }),
2578 )
2579 })
2580 .when_some(parent_abs_path, |menu, parent_abs_path| {
2581 menu.entry(
2582 "Open in Terminal",
2583 Some(Box::new(OpenInTerminal)),
2584 window.handler_for(&pane, move |_, window, cx| {
2585 window.dispatch_action(
2586 OpenTerminal {
2587 working_directory: parent_abs_path.clone(),
2588 }
2589 .boxed_clone(),
2590 cx,
2591 );
2592 }),
2593 )
2594 });
2595 } else {
2596 menu = menu.map(pin_tab_entries);
2597 }
2598 }
2599
2600 menu.context(menu_context)
2601 })
2602 })
2603 }
2604
2605 fn render_tab_bar(&mut self, window: &mut Window, cx: &mut Context<Pane>) -> AnyElement {
2606 let focus_handle = self.focus_handle.clone();
2607 let navigate_backward = IconButton::new("navigate_backward", IconName::ArrowLeft)
2608 .icon_size(IconSize::Small)
2609 .on_click({
2610 let entity = cx.entity().clone();
2611 move |_, window, cx| {
2612 entity.update(cx, |pane, cx| pane.navigate_backward(window, cx))
2613 }
2614 })
2615 .disabled(!self.can_navigate_backward())
2616 .tooltip({
2617 let focus_handle = focus_handle.clone();
2618 move |window, cx| {
2619 Tooltip::for_action_in("Go Back", &GoBack, &focus_handle, window, cx)
2620 }
2621 });
2622
2623 let navigate_forward = IconButton::new("navigate_forward", IconName::ArrowRight)
2624 .icon_size(IconSize::Small)
2625 .on_click({
2626 let entity = cx.entity().clone();
2627 move |_, window, cx| entity.update(cx, |pane, cx| pane.navigate_forward(window, cx))
2628 })
2629 .disabled(!self.can_navigate_forward())
2630 .tooltip({
2631 let focus_handle = focus_handle.clone();
2632 move |window, cx| {
2633 Tooltip::for_action_in("Go Forward", &GoForward, &focus_handle, window, cx)
2634 }
2635 });
2636
2637 let mut tab_items = self
2638 .items
2639 .iter()
2640 .enumerate()
2641 .zip(tab_details(&self.items, window, cx))
2642 .map(|((ix, item), detail)| {
2643 self.render_tab(ix, &**item, detail, &focus_handle, window, cx)
2644 })
2645 .collect::<Vec<_>>();
2646 let tab_count = tab_items.len();
2647 let unpinned_tabs = tab_items.split_off(self.pinned_tab_count);
2648 let pinned_tabs = tab_items;
2649 TabBar::new("tab_bar")
2650 .when(
2651 self.display_nav_history_buttons.unwrap_or_default(),
2652 |tab_bar| {
2653 tab_bar
2654 .start_child(navigate_backward)
2655 .start_child(navigate_forward)
2656 },
2657 )
2658 .map(|tab_bar| {
2659 if self.show_tab_bar_buttons {
2660 let render_tab_buttons = self.render_tab_bar_buttons.clone();
2661 let (left_children, right_children) = render_tab_buttons(self, window, cx);
2662 tab_bar
2663 .start_children(left_children)
2664 .end_children(right_children)
2665 } else {
2666 tab_bar
2667 }
2668 })
2669 .children(pinned_tabs.len().ne(&0).then(|| {
2670 let content_width = self
2671 .tab_bar_scroll_handle
2672 .content_size()
2673 .map(|content_size| content_size.size.width)
2674 .unwrap_or(px(0.));
2675 let viewport_width = self.tab_bar_scroll_handle.viewport().size.width;
2676 // We need to check both because offset returns delta values even when the scroll handle is not scrollable
2677 let is_scrollable = content_width > viewport_width;
2678 let is_scrolled = self.tab_bar_scroll_handle.offset().x < px(0.);
2679 h_flex()
2680 .children(pinned_tabs)
2681 .when(is_scrollable && is_scrolled, |this| {
2682 this.border_r_1().border_color(cx.theme().colors().border)
2683 })
2684 }))
2685 .child(
2686 h_flex()
2687 .id("unpinned tabs")
2688 .overflow_x_scroll()
2689 .w_full()
2690 .track_scroll(&self.tab_bar_scroll_handle)
2691 .children(unpinned_tabs)
2692 .child(
2693 div()
2694 .id("tab_bar_drop_target")
2695 .min_w_6()
2696 // HACK: This empty child is currently necessary to force the drop target to appear
2697 // despite us setting a min width above.
2698 .child("")
2699 .h_full()
2700 .flex_grow()
2701 .drag_over::<DraggedTab>(|bar, _, _, cx| {
2702 bar.bg(cx.theme().colors().drop_target_background)
2703 })
2704 .drag_over::<DraggedSelection>(|bar, _, _, cx| {
2705 bar.bg(cx.theme().colors().drop_target_background)
2706 })
2707 .on_drop(cx.listener(
2708 move |this, dragged_tab: &DraggedTab, window, cx| {
2709 this.drag_split_direction = None;
2710 this.handle_tab_drop(dragged_tab, this.items.len(), window, cx)
2711 },
2712 ))
2713 .on_drop(cx.listener(
2714 move |this, selection: &DraggedSelection, window, cx| {
2715 this.drag_split_direction = None;
2716 this.handle_project_entry_drop(
2717 &selection.active_selection.entry_id,
2718 Some(tab_count),
2719 window,
2720 cx,
2721 )
2722 },
2723 ))
2724 .on_drop(cx.listener(move |this, paths, window, cx| {
2725 this.drag_split_direction = None;
2726 this.handle_external_paths_drop(paths, window, cx)
2727 }))
2728 .on_click(cx.listener(move |this, event: &ClickEvent, window, cx| {
2729 if event.up.click_count == 2 {
2730 window.dispatch_action(
2731 this.double_click_dispatch_action.boxed_clone(),
2732 cx,
2733 );
2734 }
2735 })),
2736 ),
2737 )
2738 .into_any_element()
2739 }
2740
2741 pub fn render_menu_overlay(menu: &Entity<ContextMenu>) -> Div {
2742 div().absolute().bottom_0().right_0().size_0().child(
2743 deferred(anchored().anchor(Corner::TopRight).child(menu.clone())).with_priority(1),
2744 )
2745 }
2746
2747 pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut Context<Self>) {
2748 self.zoomed = zoomed;
2749 cx.notify();
2750 }
2751
2752 pub fn is_zoomed(&self) -> bool {
2753 self.zoomed
2754 }
2755
2756 fn handle_drag_move<T: 'static>(
2757 &mut self,
2758 event: &DragMoveEvent<T>,
2759 window: &mut Window,
2760 cx: &mut Context<Self>,
2761 ) {
2762 let can_split_predicate = self.can_split_predicate.take();
2763 let can_split = match &can_split_predicate {
2764 Some(can_split_predicate) => {
2765 can_split_predicate(self, event.dragged_item(), window, cx)
2766 }
2767 None => false,
2768 };
2769 self.can_split_predicate = can_split_predicate;
2770 if !can_split {
2771 return;
2772 }
2773
2774 let rect = event.bounds.size;
2775
2776 let size = event.bounds.size.width.min(event.bounds.size.height)
2777 * WorkspaceSettings::get_global(cx).drop_target_size;
2778
2779 let relative_cursor = Point::new(
2780 event.event.position.x - event.bounds.left(),
2781 event.event.position.y - event.bounds.top(),
2782 );
2783
2784 let direction = if relative_cursor.x < size
2785 || relative_cursor.x > rect.width - size
2786 || relative_cursor.y < size
2787 || relative_cursor.y > rect.height - size
2788 {
2789 [
2790 SplitDirection::Up,
2791 SplitDirection::Right,
2792 SplitDirection::Down,
2793 SplitDirection::Left,
2794 ]
2795 .iter()
2796 .min_by_key(|side| match side {
2797 SplitDirection::Up => relative_cursor.y,
2798 SplitDirection::Right => rect.width - relative_cursor.x,
2799 SplitDirection::Down => rect.height - relative_cursor.y,
2800 SplitDirection::Left => relative_cursor.x,
2801 })
2802 .cloned()
2803 } else {
2804 None
2805 };
2806
2807 if direction != self.drag_split_direction {
2808 self.drag_split_direction = direction;
2809 }
2810 }
2811
2812 pub fn handle_tab_drop(
2813 &mut self,
2814 dragged_tab: &DraggedTab,
2815 ix: usize,
2816 window: &mut Window,
2817 cx: &mut Context<Self>,
2818 ) {
2819 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2820 if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_tab, window, cx) {
2821 return;
2822 }
2823 }
2824 let mut to_pane = cx.entity().clone();
2825 let split_direction = self.drag_split_direction;
2826 let item_id = dragged_tab.item.item_id();
2827 if let Some(preview_item_id) = self.preview_item_id {
2828 if item_id == preview_item_id {
2829 self.set_preview_item_id(None, cx);
2830 }
2831 }
2832
2833 let from_pane = dragged_tab.pane.clone();
2834 self.workspace
2835 .update(cx, |_, cx| {
2836 cx.defer_in(window, move |workspace, window, cx| {
2837 if let Some(split_direction) = split_direction {
2838 to_pane = workspace.split_pane(to_pane, split_direction, window, cx);
2839 }
2840 let old_ix = from_pane.read(cx).index_for_item_id(item_id);
2841 let old_len = to_pane.read(cx).items.len();
2842 move_item(&from_pane, &to_pane, item_id, ix, window, cx);
2843 if to_pane == from_pane {
2844 if let Some(old_index) = old_ix {
2845 to_pane.update(cx, |this, _| {
2846 if old_index < this.pinned_tab_count
2847 && (ix == this.items.len() || ix > this.pinned_tab_count)
2848 {
2849 this.pinned_tab_count -= 1;
2850 } else if this.has_pinned_tabs()
2851 && old_index >= this.pinned_tab_count
2852 && ix < this.pinned_tab_count
2853 {
2854 this.pinned_tab_count += 1;
2855 }
2856 });
2857 }
2858 } else {
2859 to_pane.update(cx, |this, _| {
2860 if this.items.len() > old_len // Did we not deduplicate on drag?
2861 && this.has_pinned_tabs()
2862 && ix < this.pinned_tab_count
2863 {
2864 this.pinned_tab_count += 1;
2865 }
2866 });
2867 from_pane.update(cx, |this, _| {
2868 if let Some(index) = old_ix {
2869 if this.pinned_tab_count > index {
2870 this.pinned_tab_count -= 1;
2871 }
2872 }
2873 })
2874 }
2875 });
2876 })
2877 .log_err();
2878 }
2879
2880 fn handle_dragged_selection_drop(
2881 &mut self,
2882 dragged_selection: &DraggedSelection,
2883 dragged_onto: Option<usize>,
2884 window: &mut Window,
2885 cx: &mut Context<Self>,
2886 ) {
2887 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2888 if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_selection, window, cx)
2889 {
2890 return;
2891 }
2892 }
2893 self.handle_project_entry_drop(
2894 &dragged_selection.active_selection.entry_id,
2895 dragged_onto,
2896 window,
2897 cx,
2898 );
2899 }
2900
2901 fn handle_project_entry_drop(
2902 &mut self,
2903 project_entry_id: &ProjectEntryId,
2904 target: Option<usize>,
2905 window: &mut Window,
2906 cx: &mut Context<Self>,
2907 ) {
2908 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2909 if let ControlFlow::Break(()) = custom_drop_handle(self, project_entry_id, window, cx) {
2910 return;
2911 }
2912 }
2913 let mut to_pane = cx.entity().clone();
2914 let split_direction = self.drag_split_direction;
2915 let project_entry_id = *project_entry_id;
2916 self.workspace
2917 .update(cx, |_, cx| {
2918 cx.defer_in(window, move |workspace, window, cx| {
2919 if let Some(path) = workspace
2920 .project()
2921 .read(cx)
2922 .path_for_entry(project_entry_id, cx)
2923 {
2924 let load_path_task = workspace.load_path(path, window, cx);
2925 cx.spawn_in(window, async move |workspace, cx| {
2926 if let Some((project_entry_id, build_item)) =
2927 load_path_task.await.notify_async_err(cx)
2928 {
2929 let (to_pane, new_item_handle) = workspace
2930 .update_in(cx, |workspace, window, cx| {
2931 if let Some(split_direction) = split_direction {
2932 to_pane = workspace.split_pane(
2933 to_pane,
2934 split_direction,
2935 window,
2936 cx,
2937 );
2938 }
2939 let new_item_handle = to_pane.update(cx, |pane, cx| {
2940 pane.open_item(
2941 project_entry_id,
2942 true,
2943 false,
2944 true,
2945 target,
2946 window,
2947 cx,
2948 build_item,
2949 )
2950 });
2951 (to_pane, new_item_handle)
2952 })
2953 .log_err()?;
2954 to_pane
2955 .update_in(cx, |this, window, cx| {
2956 let Some(index) = this.index_for_item(&*new_item_handle)
2957 else {
2958 return;
2959 };
2960
2961 if target.map_or(false, |target| this.is_tab_pinned(target))
2962 {
2963 this.pin_tab_at(index, window, cx);
2964 }
2965 })
2966 .ok()?
2967 }
2968 Some(())
2969 })
2970 .detach();
2971 };
2972 });
2973 })
2974 .log_err();
2975 }
2976
2977 fn handle_external_paths_drop(
2978 &mut self,
2979 paths: &ExternalPaths,
2980 window: &mut Window,
2981 cx: &mut Context<Self>,
2982 ) {
2983 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2984 if let ControlFlow::Break(()) = custom_drop_handle(self, paths, window, cx) {
2985 return;
2986 }
2987 }
2988 let mut to_pane = cx.entity().clone();
2989 let mut split_direction = self.drag_split_direction;
2990 let paths = paths.paths().to_vec();
2991 let is_remote = self
2992 .workspace
2993 .update(cx, |workspace, cx| {
2994 if workspace.project().read(cx).is_via_collab() {
2995 workspace.show_error(
2996 &anyhow::anyhow!("Cannot drop files on a remote project"),
2997 cx,
2998 );
2999 true
3000 } else {
3001 false
3002 }
3003 })
3004 .unwrap_or(true);
3005 if is_remote {
3006 return;
3007 }
3008
3009 self.workspace
3010 .update(cx, |workspace, cx| {
3011 let fs = Arc::clone(workspace.project().read(cx).fs());
3012 cx.spawn_in(window, async move |workspace, cx| {
3013 let mut is_file_checks = FuturesUnordered::new();
3014 for path in &paths {
3015 is_file_checks.push(fs.is_file(path))
3016 }
3017 let mut has_files_to_open = false;
3018 while let Some(is_file) = is_file_checks.next().await {
3019 if is_file {
3020 has_files_to_open = true;
3021 break;
3022 }
3023 }
3024 drop(is_file_checks);
3025 if !has_files_to_open {
3026 split_direction = None;
3027 }
3028
3029 if let Ok(open_task) = workspace.update_in(cx, |workspace, window, cx| {
3030 if let Some(split_direction) = split_direction {
3031 to_pane = workspace.split_pane(to_pane, split_direction, window, cx);
3032 }
3033 workspace.open_paths(
3034 paths,
3035 OpenOptions {
3036 visible: Some(OpenVisible::OnlyDirectories),
3037 ..Default::default()
3038 },
3039 Some(to_pane.downgrade()),
3040 window,
3041 cx,
3042 )
3043 }) {
3044 let opened_items: Vec<_> = open_task.await;
3045 _ = workspace.update(cx, |workspace, cx| {
3046 for item in opened_items.into_iter().flatten() {
3047 if let Err(e) = item {
3048 workspace.show_error(&e, cx);
3049 }
3050 }
3051 });
3052 }
3053 })
3054 .detach();
3055 })
3056 .log_err();
3057 }
3058
3059 pub fn display_nav_history_buttons(&mut self, display: Option<bool>) {
3060 self.display_nav_history_buttons = display;
3061 }
3062
3063 fn get_non_closeable_item_ids(&self, close_pinned: bool) -> Vec<EntityId> {
3064 if close_pinned {
3065 return vec![];
3066 }
3067
3068 self.items
3069 .iter()
3070 .enumerate()
3071 .filter(|(index, _item)| self.is_tab_pinned(*index))
3072 .map(|(_, item)| item.item_id())
3073 .collect()
3074 }
3075
3076 pub fn drag_split_direction(&self) -> Option<SplitDirection> {
3077 self.drag_split_direction
3078 }
3079
3080 pub fn set_zoom_out_on_close(&mut self, zoom_out_on_close: bool) {
3081 self.zoom_out_on_close = zoom_out_on_close;
3082 }
3083}
3084
3085fn default_render_tab_bar_buttons(
3086 pane: &mut Pane,
3087 window: &mut Window,
3088 cx: &mut Context<Pane>,
3089) -> (Option<AnyElement>, Option<AnyElement>) {
3090 if !pane.has_focus(window, cx) && !pane.context_menu_focused(window, cx) {
3091 return (None, None);
3092 }
3093 // Ideally we would return a vec of elements here to pass directly to the [TabBar]'s
3094 // `end_slot`, but due to needing a view here that isn't possible.
3095 let right_children = h_flex()
3096 // Instead we need to replicate the spacing from the [TabBar]'s `end_slot` here.
3097 .gap(DynamicSpacing::Base04.rems(cx))
3098 .child(
3099 PopoverMenu::new("pane-tab-bar-popover-menu")
3100 .trigger_with_tooltip(
3101 IconButton::new("plus", IconName::Plus).icon_size(IconSize::Small),
3102 Tooltip::text("New..."),
3103 )
3104 .anchor(Corner::TopRight)
3105 .with_handle(pane.new_item_context_menu_handle.clone())
3106 .menu(move |window, cx| {
3107 Some(ContextMenu::build(window, cx, |menu, _, _| {
3108 menu.action("New File", NewFile.boxed_clone())
3109 .action("Open File", ToggleFileFinder::default().boxed_clone())
3110 .separator()
3111 .action(
3112 "Search Project",
3113 DeploySearch {
3114 replace_enabled: false,
3115 }
3116 .boxed_clone(),
3117 )
3118 .action("Search Symbols", ToggleProjectSymbols.boxed_clone())
3119 .separator()
3120 .action("New Terminal", NewTerminal.boxed_clone())
3121 }))
3122 }),
3123 )
3124 .child(
3125 PopoverMenu::new("pane-tab-bar-split")
3126 .trigger_with_tooltip(
3127 IconButton::new("split", IconName::Split).icon_size(IconSize::Small),
3128 Tooltip::text("Split Pane"),
3129 )
3130 .anchor(Corner::TopRight)
3131 .with_handle(pane.split_item_context_menu_handle.clone())
3132 .menu(move |window, cx| {
3133 ContextMenu::build(window, cx, |menu, _, _| {
3134 menu.action("Split Right", SplitRight.boxed_clone())
3135 .action("Split Left", SplitLeft.boxed_clone())
3136 .action("Split Up", SplitUp.boxed_clone())
3137 .action("Split Down", SplitDown.boxed_clone())
3138 })
3139 .into()
3140 }),
3141 )
3142 .child({
3143 let zoomed = pane.is_zoomed();
3144 IconButton::new("toggle_zoom", IconName::Maximize)
3145 .icon_size(IconSize::Small)
3146 .toggle_state(zoomed)
3147 .selected_icon(IconName::Minimize)
3148 .on_click(cx.listener(|pane, _, window, cx| {
3149 pane.toggle_zoom(&crate::ToggleZoom, window, cx);
3150 }))
3151 .tooltip(move |window, cx| {
3152 Tooltip::for_action(
3153 if zoomed { "Zoom Out" } else { "Zoom In" },
3154 &ToggleZoom,
3155 window,
3156 cx,
3157 )
3158 })
3159 })
3160 .into_any_element()
3161 .into();
3162 (None, right_children)
3163}
3164
3165impl Focusable for Pane {
3166 fn focus_handle(&self, _cx: &App) -> FocusHandle {
3167 self.focus_handle.clone()
3168 }
3169}
3170
3171impl Render for Pane {
3172 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3173 let mut key_context = KeyContext::new_with_defaults();
3174 key_context.add("Pane");
3175 if self.active_item().is_none() {
3176 key_context.add("EmptyPane");
3177 }
3178
3179 let should_display_tab_bar = self.should_display_tab_bar.clone();
3180 let display_tab_bar = should_display_tab_bar(window, cx);
3181 let Some(project) = self.project.upgrade() else {
3182 return div().track_focus(&self.focus_handle(cx));
3183 };
3184 let is_local = project.read(cx).is_local();
3185
3186 v_flex()
3187 .key_context(key_context)
3188 .track_focus(&self.focus_handle(cx))
3189 .size_full()
3190 .flex_none()
3191 .overflow_hidden()
3192 .on_action(cx.listener(|pane, _: &AlternateFile, window, cx| {
3193 pane.alternate_file(window, cx);
3194 }))
3195 .on_action(
3196 cx.listener(|pane, _: &SplitLeft, _, cx| pane.split(SplitDirection::Left, cx)),
3197 )
3198 .on_action(cx.listener(|pane, _: &SplitUp, _, cx| pane.split(SplitDirection::Up, cx)))
3199 .on_action(cx.listener(|pane, _: &SplitHorizontal, _, cx| {
3200 pane.split(SplitDirection::horizontal(cx), cx)
3201 }))
3202 .on_action(cx.listener(|pane, _: &SplitVertical, _, cx| {
3203 pane.split(SplitDirection::vertical(cx), cx)
3204 }))
3205 .on_action(
3206 cx.listener(|pane, _: &SplitRight, _, cx| pane.split(SplitDirection::Right, cx)),
3207 )
3208 .on_action(
3209 cx.listener(|pane, _: &SplitDown, _, cx| pane.split(SplitDirection::Down, cx)),
3210 )
3211 .on_action(
3212 cx.listener(|pane, _: &GoBack, window, cx| pane.navigate_backward(window, cx)),
3213 )
3214 .on_action(
3215 cx.listener(|pane, _: &GoForward, window, cx| pane.navigate_forward(window, cx)),
3216 )
3217 .on_action(cx.listener(|_, _: &JoinIntoNext, _, cx| {
3218 cx.emit(Event::JoinIntoNext);
3219 }))
3220 .on_action(cx.listener(|_, _: &JoinAll, _, cx| {
3221 cx.emit(Event::JoinAll);
3222 }))
3223 .on_action(cx.listener(Pane::toggle_zoom))
3224 .on_action(
3225 cx.listener(|pane: &mut Pane, action: &ActivateItem, window, cx| {
3226 pane.activate_item(action.0, true, true, window, cx);
3227 }),
3228 )
3229 .on_action(
3230 cx.listener(|pane: &mut Pane, _: &ActivateLastItem, window, cx| {
3231 pane.activate_item(pane.items.len() - 1, true, true, window, cx);
3232 }),
3233 )
3234 .on_action(
3235 cx.listener(|pane: &mut Pane, _: &ActivatePreviousItem, window, cx| {
3236 pane.activate_prev_item(true, window, cx);
3237 }),
3238 )
3239 .on_action(
3240 cx.listener(|pane: &mut Pane, _: &ActivateNextItem, window, cx| {
3241 pane.activate_next_item(true, window, cx);
3242 }),
3243 )
3244 .on_action(
3245 cx.listener(|pane, _: &SwapItemLeft, window, cx| pane.swap_item_left(window, cx)),
3246 )
3247 .on_action(
3248 cx.listener(|pane, _: &SwapItemRight, window, cx| pane.swap_item_right(window, cx)),
3249 )
3250 .on_action(cx.listener(|pane, action, window, cx| {
3251 pane.toggle_pin_tab(action, window, cx);
3252 }))
3253 .when(PreviewTabsSettings::get_global(cx).enabled, |this| {
3254 this.on_action(cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, _, cx| {
3255 if let Some(active_item_id) = pane.active_item().map(|i| i.item_id()) {
3256 if pane.is_active_preview_item(active_item_id) {
3257 pane.set_preview_item_id(None, cx);
3258 } else {
3259 pane.set_preview_item_id(Some(active_item_id), cx);
3260 }
3261 }
3262 }))
3263 })
3264 .on_action(
3265 cx.listener(|pane: &mut Self, action: &CloseActiveItem, window, cx| {
3266 if let Some(task) = pane.close_active_item(action, window, cx) {
3267 task.detach_and_log_err(cx)
3268 }
3269 }),
3270 )
3271 .on_action(
3272 cx.listener(|pane: &mut Self, action: &CloseInactiveItems, window, cx| {
3273 if let Some(task) = pane.close_inactive_items(action, window, cx) {
3274 task.detach_and_log_err(cx)
3275 }
3276 }),
3277 )
3278 .on_action(
3279 cx.listener(|pane: &mut Self, action: &CloseCleanItems, window, cx| {
3280 if let Some(task) = pane.close_clean_items(action, window, cx) {
3281 task.detach_and_log_err(cx)
3282 }
3283 }),
3284 )
3285 .on_action(cx.listener(
3286 |pane: &mut Self, action: &CloseItemsToTheLeft, window, cx| {
3287 if let Some(task) = pane.close_items_to_the_left(action, window, cx) {
3288 task.detach_and_log_err(cx)
3289 }
3290 },
3291 ))
3292 .on_action(cx.listener(
3293 |pane: &mut Self, action: &CloseItemsToTheRight, window, cx| {
3294 if let Some(task) = pane.close_items_to_the_right(action, window, cx) {
3295 task.detach_and_log_err(cx)
3296 }
3297 },
3298 ))
3299 .on_action(
3300 cx.listener(|pane: &mut Self, action: &CloseAllItems, window, cx| {
3301 if let Some(task) = pane.close_all_items(action, window, cx) {
3302 task.detach_and_log_err(cx)
3303 }
3304 }),
3305 )
3306 .on_action(
3307 cx.listener(|pane: &mut Self, action: &CloseActiveItem, window, cx| {
3308 if let Some(task) = pane.close_active_item(action, window, cx) {
3309 task.detach_and_log_err(cx)
3310 }
3311 }),
3312 )
3313 .on_action(
3314 cx.listener(|pane: &mut Self, action: &RevealInProjectPanel, _, cx| {
3315 let entry_id = action
3316 .entry_id
3317 .map(ProjectEntryId::from_proto)
3318 .or_else(|| pane.active_item()?.project_entry_ids(cx).first().copied());
3319 if let Some(entry_id) = entry_id {
3320 pane.project
3321 .update(cx, |_, cx| {
3322 cx.emit(project::Event::RevealInProjectPanel(entry_id))
3323 })
3324 .ok();
3325 }
3326 }),
3327 )
3328 .when(self.active_item().is_some() && display_tab_bar, |pane| {
3329 pane.child((self.render_tab_bar.clone())(self, window, cx))
3330 })
3331 .child({
3332 let has_worktrees = project.read(cx).visible_worktrees(cx).next().is_some();
3333 // main content
3334 div()
3335 .flex_1()
3336 .relative()
3337 .group("")
3338 .overflow_hidden()
3339 .on_drag_move::<DraggedTab>(cx.listener(Self::handle_drag_move))
3340 .on_drag_move::<DraggedSelection>(cx.listener(Self::handle_drag_move))
3341 .when(is_local, |div| {
3342 div.on_drag_move::<ExternalPaths>(cx.listener(Self::handle_drag_move))
3343 })
3344 .map(|div| {
3345 if let Some(item) = self.active_item() {
3346 div.id("pane_placeholder")
3347 .v_flex()
3348 .size_full()
3349 .overflow_hidden()
3350 .child(self.toolbar.clone())
3351 .child(item.to_any())
3352 } else {
3353 let placeholder = div
3354 .id("pane_placeholder")
3355 .h_flex()
3356 .size_full()
3357 .justify_center()
3358 .on_click(cx.listener(
3359 move |this, event: &ClickEvent, window, cx| {
3360 if event.up.click_count == 2 {
3361 window.dispatch_action(
3362 this.double_click_dispatch_action.boxed_clone(),
3363 cx,
3364 );
3365 }
3366 },
3367 ));
3368 if has_worktrees {
3369 placeholder
3370 } else {
3371 placeholder.child(
3372 Label::new("Open a file or project to get started.")
3373 .color(Color::Muted),
3374 )
3375 }
3376 }
3377 })
3378 .child(
3379 // drag target
3380 div()
3381 .invisible()
3382 .absolute()
3383 .bg(cx.theme().colors().drop_target_background)
3384 .group_drag_over::<DraggedTab>("", |style| style.visible())
3385 .group_drag_over::<DraggedSelection>("", |style| style.visible())
3386 .when(is_local, |div| {
3387 div.group_drag_over::<ExternalPaths>("", |style| style.visible())
3388 })
3389 .when_some(self.can_drop_predicate.clone(), |this, p| {
3390 this.can_drop(move |a, window, cx| p(a, window, cx))
3391 })
3392 .on_drop(cx.listener(move |this, dragged_tab, window, cx| {
3393 this.handle_tab_drop(
3394 dragged_tab,
3395 this.active_item_index(),
3396 window,
3397 cx,
3398 )
3399 }))
3400 .on_drop(cx.listener(
3401 move |this, selection: &DraggedSelection, window, cx| {
3402 this.handle_dragged_selection_drop(selection, None, window, cx)
3403 },
3404 ))
3405 .on_drop(cx.listener(move |this, paths, window, cx| {
3406 this.handle_external_paths_drop(paths, window, cx)
3407 }))
3408 .map(|div| {
3409 let size = DefiniteLength::Fraction(0.5);
3410 match self.drag_split_direction {
3411 None => div.top_0().right_0().bottom_0().left_0(),
3412 Some(SplitDirection::Up) => {
3413 div.top_0().left_0().right_0().h(size)
3414 }
3415 Some(SplitDirection::Down) => {
3416 div.left_0().bottom_0().right_0().h(size)
3417 }
3418 Some(SplitDirection::Left) => {
3419 div.top_0().left_0().bottom_0().w(size)
3420 }
3421 Some(SplitDirection::Right) => {
3422 div.top_0().bottom_0().right_0().w(size)
3423 }
3424 }
3425 }),
3426 )
3427 })
3428 .on_mouse_down(
3429 MouseButton::Navigate(NavigationDirection::Back),
3430 cx.listener(|pane, _, window, cx| {
3431 if let Some(workspace) = pane.workspace.upgrade() {
3432 let pane = cx.entity().downgrade();
3433 window.defer(cx, move |window, cx| {
3434 workspace.update(cx, |workspace, cx| {
3435 workspace.go_back(pane, window, cx).detach_and_log_err(cx)
3436 })
3437 })
3438 }
3439 }),
3440 )
3441 .on_mouse_down(
3442 MouseButton::Navigate(NavigationDirection::Forward),
3443 cx.listener(|pane, _, window, cx| {
3444 if let Some(workspace) = pane.workspace.upgrade() {
3445 let pane = cx.entity().downgrade();
3446 window.defer(cx, move |window, cx| {
3447 workspace.update(cx, |workspace, cx| {
3448 workspace
3449 .go_forward(pane, window, cx)
3450 .detach_and_log_err(cx)
3451 })
3452 })
3453 }
3454 }),
3455 )
3456 }
3457}
3458
3459impl ItemNavHistory {
3460 pub fn push<D: 'static + Send + Any>(&mut self, data: Option<D>, cx: &mut App) {
3461 if self
3462 .item
3463 .upgrade()
3464 .is_some_and(|item| item.include_in_nav_history())
3465 {
3466 self.history
3467 .push(data, self.item.clone(), self.is_preview, cx);
3468 }
3469 }
3470
3471 pub fn pop_backward(&mut self, cx: &mut App) -> Option<NavigationEntry> {
3472 self.history.pop(NavigationMode::GoingBack, cx)
3473 }
3474
3475 pub fn pop_forward(&mut self, cx: &mut App) -> Option<NavigationEntry> {
3476 self.history.pop(NavigationMode::GoingForward, cx)
3477 }
3478}
3479
3480impl NavHistory {
3481 pub fn for_each_entry(
3482 &self,
3483 cx: &App,
3484 mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
3485 ) {
3486 let borrowed_history = self.0.lock();
3487 borrowed_history
3488 .forward_stack
3489 .iter()
3490 .chain(borrowed_history.backward_stack.iter())
3491 .chain(borrowed_history.closed_stack.iter())
3492 .for_each(|entry| {
3493 if let Some(project_and_abs_path) =
3494 borrowed_history.paths_by_item.get(&entry.item.id())
3495 {
3496 f(entry, project_and_abs_path.clone());
3497 } else if let Some(item) = entry.item.upgrade() {
3498 if let Some(path) = item.project_path(cx) {
3499 f(entry, (path, None));
3500 }
3501 }
3502 })
3503 }
3504
3505 pub fn set_mode(&mut self, mode: NavigationMode) {
3506 self.0.lock().mode = mode;
3507 }
3508
3509 pub fn mode(&self) -> NavigationMode {
3510 self.0.lock().mode
3511 }
3512
3513 pub fn disable(&mut self) {
3514 self.0.lock().mode = NavigationMode::Disabled;
3515 }
3516
3517 pub fn enable(&mut self) {
3518 self.0.lock().mode = NavigationMode::Normal;
3519 }
3520
3521 pub fn pop(&mut self, mode: NavigationMode, cx: &mut App) -> Option<NavigationEntry> {
3522 let mut state = self.0.lock();
3523 let entry = match mode {
3524 NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
3525 return None;
3526 }
3527 NavigationMode::GoingBack => &mut state.backward_stack,
3528 NavigationMode::GoingForward => &mut state.forward_stack,
3529 NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
3530 }
3531 .pop_back();
3532 if entry.is_some() {
3533 state.did_update(cx);
3534 }
3535 entry
3536 }
3537
3538 pub fn push<D: 'static + Send + Any>(
3539 &mut self,
3540 data: Option<D>,
3541 item: Arc<dyn WeakItemHandle>,
3542 is_preview: bool,
3543 cx: &mut App,
3544 ) {
3545 let state = &mut *self.0.lock();
3546 match state.mode {
3547 NavigationMode::Disabled => {}
3548 NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
3549 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3550 state.backward_stack.pop_front();
3551 }
3552 state.backward_stack.push_back(NavigationEntry {
3553 item,
3554 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3555 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3556 is_preview,
3557 });
3558 state.forward_stack.clear();
3559 }
3560 NavigationMode::GoingBack => {
3561 if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3562 state.forward_stack.pop_front();
3563 }
3564 state.forward_stack.push_back(NavigationEntry {
3565 item,
3566 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3567 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3568 is_preview,
3569 });
3570 }
3571 NavigationMode::GoingForward => {
3572 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3573 state.backward_stack.pop_front();
3574 }
3575 state.backward_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::ClosingItem => {
3583 if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3584 state.closed_stack.pop_front();
3585 }
3586 state.closed_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 }
3594 state.did_update(cx);
3595 }
3596
3597 pub fn remove_item(&mut self, item_id: EntityId) {
3598 let mut state = self.0.lock();
3599 state.paths_by_item.remove(&item_id);
3600 state
3601 .backward_stack
3602 .retain(|entry| entry.item.id() != item_id);
3603 state
3604 .forward_stack
3605 .retain(|entry| entry.item.id() != item_id);
3606 state
3607 .closed_stack
3608 .retain(|entry| entry.item.id() != item_id);
3609 }
3610
3611 pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option<PathBuf>)> {
3612 self.0.lock().paths_by_item.get(&item_id).cloned()
3613 }
3614}
3615
3616impl NavHistoryState {
3617 pub fn did_update(&self, cx: &mut App) {
3618 if let Some(pane) = self.pane.upgrade() {
3619 cx.defer(move |cx| {
3620 pane.update(cx, |pane, cx| pane.history_updated(cx));
3621 });
3622 }
3623 }
3624}
3625
3626fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
3627 let path = buffer_path
3628 .as_ref()
3629 .and_then(|p| {
3630 p.path
3631 .to_str()
3632 .and_then(|s| if s.is_empty() { None } else { Some(s) })
3633 })
3634 .unwrap_or("This buffer");
3635 let path = truncate_and_remove_front(path, 80);
3636 format!("{path} contains unsaved edits. Do you want to save it?")
3637}
3638
3639pub fn tab_details(items: &[Box<dyn ItemHandle>], _window: &Window, cx: &App) -> Vec<usize> {
3640 let mut tab_details = items.iter().map(|_| 0).collect::<Vec<_>>();
3641 let mut tab_descriptions = HashMap::default();
3642 let mut done = false;
3643 while !done {
3644 done = true;
3645
3646 // Store item indices by their tab description.
3647 for (ix, (item, detail)) in items.iter().zip(&tab_details).enumerate() {
3648 let description = item.tab_content_text(*detail, cx);
3649 if *detail == 0 || description != item.tab_content_text(detail - 1, cx) {
3650 tab_descriptions
3651 .entry(description)
3652 .or_insert(Vec::new())
3653 .push(ix);
3654 }
3655 }
3656
3657 // If two or more items have the same tab description, increase their level
3658 // of detail and try again.
3659 for (_, item_ixs) in tab_descriptions.drain() {
3660 if item_ixs.len() > 1 {
3661 done = false;
3662 for ix in item_ixs {
3663 tab_details[ix] += 1;
3664 }
3665 }
3666 }
3667 }
3668
3669 tab_details
3670}
3671
3672pub fn render_item_indicator(item: Box<dyn ItemHandle>, cx: &App) -> Option<Indicator> {
3673 maybe!({
3674 let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
3675 (true, _) => Color::Warning,
3676 (_, true) => Color::Accent,
3677 (false, false) => return None,
3678 };
3679
3680 Some(Indicator::dot().color(indicator_color))
3681 })
3682}
3683
3684impl Render for DraggedTab {
3685 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3686 let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
3687 let label = self.item.tab_content(
3688 TabContentParams {
3689 detail: Some(self.detail),
3690 selected: false,
3691 preview: false,
3692 deemphasized: false,
3693 },
3694 window,
3695 cx,
3696 );
3697 Tab::new("")
3698 .toggle_state(self.is_active)
3699 .child(label)
3700 .render(window, cx)
3701 .font(ui_font)
3702 }
3703}
3704
3705#[cfg(test)]
3706mod tests {
3707 use std::num::NonZero;
3708
3709 use super::*;
3710 use crate::item::test::{TestItem, TestProjectItem};
3711 use gpui::{TestAppContext, VisualTestContext};
3712 use project::FakeFs;
3713 use settings::SettingsStore;
3714 use theme::LoadThemes;
3715
3716 #[gpui::test]
3717 async fn test_remove_active_empty(cx: &mut TestAppContext) {
3718 init_test(cx);
3719 let fs = FakeFs::new(cx.executor());
3720
3721 let project = Project::test(fs, None, cx).await;
3722 let (workspace, cx) =
3723 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3724 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3725
3726 pane.update_in(cx, |pane, window, cx| {
3727 assert!(
3728 pane.close_active_item(
3729 &CloseActiveItem {
3730 save_intent: None,
3731 close_pinned: false
3732 },
3733 window,
3734 cx
3735 )
3736 .is_none()
3737 )
3738 });
3739 }
3740
3741 #[gpui::test]
3742 async fn test_add_item_capped_to_max_tabs(cx: &mut TestAppContext) {
3743 init_test(cx);
3744 let fs = FakeFs::new(cx.executor());
3745
3746 let project = Project::test(fs, None, cx).await;
3747 let (workspace, cx) =
3748 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3749 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3750
3751 for i in 0..7 {
3752 add_labeled_item(&pane, format!("{}", i).as_str(), false, cx);
3753 }
3754 set_max_tabs(cx, Some(5));
3755 add_labeled_item(&pane, "7", false, cx);
3756 // Remove items to respect the max tab cap.
3757 assert_item_labels(&pane, ["3", "4", "5", "6", "7*"], cx);
3758 pane.update_in(cx, |pane, window, cx| {
3759 pane.activate_item(0, false, false, window, cx);
3760 });
3761 add_labeled_item(&pane, "X", false, cx);
3762 // Respect activation order.
3763 assert_item_labels(&pane, ["3", "X*", "5", "6", "7"], cx);
3764
3765 for i in 0..7 {
3766 add_labeled_item(&pane, format!("D{}", i).as_str(), true, cx);
3767 }
3768 // Keeps dirty items, even over max tab cap.
3769 assert_item_labels(
3770 &pane,
3771 ["D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6*^"],
3772 cx,
3773 );
3774
3775 set_max_tabs(cx, None);
3776 for i in 0..7 {
3777 add_labeled_item(&pane, format!("N{}", i).as_str(), false, cx);
3778 }
3779 // No cap when max tabs is None.
3780 assert_item_labels(
3781 &pane,
3782 [
3783 "D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6^", "N0", "N1", "N2", "N3", "N4",
3784 "N5", "N6*",
3785 ],
3786 cx,
3787 );
3788 }
3789
3790 #[gpui::test]
3791 async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
3792 init_test(cx);
3793 let fs = FakeFs::new(cx.executor());
3794
3795 let project = Project::test(fs, None, cx).await;
3796 let (workspace, cx) =
3797 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3798 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3799
3800 // 1. Add with a destination index
3801 // a. Add before the active item
3802 set_labeled_items(&pane, ["A", "B*", "C"], cx);
3803 pane.update_in(cx, |pane, window, cx| {
3804 pane.add_item(
3805 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3806 false,
3807 false,
3808 Some(0),
3809 window,
3810 cx,
3811 );
3812 });
3813 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
3814
3815 // b. Add after the active item
3816 set_labeled_items(&pane, ["A", "B*", "C"], cx);
3817 pane.update_in(cx, |pane, window, cx| {
3818 pane.add_item(
3819 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3820 false,
3821 false,
3822 Some(2),
3823 window,
3824 cx,
3825 );
3826 });
3827 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3828
3829 // c. Add at the end of the item list (including off the length)
3830 set_labeled_items(&pane, ["A", "B*", "C"], cx);
3831 pane.update_in(cx, |pane, window, cx| {
3832 pane.add_item(
3833 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3834 false,
3835 false,
3836 Some(5),
3837 window,
3838 cx,
3839 );
3840 });
3841 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3842
3843 // 2. Add without a destination index
3844 // a. Add with active item at the start of the item list
3845 set_labeled_items(&pane, ["A*", "B", "C"], cx);
3846 pane.update_in(cx, |pane, window, cx| {
3847 pane.add_item(
3848 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3849 false,
3850 false,
3851 None,
3852 window,
3853 cx,
3854 );
3855 });
3856 set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
3857
3858 // b. Add with active item at the end of the item list
3859 set_labeled_items(&pane, ["A", "B", "C*"], cx);
3860 pane.update_in(cx, |pane, window, cx| {
3861 pane.add_item(
3862 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3863 false,
3864 false,
3865 None,
3866 window,
3867 cx,
3868 );
3869 });
3870 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3871 }
3872
3873 #[gpui::test]
3874 async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
3875 init_test(cx);
3876 let fs = FakeFs::new(cx.executor());
3877
3878 let project = Project::test(fs, None, cx).await;
3879 let (workspace, cx) =
3880 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3881 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3882
3883 // 1. Add with a destination index
3884 // 1a. Add before the active item
3885 let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3886 pane.update_in(cx, |pane, window, cx| {
3887 pane.add_item(d, false, false, Some(0), window, cx);
3888 });
3889 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
3890
3891 // 1b. Add after the active item
3892 let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3893 pane.update_in(cx, |pane, window, cx| {
3894 pane.add_item(d, false, false, Some(2), window, cx);
3895 });
3896 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3897
3898 // 1c. Add at the end of the item list (including off the length)
3899 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3900 pane.update_in(cx, |pane, window, cx| {
3901 pane.add_item(a, false, false, Some(5), window, cx);
3902 });
3903 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3904
3905 // 1d. Add same item to active index
3906 let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3907 pane.update_in(cx, |pane, window, cx| {
3908 pane.add_item(b, false, false, Some(1), window, cx);
3909 });
3910 assert_item_labels(&pane, ["A", "B*", "C"], cx);
3911
3912 // 1e. Add item to index after same item in last position
3913 let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3914 pane.update_in(cx, |pane, window, cx| {
3915 pane.add_item(c, false, false, Some(2), window, cx);
3916 });
3917 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3918
3919 // 2. Add without a destination index
3920 // 2a. Add with active item at the start of the item list
3921 let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
3922 pane.update_in(cx, |pane, window, cx| {
3923 pane.add_item(d, false, false, None, window, cx);
3924 });
3925 assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
3926
3927 // 2b. Add with active item at the end of the item list
3928 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
3929 pane.update_in(cx, |pane, window, cx| {
3930 pane.add_item(a, false, false, None, window, cx);
3931 });
3932 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3933
3934 // 2c. Add active item to active item at end of list
3935 let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
3936 pane.update_in(cx, |pane, window, cx| {
3937 pane.add_item(c, false, false, None, window, cx);
3938 });
3939 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3940
3941 // 2d. Add active item to active item at start of list
3942 let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
3943 pane.update_in(cx, |pane, window, cx| {
3944 pane.add_item(a, false, false, None, window, cx);
3945 });
3946 assert_item_labels(&pane, ["A*", "B", "C"], cx);
3947 }
3948
3949 #[gpui::test]
3950 async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
3951 init_test(cx);
3952 let fs = FakeFs::new(cx.executor());
3953
3954 let project = Project::test(fs, None, cx).await;
3955 let (workspace, cx) =
3956 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3957 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3958
3959 // singleton view
3960 pane.update_in(cx, |pane, window, cx| {
3961 pane.add_item(
3962 Box::new(cx.new(|cx| {
3963 TestItem::new(cx)
3964 .with_singleton(true)
3965 .with_label("buffer 1")
3966 .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
3967 })),
3968 false,
3969 false,
3970 None,
3971 window,
3972 cx,
3973 );
3974 });
3975 assert_item_labels(&pane, ["buffer 1*"], cx);
3976
3977 // new singleton view with the same project entry
3978 pane.update_in(cx, |pane, window, cx| {
3979 pane.add_item(
3980 Box::new(cx.new(|cx| {
3981 TestItem::new(cx)
3982 .with_singleton(true)
3983 .with_label("buffer 1")
3984 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3985 })),
3986 false,
3987 false,
3988 None,
3989 window,
3990 cx,
3991 );
3992 });
3993 assert_item_labels(&pane, ["buffer 1*"], cx);
3994
3995 // new singleton view with different project entry
3996 pane.update_in(cx, |pane, window, cx| {
3997 pane.add_item(
3998 Box::new(cx.new(|cx| {
3999 TestItem::new(cx)
4000 .with_singleton(true)
4001 .with_label("buffer 2")
4002 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
4003 })),
4004 false,
4005 false,
4006 None,
4007 window,
4008 cx,
4009 );
4010 });
4011 assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
4012
4013 // new multibuffer view with the same project entry
4014 pane.update_in(cx, |pane, window, cx| {
4015 pane.add_item(
4016 Box::new(cx.new(|cx| {
4017 TestItem::new(cx)
4018 .with_singleton(false)
4019 .with_label("multibuffer 1")
4020 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4021 })),
4022 false,
4023 false,
4024 None,
4025 window,
4026 cx,
4027 );
4028 });
4029 assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
4030
4031 // another multibuffer view with the same project entry
4032 pane.update_in(cx, |pane, window, cx| {
4033 pane.add_item(
4034 Box::new(cx.new(|cx| {
4035 TestItem::new(cx)
4036 .with_singleton(false)
4037 .with_label("multibuffer 1b")
4038 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4039 })),
4040 false,
4041 false,
4042 None,
4043 window,
4044 cx,
4045 );
4046 });
4047 assert_item_labels(
4048 &pane,
4049 ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
4050 cx,
4051 );
4052 }
4053
4054 #[gpui::test]
4055 async fn test_remove_item_ordering_history(cx: &mut TestAppContext) {
4056 init_test(cx);
4057 let fs = FakeFs::new(cx.executor());
4058
4059 let project = Project::test(fs, None, cx).await;
4060 let (workspace, cx) =
4061 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4062 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4063
4064 add_labeled_item(&pane, "A", false, cx);
4065 add_labeled_item(&pane, "B", false, cx);
4066 add_labeled_item(&pane, "C", false, cx);
4067 add_labeled_item(&pane, "D", false, cx);
4068 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4069
4070 pane.update_in(cx, |pane, window, cx| {
4071 pane.activate_item(1, false, false, window, cx)
4072 });
4073 add_labeled_item(&pane, "1", false, cx);
4074 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
4075
4076 pane.update_in(cx, |pane, window, cx| {
4077 pane.close_active_item(
4078 &CloseActiveItem {
4079 save_intent: None,
4080 close_pinned: false,
4081 },
4082 window,
4083 cx,
4084 )
4085 })
4086 .unwrap()
4087 .await
4088 .unwrap();
4089 assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
4090
4091 pane.update_in(cx, |pane, window, cx| {
4092 pane.activate_item(3, false, false, window, cx)
4093 });
4094 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4095
4096 pane.update_in(cx, |pane, window, cx| {
4097 pane.close_active_item(
4098 &CloseActiveItem {
4099 save_intent: None,
4100 close_pinned: false,
4101 },
4102 window,
4103 cx,
4104 )
4105 })
4106 .unwrap()
4107 .await
4108 .unwrap();
4109 assert_item_labels(&pane, ["A", "B*", "C"], cx);
4110
4111 pane.update_in(cx, |pane, window, cx| {
4112 pane.close_active_item(
4113 &CloseActiveItem {
4114 save_intent: None,
4115 close_pinned: false,
4116 },
4117 window,
4118 cx,
4119 )
4120 })
4121 .unwrap()
4122 .await
4123 .unwrap();
4124 assert_item_labels(&pane, ["A", "C*"], cx);
4125
4126 pane.update_in(cx, |pane, window, cx| {
4127 pane.close_active_item(
4128 &CloseActiveItem {
4129 save_intent: None,
4130 close_pinned: false,
4131 },
4132 window,
4133 cx,
4134 )
4135 })
4136 .unwrap()
4137 .await
4138 .unwrap();
4139 assert_item_labels(&pane, ["A*"], cx);
4140 }
4141
4142 #[gpui::test]
4143 async fn test_remove_item_ordering_neighbour(cx: &mut TestAppContext) {
4144 init_test(cx);
4145 cx.update_global::<SettingsStore, ()>(|s, cx| {
4146 s.update_user_settings::<ItemSettings>(cx, |s| {
4147 s.activate_on_close = Some(ActivateOnClose::Neighbour);
4148 });
4149 });
4150 let fs = FakeFs::new(cx.executor());
4151
4152 let project = Project::test(fs, None, cx).await;
4153 let (workspace, cx) =
4154 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4155 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4156
4157 add_labeled_item(&pane, "A", false, cx);
4158 add_labeled_item(&pane, "B", false, cx);
4159 add_labeled_item(&pane, "C", false, cx);
4160 add_labeled_item(&pane, "D", false, cx);
4161 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4162
4163 pane.update_in(cx, |pane, window, cx| {
4164 pane.activate_item(1, false, false, window, cx)
4165 });
4166 add_labeled_item(&pane, "1", false, cx);
4167 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
4168
4169 pane.update_in(cx, |pane, window, cx| {
4170 pane.close_active_item(
4171 &CloseActiveItem {
4172 save_intent: None,
4173 close_pinned: false,
4174 },
4175 window,
4176 cx,
4177 )
4178 })
4179 .unwrap()
4180 .await
4181 .unwrap();
4182 assert_item_labels(&pane, ["A", "B", "C*", "D"], cx);
4183
4184 pane.update_in(cx, |pane, window, cx| {
4185 pane.activate_item(3, false, false, window, cx)
4186 });
4187 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4188
4189 pane.update_in(cx, |pane, window, cx| {
4190 pane.close_active_item(
4191 &CloseActiveItem {
4192 save_intent: None,
4193 close_pinned: false,
4194 },
4195 window,
4196 cx,
4197 )
4198 })
4199 .unwrap()
4200 .await
4201 .unwrap();
4202 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4203
4204 pane.update_in(cx, |pane, window, cx| {
4205 pane.close_active_item(
4206 &CloseActiveItem {
4207 save_intent: None,
4208 close_pinned: false,
4209 },
4210 window,
4211 cx,
4212 )
4213 })
4214 .unwrap()
4215 .await
4216 .unwrap();
4217 assert_item_labels(&pane, ["A", "B*"], cx);
4218
4219 pane.update_in(cx, |pane, window, cx| {
4220 pane.close_active_item(
4221 &CloseActiveItem {
4222 save_intent: None,
4223 close_pinned: false,
4224 },
4225 window,
4226 cx,
4227 )
4228 })
4229 .unwrap()
4230 .await
4231 .unwrap();
4232 assert_item_labels(&pane, ["A*"], cx);
4233 }
4234
4235 #[gpui::test]
4236 async fn test_remove_item_ordering_left_neighbour(cx: &mut TestAppContext) {
4237 init_test(cx);
4238 cx.update_global::<SettingsStore, ()>(|s, cx| {
4239 s.update_user_settings::<ItemSettings>(cx, |s| {
4240 s.activate_on_close = Some(ActivateOnClose::LeftNeighbour);
4241 });
4242 });
4243 let fs = FakeFs::new(cx.executor());
4244
4245 let project = Project::test(fs, None, cx).await;
4246 let (workspace, cx) =
4247 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4248 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4249
4250 add_labeled_item(&pane, "A", false, cx);
4251 add_labeled_item(&pane, "B", false, cx);
4252 add_labeled_item(&pane, "C", false, cx);
4253 add_labeled_item(&pane, "D", false, cx);
4254 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4255
4256 pane.update_in(cx, |pane, window, cx| {
4257 pane.activate_item(1, false, false, window, cx)
4258 });
4259 add_labeled_item(&pane, "1", false, cx);
4260 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
4261
4262 pane.update_in(cx, |pane, window, cx| {
4263 pane.close_active_item(
4264 &CloseActiveItem {
4265 save_intent: None,
4266 close_pinned: false,
4267 },
4268 window,
4269 cx,
4270 )
4271 })
4272 .unwrap()
4273 .await
4274 .unwrap();
4275 assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
4276
4277 pane.update_in(cx, |pane, window, cx| {
4278 pane.activate_item(3, false, false, window, cx)
4279 });
4280 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4281
4282 pane.update_in(cx, |pane, window, cx| {
4283 pane.close_active_item(
4284 &CloseActiveItem {
4285 save_intent: None,
4286 close_pinned: false,
4287 },
4288 window,
4289 cx,
4290 )
4291 })
4292 .unwrap()
4293 .await
4294 .unwrap();
4295 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4296
4297 pane.update_in(cx, |pane, window, cx| {
4298 pane.activate_item(0, false, false, window, cx)
4299 });
4300 assert_item_labels(&pane, ["A*", "B", "C"], cx);
4301
4302 pane.update_in(cx, |pane, window, cx| {
4303 pane.close_active_item(
4304 &CloseActiveItem {
4305 save_intent: None,
4306 close_pinned: false,
4307 },
4308 window,
4309 cx,
4310 )
4311 })
4312 .unwrap()
4313 .await
4314 .unwrap();
4315 assert_item_labels(&pane, ["B*", "C"], cx);
4316
4317 pane.update_in(cx, |pane, window, cx| {
4318 pane.close_active_item(
4319 &CloseActiveItem {
4320 save_intent: None,
4321 close_pinned: false,
4322 },
4323 window,
4324 cx,
4325 )
4326 })
4327 .unwrap()
4328 .await
4329 .unwrap();
4330 assert_item_labels(&pane, ["C*"], cx);
4331 }
4332
4333 #[gpui::test]
4334 async fn test_close_inactive_items(cx: &mut TestAppContext) {
4335 init_test(cx);
4336 let fs = FakeFs::new(cx.executor());
4337
4338 let project = Project::test(fs, None, cx).await;
4339 let (workspace, cx) =
4340 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4341 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4342
4343 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
4344
4345 pane.update_in(cx, |pane, window, cx| {
4346 pane.close_inactive_items(
4347 &CloseInactiveItems {
4348 save_intent: None,
4349 close_pinned: false,
4350 },
4351 window,
4352 cx,
4353 )
4354 })
4355 .unwrap()
4356 .await
4357 .unwrap();
4358 assert_item_labels(&pane, ["C*"], cx);
4359 }
4360
4361 #[gpui::test]
4362 async fn test_close_clean_items(cx: &mut TestAppContext) {
4363 init_test(cx);
4364 let fs = FakeFs::new(cx.executor());
4365
4366 let project = Project::test(fs, None, cx).await;
4367 let (workspace, cx) =
4368 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4369 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4370
4371 add_labeled_item(&pane, "A", true, cx);
4372 add_labeled_item(&pane, "B", false, cx);
4373 add_labeled_item(&pane, "C", true, cx);
4374 add_labeled_item(&pane, "D", false, cx);
4375 add_labeled_item(&pane, "E", false, cx);
4376 assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
4377
4378 pane.update_in(cx, |pane, window, cx| {
4379 pane.close_clean_items(
4380 &CloseCleanItems {
4381 close_pinned: false,
4382 },
4383 window,
4384 cx,
4385 )
4386 })
4387 .unwrap()
4388 .await
4389 .unwrap();
4390 assert_item_labels(&pane, ["A^", "C*^"], cx);
4391 }
4392
4393 #[gpui::test]
4394 async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
4395 init_test(cx);
4396 let fs = FakeFs::new(cx.executor());
4397
4398 let project = Project::test(fs, None, cx).await;
4399 let (workspace, cx) =
4400 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4401 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4402
4403 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
4404
4405 pane.update_in(cx, |pane, window, cx| {
4406 pane.close_items_to_the_left(
4407 &CloseItemsToTheLeft {
4408 close_pinned: false,
4409 },
4410 window,
4411 cx,
4412 )
4413 })
4414 .unwrap()
4415 .await
4416 .unwrap();
4417 assert_item_labels(&pane, ["C*", "D", "E"], cx);
4418 }
4419
4420 #[gpui::test]
4421 async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
4422 init_test(cx);
4423 let fs = FakeFs::new(cx.executor());
4424
4425 let project = Project::test(fs, None, cx).await;
4426 let (workspace, cx) =
4427 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4428 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4429
4430 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
4431
4432 pane.update_in(cx, |pane, window, cx| {
4433 pane.close_items_to_the_right(
4434 &CloseItemsToTheRight {
4435 close_pinned: false,
4436 },
4437 window,
4438 cx,
4439 )
4440 })
4441 .unwrap()
4442 .await
4443 .unwrap();
4444 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4445 }
4446
4447 #[gpui::test]
4448 async fn test_close_all_items(cx: &mut TestAppContext) {
4449 init_test(cx);
4450 let fs = FakeFs::new(cx.executor());
4451
4452 let project = Project::test(fs, None, cx).await;
4453 let (workspace, cx) =
4454 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4455 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4456
4457 let item_a = add_labeled_item(&pane, "A", false, cx);
4458 add_labeled_item(&pane, "B", false, cx);
4459 add_labeled_item(&pane, "C", false, cx);
4460 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4461
4462 pane.update_in(cx, |pane, window, cx| {
4463 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4464 pane.pin_tab_at(ix, window, cx);
4465 pane.close_all_items(
4466 &CloseAllItems {
4467 save_intent: None,
4468 close_pinned: false,
4469 },
4470 window,
4471 cx,
4472 )
4473 })
4474 .unwrap()
4475 .await
4476 .unwrap();
4477 assert_item_labels(&pane, ["A*"], cx);
4478
4479 pane.update_in(cx, |pane, window, cx| {
4480 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4481 pane.unpin_tab_at(ix, window, cx);
4482 pane.close_all_items(
4483 &CloseAllItems {
4484 save_intent: None,
4485 close_pinned: false,
4486 },
4487 window,
4488 cx,
4489 )
4490 })
4491 .unwrap()
4492 .await
4493 .unwrap();
4494
4495 assert_item_labels(&pane, [], cx);
4496
4497 add_labeled_item(&pane, "A", true, cx).update(cx, |item, cx| {
4498 item.project_items
4499 .push(TestProjectItem::new_dirty(1, "A.txt", cx))
4500 });
4501 add_labeled_item(&pane, "B", true, cx).update(cx, |item, cx| {
4502 item.project_items
4503 .push(TestProjectItem::new_dirty(2, "B.txt", cx))
4504 });
4505 add_labeled_item(&pane, "C", true, cx).update(cx, |item, cx| {
4506 item.project_items
4507 .push(TestProjectItem::new_dirty(3, "C.txt", cx))
4508 });
4509 assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
4510
4511 let save = pane
4512 .update_in(cx, |pane, window, cx| {
4513 pane.close_all_items(
4514 &CloseAllItems {
4515 save_intent: None,
4516 close_pinned: false,
4517 },
4518 window,
4519 cx,
4520 )
4521 })
4522 .unwrap();
4523
4524 cx.executor().run_until_parked();
4525 cx.simulate_prompt_answer("Save all");
4526 save.await.unwrap();
4527 assert_item_labels(&pane, [], cx);
4528
4529 add_labeled_item(&pane, "A", true, cx);
4530 add_labeled_item(&pane, "B", true, cx);
4531 add_labeled_item(&pane, "C", true, cx);
4532 assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
4533 let save = pane
4534 .update_in(cx, |pane, window, cx| {
4535 pane.close_all_items(
4536 &CloseAllItems {
4537 save_intent: None,
4538 close_pinned: false,
4539 },
4540 window,
4541 cx,
4542 )
4543 })
4544 .unwrap();
4545
4546 cx.executor().run_until_parked();
4547 cx.simulate_prompt_answer("Discard all");
4548 save.await.unwrap();
4549 assert_item_labels(&pane, [], cx);
4550 }
4551
4552 #[gpui::test]
4553 async fn test_close_with_save_intent(cx: &mut TestAppContext) {
4554 init_test(cx);
4555 let fs = FakeFs::new(cx.executor());
4556
4557 let project = Project::test(fs, None, cx).await;
4558 let (workspace, cx) =
4559 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
4560 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4561
4562 let a = cx.update(|_, cx| TestProjectItem::new_dirty(1, "A.txt", cx));
4563 let b = cx.update(|_, cx| TestProjectItem::new_dirty(1, "B.txt", cx));
4564 let c = cx.update(|_, cx| TestProjectItem::new_dirty(1, "C.txt", cx));
4565
4566 add_labeled_item(&pane, "AB", true, cx).update(cx, |item, _| {
4567 item.project_items.push(a.clone());
4568 item.project_items.push(b.clone());
4569 });
4570 add_labeled_item(&pane, "C", true, cx)
4571 .update(cx, |item, _| item.project_items.push(c.clone()));
4572 assert_item_labels(&pane, ["AB^", "C*^"], cx);
4573
4574 pane.update_in(cx, |pane, window, cx| {
4575 pane.close_all_items(
4576 &CloseAllItems {
4577 save_intent: Some(SaveIntent::Save),
4578 close_pinned: false,
4579 },
4580 window,
4581 cx,
4582 )
4583 })
4584 .unwrap()
4585 .await
4586 .unwrap();
4587
4588 assert_item_labels(&pane, [], cx);
4589 cx.update(|_, cx| {
4590 assert!(!a.read(cx).is_dirty);
4591 assert!(!b.read(cx).is_dirty);
4592 assert!(!c.read(cx).is_dirty);
4593 });
4594 }
4595
4596 #[gpui::test]
4597 async fn test_close_all_items_including_pinned(cx: &mut TestAppContext) {
4598 init_test(cx);
4599 let fs = FakeFs::new(cx.executor());
4600
4601 let project = Project::test(fs, None, cx).await;
4602 let (workspace, cx) =
4603 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
4604 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4605
4606 let item_a = add_labeled_item(&pane, "A", false, cx);
4607 add_labeled_item(&pane, "B", false, cx);
4608 add_labeled_item(&pane, "C", false, cx);
4609 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4610
4611 pane.update_in(cx, |pane, window, cx| {
4612 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4613 pane.pin_tab_at(ix, window, cx);
4614 pane.close_all_items(
4615 &CloseAllItems {
4616 save_intent: None,
4617 close_pinned: true,
4618 },
4619 window,
4620 cx,
4621 )
4622 })
4623 .unwrap()
4624 .await
4625 .unwrap();
4626 assert_item_labels(&pane, [], cx);
4627 }
4628
4629 #[gpui::test]
4630 async fn test_close_pinned_tab_with_non_pinned_in_same_pane(cx: &mut TestAppContext) {
4631 init_test(cx);
4632 let fs = FakeFs::new(cx.executor());
4633 let project = Project::test(fs, None, cx).await;
4634 let (workspace, cx) =
4635 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
4636
4637 // Non-pinned tabs in same pane
4638 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4639 add_labeled_item(&pane, "A", false, cx);
4640 add_labeled_item(&pane, "B", false, cx);
4641 add_labeled_item(&pane, "C", false, cx);
4642 pane.update_in(cx, |pane, window, cx| {
4643 pane.pin_tab_at(0, window, cx);
4644 });
4645 set_labeled_items(&pane, ["A*", "B", "C"], cx);
4646 pane.update_in(cx, |pane, window, cx| {
4647 pane.close_active_item(
4648 &CloseActiveItem {
4649 save_intent: None,
4650 close_pinned: false,
4651 },
4652 window,
4653 cx,
4654 );
4655 });
4656 // Non-pinned tab should be active
4657 assert_item_labels(&pane, ["A", "B*", "C"], cx);
4658 }
4659
4660 #[gpui::test]
4661 async fn test_close_pinned_tab_with_non_pinned_in_different_pane(cx: &mut TestAppContext) {
4662 init_test(cx);
4663 let fs = FakeFs::new(cx.executor());
4664 let project = Project::test(fs, None, cx).await;
4665 let (workspace, cx) =
4666 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
4667
4668 // No non-pinned tabs in same pane, non-pinned tabs in another pane
4669 let pane1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4670 let pane2 = workspace.update_in(cx, |workspace, window, cx| {
4671 workspace.split_pane(pane1.clone(), SplitDirection::Right, window, cx)
4672 });
4673 add_labeled_item(&pane1, "A", false, cx);
4674 pane1.update_in(cx, |pane, window, cx| {
4675 pane.pin_tab_at(0, window, cx);
4676 });
4677 set_labeled_items(&pane1, ["A*"], cx);
4678 add_labeled_item(&pane2, "B", false, cx);
4679 set_labeled_items(&pane2, ["B"], cx);
4680 pane1.update_in(cx, |pane, window, cx| {
4681 pane.close_active_item(
4682 &CloseActiveItem {
4683 save_intent: None,
4684 close_pinned: false,
4685 },
4686 window,
4687 cx,
4688 );
4689 });
4690 // Non-pinned tab of other pane should be active
4691 assert_item_labels(&pane2, ["B*"], cx);
4692 }
4693
4694 fn init_test(cx: &mut TestAppContext) {
4695 cx.update(|cx| {
4696 let settings_store = SettingsStore::test(cx);
4697 cx.set_global(settings_store);
4698 theme::init(LoadThemes::JustBase, cx);
4699 crate::init_settings(cx);
4700 Project::init_settings(cx);
4701 });
4702 }
4703
4704 fn set_max_tabs(cx: &mut TestAppContext, value: Option<usize>) {
4705 cx.update_global(|store: &mut SettingsStore, cx| {
4706 store.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4707 settings.max_tabs = value.map(|v| NonZero::new(v).unwrap())
4708 });
4709 });
4710 }
4711
4712 fn add_labeled_item(
4713 pane: &Entity<Pane>,
4714 label: &str,
4715 is_dirty: bool,
4716 cx: &mut VisualTestContext,
4717 ) -> Box<Entity<TestItem>> {
4718 pane.update_in(cx, |pane, window, cx| {
4719 let labeled_item =
4720 Box::new(cx.new(|cx| TestItem::new(cx).with_label(label).with_dirty(is_dirty)));
4721 pane.add_item(labeled_item.clone(), false, false, None, window, cx);
4722 labeled_item
4723 })
4724 }
4725
4726 fn set_labeled_items<const COUNT: usize>(
4727 pane: &Entity<Pane>,
4728 labels: [&str; COUNT],
4729 cx: &mut VisualTestContext,
4730 ) -> [Box<Entity<TestItem>>; COUNT] {
4731 pane.update_in(cx, |pane, window, cx| {
4732 pane.items.clear();
4733 let mut active_item_index = 0;
4734
4735 let mut index = 0;
4736 let items = labels.map(|mut label| {
4737 if label.ends_with('*') {
4738 label = label.trim_end_matches('*');
4739 active_item_index = index;
4740 }
4741
4742 let labeled_item = Box::new(cx.new(|cx| TestItem::new(cx).with_label(label)));
4743 pane.add_item(labeled_item.clone(), false, false, None, window, cx);
4744 index += 1;
4745 labeled_item
4746 });
4747
4748 pane.activate_item(active_item_index, false, false, window, cx);
4749
4750 items
4751 })
4752 }
4753
4754 // Assert the item label, with the active item label suffixed with a '*'
4755 #[track_caller]
4756 fn assert_item_labels<const COUNT: usize>(
4757 pane: &Entity<Pane>,
4758 expected_states: [&str; COUNT],
4759 cx: &mut VisualTestContext,
4760 ) {
4761 let actual_states = pane.update(cx, |pane, cx| {
4762 pane.items
4763 .iter()
4764 .enumerate()
4765 .map(|(ix, item)| {
4766 let mut state = item
4767 .to_any()
4768 .downcast::<TestItem>()
4769 .unwrap()
4770 .read(cx)
4771 .label
4772 .clone();
4773 if ix == pane.active_item_index {
4774 state.push('*');
4775 }
4776 if item.is_dirty(cx) {
4777 state.push('^');
4778 }
4779 state
4780 })
4781 .collect::<Vec<_>>()
4782 });
4783 assert_eq!(
4784 actual_states, expected_states,
4785 "pane items do not match expectation"
4786 );
4787 }
4788}