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