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