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