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