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