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