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