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