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.can_save(cx) && 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 left_neighbour_index = || item_index.min(self.items.len()).saturating_sub(1);
1510 let index_to_activate = match activate_on_close {
1511 ActivateOnClose::History => self
1512 .activation_history
1513 .pop()
1514 .and_then(|last_activated_item| {
1515 self.items.iter().enumerate().find_map(|(index, item)| {
1516 (item.item_id() == last_activated_item.entity_id).then_some(index)
1517 })
1518 })
1519 // We didn't have a valid activation history entry, so fallback
1520 // to activating the item to the left
1521 .unwrap_or_else(left_neighbour_index),
1522 ActivateOnClose::Neighbour => {
1523 self.activation_history.pop();
1524 if item_index + 1 < self.items.len() {
1525 item_index + 1
1526 } else {
1527 item_index.saturating_sub(1)
1528 }
1529 }
1530 ActivateOnClose::LeftNeighbour => {
1531 self.activation_history.pop();
1532 left_neighbour_index()
1533 }
1534 };
1535
1536 let should_activate = activate_pane || self.has_focus(cx);
1537 if self.items.len() == 1 && should_activate {
1538 self.focus_handle.focus(cx);
1539 } else {
1540 self.activate_item(index_to_activate, should_activate, should_activate, cx);
1541 }
1542 }
1543
1544 cx.emit(Event::RemoveItem { idx: item_index });
1545
1546 let item = self.items.remove(item_index);
1547
1548 cx.emit(Event::RemovedItem {
1549 item_id: item.item_id(),
1550 });
1551 if self.items.is_empty() {
1552 item.deactivated(cx);
1553 if close_pane_if_empty {
1554 self.update_toolbar(cx);
1555 cx.emit(Event::Remove {
1556 focus_on_pane: focus_on_pane_if_closed,
1557 });
1558 }
1559 }
1560
1561 if item_index < self.active_item_index {
1562 self.active_item_index -= 1;
1563 }
1564
1565 let mode = self.nav_history.mode();
1566 self.nav_history.set_mode(NavigationMode::ClosingItem);
1567 item.deactivated(cx);
1568 self.nav_history.set_mode(mode);
1569
1570 if self.is_active_preview_item(item.item_id()) {
1571 self.set_preview_item_id(None, cx);
1572 }
1573
1574 if let Some(path) = item.project_path(cx) {
1575 let abs_path = self
1576 .nav_history
1577 .0
1578 .lock()
1579 .paths_by_item
1580 .get(&item.item_id())
1581 .and_then(|(_, abs_path)| abs_path.clone());
1582
1583 self.nav_history
1584 .0
1585 .lock()
1586 .paths_by_item
1587 .insert(item.item_id(), (path, abs_path));
1588 } else {
1589 self.nav_history
1590 .0
1591 .lock()
1592 .paths_by_item
1593 .remove(&item.item_id());
1594 }
1595
1596 if self.zoom_out_on_close && self.items.is_empty() && close_pane_if_empty && self.zoomed {
1597 cx.emit(Event::ZoomOut);
1598 }
1599
1600 cx.notify();
1601 }
1602
1603 pub async fn save_item(
1604 project: Model<Project>,
1605 pane: &WeakView<Pane>,
1606 item_ix: usize,
1607 item: &dyn ItemHandle,
1608 save_intent: SaveIntent,
1609 cx: &mut AsyncWindowContext,
1610 ) -> Result<bool> {
1611 const CONFLICT_MESSAGE: &str =
1612 "This file has changed on disk since you started editing it. Do you want to overwrite it?";
1613
1614 const DELETED_MESSAGE: &str =
1615 "This file has been deleted on disk since you started editing it. Do you want to recreate it?";
1616
1617 if save_intent == SaveIntent::Skip {
1618 return Ok(true);
1619 }
1620
1621 let (mut has_conflict, mut is_dirty, mut can_save, is_singleton, has_deleted_file) = cx
1622 .update(|cx| {
1623 (
1624 item.has_conflict(cx),
1625 item.is_dirty(cx),
1626 item.can_save(cx),
1627 item.is_singleton(cx),
1628 item.has_deleted_file(cx),
1629 )
1630 })?;
1631
1632 let can_save_as = is_singleton;
1633
1634 // when saving a single buffer, we ignore whether or not it's dirty.
1635 if save_intent == SaveIntent::Save || save_intent == SaveIntent::SaveWithoutFormat {
1636 is_dirty = true;
1637 }
1638
1639 if save_intent == SaveIntent::SaveAs {
1640 is_dirty = true;
1641 has_conflict = false;
1642 can_save = false;
1643 }
1644
1645 if save_intent == SaveIntent::Overwrite {
1646 has_conflict = false;
1647 }
1648
1649 let should_format = save_intent != SaveIntent::SaveWithoutFormat;
1650
1651 if has_conflict && can_save {
1652 if has_deleted_file && is_singleton {
1653 let answer = pane.update(cx, |pane, cx| {
1654 pane.activate_item(item_ix, true, true, cx);
1655 cx.prompt(
1656 PromptLevel::Warning,
1657 DELETED_MESSAGE,
1658 None,
1659 &["Save", "Close", "Cancel"],
1660 )
1661 })?;
1662 match answer.await {
1663 Ok(0) => {
1664 pane.update(cx, |_, cx| item.save(should_format, project, cx))?
1665 .await?
1666 }
1667 Ok(1) => {
1668 pane.update(cx, |pane, cx| {
1669 pane.remove_item(item.item_id(), false, false, cx)
1670 })?;
1671 }
1672 _ => return Ok(false),
1673 }
1674 return Ok(true);
1675 } else {
1676 let answer = pane.update(cx, |pane, cx| {
1677 pane.activate_item(item_ix, true, true, cx);
1678 cx.prompt(
1679 PromptLevel::Warning,
1680 CONFLICT_MESSAGE,
1681 None,
1682 &["Overwrite", "Discard", "Cancel"],
1683 )
1684 })?;
1685 match answer.await {
1686 Ok(0) => {
1687 pane.update(cx, |_, cx| item.save(should_format, project, cx))?
1688 .await?
1689 }
1690 Ok(1) => pane.update(cx, |_, cx| item.reload(project, cx))?.await?,
1691 _ => return Ok(false),
1692 }
1693 }
1694 } else if is_dirty && (can_save || can_save_as) {
1695 if save_intent == SaveIntent::Close {
1696 let will_autosave = cx.update(|cx| {
1697 matches!(
1698 item.workspace_settings(cx).autosave,
1699 AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange
1700 ) && Self::can_autosave_item(item, cx)
1701 })?;
1702 if !will_autosave {
1703 let item_id = item.item_id();
1704 let answer_task = pane.update(cx, |pane, cx| {
1705 if pane.save_modals_spawned.insert(item_id) {
1706 pane.activate_item(item_ix, true, true, cx);
1707 let prompt = dirty_message_for(item.project_path(cx));
1708 Some(cx.prompt(
1709 PromptLevel::Warning,
1710 &prompt,
1711 None,
1712 &["Save", "Don't Save", "Cancel"],
1713 ))
1714 } else {
1715 None
1716 }
1717 })?;
1718 if let Some(answer_task) = answer_task {
1719 let answer = answer_task.await;
1720 pane.update(cx, |pane, _| {
1721 if !pane.save_modals_spawned.remove(&item_id) {
1722 debug_panic!(
1723 "save modal was not present in spawned modals after awaiting for its answer"
1724 )
1725 }
1726 })?;
1727 match answer {
1728 Ok(0) => {}
1729 Ok(1) => {
1730 // Don't save this file
1731 pane.update(cx, |pane, cx| {
1732 if pane.is_tab_pinned(item_ix) && !item.can_save(cx) {
1733 pane.pinned_tab_count -= 1;
1734 }
1735 item.discarded(project, cx)
1736 })
1737 .log_err();
1738 return Ok(true);
1739 }
1740 _ => return Ok(false), // Cancel
1741 }
1742 } else {
1743 return Ok(false);
1744 }
1745 }
1746 }
1747
1748 if can_save {
1749 pane.update(cx, |pane, cx| {
1750 if pane.is_active_preview_item(item.item_id()) {
1751 pane.set_preview_item_id(None, cx);
1752 }
1753 item.save(should_format, project, cx)
1754 })?
1755 .await?;
1756 } else if can_save_as {
1757 let abs_path = pane.update(cx, |pane, cx| {
1758 pane.workspace
1759 .update(cx, |workspace, cx| workspace.prompt_for_new_path(cx))
1760 })??;
1761 if let Some(abs_path) = abs_path.await.ok().flatten() {
1762 pane.update(cx, |pane, cx| {
1763 if let Some(item) = pane.item_for_path(abs_path.clone(), cx) {
1764 pane.remove_item(item.item_id(), false, false, cx);
1765 }
1766
1767 item.save_as(project, abs_path, cx)
1768 })?
1769 .await?;
1770 } else {
1771 return Ok(false);
1772 }
1773 }
1774 }
1775
1776 pane.update(cx, |_, cx| {
1777 cx.emit(Event::UserSavedItem {
1778 item: item.downgrade_item(),
1779 save_intent,
1780 });
1781 true
1782 })
1783 }
1784
1785 fn can_autosave_item(item: &dyn ItemHandle, cx: &AppContext) -> bool {
1786 let is_deleted = item.project_entry_ids(cx).is_empty();
1787 item.is_dirty(cx) && !item.has_conflict(cx) && item.can_save(cx) && !is_deleted
1788 }
1789
1790 pub fn autosave_item(
1791 item: &dyn ItemHandle,
1792 project: Model<Project>,
1793 cx: &mut WindowContext,
1794 ) -> Task<Result<()>> {
1795 let format = !matches!(
1796 item.workspace_settings(cx).autosave,
1797 AutosaveSetting::AfterDelay { .. }
1798 );
1799 if Self::can_autosave_item(item, cx) {
1800 item.save(format, project, cx)
1801 } else {
1802 Task::ready(Ok(()))
1803 }
1804 }
1805
1806 pub fn focus(&mut self, cx: &mut ViewContext<Pane>) {
1807 cx.focus(&self.focus_handle);
1808 }
1809
1810 pub fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
1811 if let Some(active_item) = self.active_item() {
1812 let focus_handle = active_item.focus_handle(cx);
1813 cx.focus(&focus_handle);
1814 }
1815 }
1816
1817 pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext<Self>) {
1818 cx.emit(Event::Split(direction));
1819 }
1820
1821 pub fn toolbar(&self) -> &View<Toolbar> {
1822 &self.toolbar
1823 }
1824
1825 pub fn handle_deleted_project_item(
1826 &mut self,
1827 entry_id: ProjectEntryId,
1828 cx: &mut ViewContext<Pane>,
1829 ) -> Option<()> {
1830 let item_id = self.items().find_map(|item| {
1831 if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
1832 Some(item.item_id())
1833 } else {
1834 None
1835 }
1836 })?;
1837
1838 self.remove_item(item_id, false, true, cx);
1839 self.nav_history.remove_item(item_id);
1840
1841 Some(())
1842 }
1843
1844 fn update_toolbar(&mut self, cx: &mut ViewContext<Self>) {
1845 let active_item = self
1846 .items
1847 .get(self.active_item_index)
1848 .map(|item| item.as_ref());
1849 self.toolbar.update(cx, |toolbar, cx| {
1850 toolbar.set_active_item(active_item, cx);
1851 });
1852 }
1853
1854 fn update_status_bar(&mut self, cx: &mut ViewContext<Self>) {
1855 let workspace = self.workspace.clone();
1856 let pane = cx.view().clone();
1857
1858 cx.window_context().defer(move |cx| {
1859 let Ok(status_bar) = workspace.update(cx, |workspace, _| workspace.status_bar.clone())
1860 else {
1861 return;
1862 };
1863
1864 status_bar.update(cx, move |status_bar, cx| {
1865 status_bar.set_active_pane(&pane, cx);
1866 });
1867 });
1868 }
1869
1870 fn entry_abs_path(&self, entry: ProjectEntryId, cx: &WindowContext) -> Option<PathBuf> {
1871 let worktree = self
1872 .workspace
1873 .upgrade()?
1874 .read(cx)
1875 .project()
1876 .read(cx)
1877 .worktree_for_entry(entry, cx)?
1878 .read(cx);
1879 let entry = worktree.entry_for_id(entry)?;
1880 match &entry.canonical_path {
1881 Some(canonical_path) => Some(canonical_path.to_path_buf()),
1882 None => worktree.absolutize(&entry.path).ok(),
1883 }
1884 }
1885
1886 pub fn icon_color(selected: bool) -> Color {
1887 if selected {
1888 Color::Default
1889 } else {
1890 Color::Muted
1891 }
1892 }
1893
1894 fn toggle_pin_tab(&mut self, _: &TogglePinTab, cx: &mut ViewContext<'_, Self>) {
1895 if self.items.is_empty() {
1896 return;
1897 }
1898 let active_tab_ix = self.active_item_index();
1899 if self.is_tab_pinned(active_tab_ix) {
1900 self.unpin_tab_at(active_tab_ix, cx);
1901 } else {
1902 self.pin_tab_at(active_tab_ix, cx);
1903 }
1904 }
1905
1906 fn pin_tab_at(&mut self, ix: usize, cx: &mut ViewContext<'_, Self>) {
1907 maybe!({
1908 let pane = cx.view().clone();
1909 let destination_index = self.pinned_tab_count.min(ix);
1910 self.pinned_tab_count += 1;
1911 let id = self.item_for_index(ix)?.item_id();
1912
1913 self.workspace
1914 .update(cx, |_, cx| {
1915 cx.defer(move |_, cx| move_item(&pane, &pane, id, destination_index, cx));
1916 })
1917 .ok()?;
1918
1919 Some(())
1920 });
1921 }
1922
1923 fn unpin_tab_at(&mut self, ix: usize, cx: &mut ViewContext<'_, Self>) {
1924 maybe!({
1925 let pane = cx.view().clone();
1926 self.pinned_tab_count = self.pinned_tab_count.checked_sub(1)?;
1927 let destination_index = self.pinned_tab_count;
1928
1929 let id = self.item_for_index(ix)?.item_id();
1930
1931 self.workspace
1932 .update(cx, |_, cx| {
1933 cx.defer(move |_, cx| move_item(&pane, &pane, id, destination_index, cx));
1934 })
1935 .ok()?;
1936
1937 Some(())
1938 });
1939 }
1940
1941 fn is_tab_pinned(&self, ix: usize) -> bool {
1942 self.pinned_tab_count > ix
1943 }
1944
1945 fn has_pinned_tabs(&self) -> bool {
1946 self.pinned_tab_count != 0
1947 }
1948
1949 fn render_tab(
1950 &self,
1951 ix: usize,
1952 item: &dyn ItemHandle,
1953 detail: usize,
1954 focus_handle: &FocusHandle,
1955 cx: &mut ViewContext<'_, Pane>,
1956 ) -> impl IntoElement {
1957 let is_active = ix == self.active_item_index;
1958 let is_preview = self
1959 .preview_item_id
1960 .map(|id| id == item.item_id())
1961 .unwrap_or(false);
1962
1963 let label = item.tab_content(
1964 TabContentParams {
1965 detail: Some(detail),
1966 selected: is_active,
1967 preview: is_preview,
1968 },
1969 cx,
1970 );
1971
1972 let item_diagnostic = item
1973 .project_path(cx)
1974 .map_or(None, |project_path| self.diagnostics.get(&project_path));
1975
1976 let decorated_icon = item_diagnostic.map_or(None, |diagnostic| {
1977 let icon = match item.tab_icon(cx) {
1978 Some(icon) => icon,
1979 None => return None,
1980 };
1981
1982 let knockout_item_color = if is_active {
1983 cx.theme().colors().tab_active_background
1984 } else {
1985 cx.theme().colors().tab_bar_background
1986 };
1987
1988 let (icon_decoration, icon_color) = if matches!(diagnostic, &DiagnosticSeverity::ERROR)
1989 {
1990 (IconDecorationKind::X, Color::Error)
1991 } else {
1992 (IconDecorationKind::Triangle, Color::Warning)
1993 };
1994
1995 Some(DecoratedIcon::new(
1996 icon.size(IconSize::Small).color(Color::Muted),
1997 Some(
1998 IconDecoration::new(icon_decoration, knockout_item_color, cx)
1999 .color(icon_color.color(cx))
2000 .position(Point {
2001 x: px(-2.),
2002 y: px(-2.),
2003 }),
2004 ),
2005 ))
2006 });
2007
2008 let icon = if decorated_icon.is_none() {
2009 match item_diagnostic {
2010 Some(&DiagnosticSeverity::ERROR) => None,
2011 Some(&DiagnosticSeverity::WARNING) => None,
2012 _ => item.tab_icon(cx).map(|icon| icon.color(Color::Muted)),
2013 }
2014 .map(|icon| icon.size(IconSize::Small))
2015 } else {
2016 None
2017 };
2018
2019 let settings = ItemSettings::get_global(cx);
2020 let close_side = &settings.close_position;
2021 let always_show_close_button = settings.always_show_close_button;
2022 let indicator = render_item_indicator(item.boxed_clone(), cx);
2023 let item_id = item.item_id();
2024 let is_first_item = ix == 0;
2025 let is_last_item = ix == self.items.len() - 1;
2026 let is_pinned = self.is_tab_pinned(ix);
2027 let position_relative_to_active_item = ix.cmp(&self.active_item_index);
2028
2029 let tab = Tab::new(ix)
2030 .position(if is_first_item {
2031 TabPosition::First
2032 } else if is_last_item {
2033 TabPosition::Last
2034 } else {
2035 TabPosition::Middle(position_relative_to_active_item)
2036 })
2037 .close_side(match close_side {
2038 ClosePosition::Left => ui::TabCloseSide::Start,
2039 ClosePosition::Right => ui::TabCloseSide::End,
2040 })
2041 .selected(is_active)
2042 .on_click(
2043 cx.listener(move |pane: &mut Self, _, cx| pane.activate_item(ix, true, true, cx)),
2044 )
2045 // TODO: This should be a click listener with the middle mouse button instead of a mouse down listener.
2046 .on_mouse_down(
2047 MouseButton::Middle,
2048 cx.listener(move |pane, _event, cx| {
2049 pane.close_item_by_id(item_id, SaveIntent::Close, cx)
2050 .detach_and_log_err(cx);
2051 }),
2052 )
2053 .on_mouse_down(
2054 MouseButton::Left,
2055 cx.listener(move |pane, event: &MouseDownEvent, cx| {
2056 if let Some(id) = pane.preview_item_id {
2057 if id == item_id && event.click_count > 1 {
2058 pane.set_preview_item_id(None, cx);
2059 }
2060 }
2061 }),
2062 )
2063 .on_drag(
2064 DraggedTab {
2065 item: item.boxed_clone(),
2066 pane: cx.view().clone(),
2067 detail,
2068 is_active,
2069 ix,
2070 },
2071 |tab, _, cx| cx.new_view(|_| tab.clone()),
2072 )
2073 .drag_over::<DraggedTab>(|tab, _, cx| {
2074 tab.bg(cx.theme().colors().drop_target_background)
2075 })
2076 .drag_over::<DraggedSelection>(|tab, _, cx| {
2077 tab.bg(cx.theme().colors().drop_target_background)
2078 })
2079 .when_some(self.can_drop_predicate.clone(), |this, p| {
2080 this.can_drop(move |a, cx| p(a, cx))
2081 })
2082 .on_drop(cx.listener(move |this, dragged_tab: &DraggedTab, cx| {
2083 this.drag_split_direction = None;
2084 this.handle_tab_drop(dragged_tab, ix, cx)
2085 }))
2086 .on_drop(cx.listener(move |this, selection: &DraggedSelection, cx| {
2087 this.drag_split_direction = None;
2088 this.handle_dragged_selection_drop(selection, Some(ix), cx)
2089 }))
2090 .on_drop(cx.listener(move |this, paths, cx| {
2091 this.drag_split_direction = None;
2092 this.handle_external_paths_drop(paths, cx)
2093 }))
2094 .when_some(item.tab_tooltip_text(cx), |tab, text| {
2095 tab.tooltip(move |cx| Tooltip::text(text.clone(), cx))
2096 })
2097 .start_slot::<Indicator>(indicator)
2098 .map(|this| {
2099 let end_slot_action: &'static dyn Action;
2100 let end_slot_tooltip_text: &'static str;
2101 let end_slot = if is_pinned {
2102 end_slot_action = &TogglePinTab;
2103 end_slot_tooltip_text = "Unpin Tab";
2104 IconButton::new("unpin tab", IconName::Pin)
2105 .shape(IconButtonShape::Square)
2106 .icon_color(Color::Muted)
2107 .size(ButtonSize::None)
2108 .icon_size(IconSize::XSmall)
2109 .on_click(cx.listener(move |pane, _, cx| {
2110 pane.unpin_tab_at(ix, cx);
2111 }))
2112 } else {
2113 end_slot_action = &CloseActiveItem { save_intent: None };
2114 end_slot_tooltip_text = "Close Tab";
2115 IconButton::new("close tab", IconName::Close)
2116 .when(!always_show_close_button, |button| {
2117 button.visible_on_hover("")
2118 })
2119 .shape(IconButtonShape::Square)
2120 .icon_color(Color::Muted)
2121 .size(ButtonSize::None)
2122 .icon_size(IconSize::XSmall)
2123 .on_click(cx.listener(move |pane, _, cx| {
2124 pane.close_item_by_id(item_id, SaveIntent::Close, cx)
2125 .detach_and_log_err(cx);
2126 }))
2127 }
2128 .map(|this| {
2129 if is_active {
2130 let focus_handle = focus_handle.clone();
2131 this.tooltip(move |cx| {
2132 Tooltip::for_action_in(
2133 end_slot_tooltip_text,
2134 end_slot_action,
2135 &focus_handle,
2136 cx,
2137 )
2138 })
2139 } else {
2140 this.tooltip(move |cx| Tooltip::text(end_slot_tooltip_text, cx))
2141 }
2142 });
2143 this.end_slot(end_slot)
2144 })
2145 .child(
2146 h_flex()
2147 .gap_1()
2148 .items_center()
2149 .children(
2150 std::iter::once(if let Some(decorated_icon) = decorated_icon {
2151 Some(div().child(decorated_icon.into_any_element()))
2152 } else if let Some(icon) = icon {
2153 Some(div().child(icon.into_any_element()))
2154 } else {
2155 None
2156 })
2157 .flatten(),
2158 )
2159 .child(label),
2160 );
2161
2162 let single_entry_to_resolve = {
2163 let item_entries = self.items[ix].project_entry_ids(cx);
2164 if item_entries.len() == 1 {
2165 Some(item_entries[0])
2166 } else {
2167 None
2168 }
2169 };
2170
2171 let is_pinned = self.is_tab_pinned(ix);
2172 let pane = cx.view().downgrade();
2173 let menu_context = item.focus_handle(cx);
2174 right_click_menu(ix).trigger(tab).menu(move |cx| {
2175 let pane = pane.clone();
2176 let menu_context = menu_context.clone();
2177 ContextMenu::build(cx, move |mut menu, cx| {
2178 if let Some(pane) = pane.upgrade() {
2179 menu = menu
2180 .entry(
2181 "Close",
2182 Some(Box::new(CloseActiveItem { save_intent: None })),
2183 cx.handler_for(&pane, move |pane, cx| {
2184 pane.close_item_by_id(item_id, SaveIntent::Close, cx)
2185 .detach_and_log_err(cx);
2186 }),
2187 )
2188 .entry(
2189 "Close Others",
2190 Some(Box::new(CloseInactiveItems {
2191 save_intent: None,
2192 close_pinned: false,
2193 })),
2194 cx.handler_for(&pane, move |pane, cx| {
2195 pane.close_items(cx, SaveIntent::Close, |id| id != item_id)
2196 .detach_and_log_err(cx);
2197 }),
2198 )
2199 .separator()
2200 .entry(
2201 "Close Left",
2202 Some(Box::new(CloseItemsToTheLeft {
2203 close_pinned: false,
2204 })),
2205 cx.handler_for(&pane, move |pane, cx| {
2206 pane.close_items_to_the_left_by_id(
2207 item_id,
2208 pane.get_non_closeable_item_ids(false),
2209 cx,
2210 )
2211 .detach_and_log_err(cx);
2212 }),
2213 )
2214 .entry(
2215 "Close Right",
2216 Some(Box::new(CloseItemsToTheRight {
2217 close_pinned: false,
2218 })),
2219 cx.handler_for(&pane, move |pane, cx| {
2220 pane.close_items_to_the_right_by_id(
2221 item_id,
2222 pane.get_non_closeable_item_ids(false),
2223 cx,
2224 )
2225 .detach_and_log_err(cx);
2226 }),
2227 )
2228 .separator()
2229 .entry(
2230 "Close Clean",
2231 Some(Box::new(CloseCleanItems {
2232 close_pinned: false,
2233 })),
2234 cx.handler_for(&pane, move |pane, cx| {
2235 if let Some(task) = pane.close_clean_items(
2236 &CloseCleanItems {
2237 close_pinned: false,
2238 },
2239 cx,
2240 ) {
2241 task.detach_and_log_err(cx)
2242 }
2243 }),
2244 )
2245 .entry(
2246 "Close All",
2247 Some(Box::new(CloseAllItems {
2248 save_intent: None,
2249 close_pinned: false,
2250 })),
2251 cx.handler_for(&pane, |pane, cx| {
2252 if let Some(task) = pane.close_all_items(
2253 &CloseAllItems {
2254 save_intent: None,
2255 close_pinned: false,
2256 },
2257 cx,
2258 ) {
2259 task.detach_and_log_err(cx)
2260 }
2261 }),
2262 );
2263
2264 let pin_tab_entries = |menu: ContextMenu| {
2265 menu.separator().map(|this| {
2266 if is_pinned {
2267 this.entry(
2268 "Unpin Tab",
2269 Some(TogglePinTab.boxed_clone()),
2270 cx.handler_for(&pane, move |pane, cx| {
2271 pane.unpin_tab_at(ix, cx);
2272 }),
2273 )
2274 } else {
2275 this.entry(
2276 "Pin Tab",
2277 Some(TogglePinTab.boxed_clone()),
2278 cx.handler_for(&pane, move |pane, cx| {
2279 pane.pin_tab_at(ix, cx);
2280 }),
2281 )
2282 }
2283 })
2284 };
2285 if let Some(entry) = single_entry_to_resolve {
2286 let entry_abs_path = pane.read(cx).entry_abs_path(entry, cx);
2287 let parent_abs_path = entry_abs_path
2288 .as_deref()
2289 .and_then(|abs_path| Some(abs_path.parent()?.to_path_buf()));
2290 let relative_path = pane
2291 .read(cx)
2292 .item_for_entry(entry, cx)
2293 .and_then(|item| item.project_path(cx))
2294 .map(|project_path| project_path.path);
2295
2296 let entry_id = entry.to_proto();
2297 menu = menu
2298 .separator()
2299 .when_some(entry_abs_path, |menu, abs_path| {
2300 menu.entry(
2301 "Copy Path",
2302 Some(Box::new(CopyPath)),
2303 cx.handler_for(&pane, move |_, cx| {
2304 cx.write_to_clipboard(ClipboardItem::new_string(
2305 abs_path.to_string_lossy().to_string(),
2306 ));
2307 }),
2308 )
2309 })
2310 .when_some(relative_path, |menu, relative_path| {
2311 menu.entry(
2312 "Copy Relative Path",
2313 Some(Box::new(CopyRelativePath)),
2314 cx.handler_for(&pane, move |_, cx| {
2315 cx.write_to_clipboard(ClipboardItem::new_string(
2316 relative_path.to_string_lossy().to_string(),
2317 ));
2318 }),
2319 )
2320 })
2321 .map(pin_tab_entries)
2322 .separator()
2323 .entry(
2324 "Reveal In Project Panel",
2325 Some(Box::new(RevealInProjectPanel {
2326 entry_id: Some(entry_id),
2327 })),
2328 cx.handler_for(&pane, move |pane, cx| {
2329 pane.project.update(cx, |_, cx| {
2330 cx.emit(project::Event::RevealInProjectPanel(
2331 ProjectEntryId::from_proto(entry_id),
2332 ))
2333 });
2334 }),
2335 )
2336 .when_some(parent_abs_path, |menu, parent_abs_path| {
2337 menu.entry(
2338 "Open in Terminal",
2339 Some(Box::new(OpenInTerminal)),
2340 cx.handler_for(&pane, move |_, cx| {
2341 cx.dispatch_action(
2342 OpenTerminal {
2343 working_directory: parent_abs_path.clone(),
2344 }
2345 .boxed_clone(),
2346 );
2347 }),
2348 )
2349 });
2350 } else {
2351 menu = menu.map(pin_tab_entries);
2352 }
2353 }
2354
2355 menu.context(menu_context)
2356 })
2357 })
2358 }
2359
2360 fn render_tab_bar(&mut self, cx: &mut ViewContext<'_, Pane>) -> impl IntoElement {
2361 let focus_handle = self.focus_handle.clone();
2362 let navigate_backward = IconButton::new("navigate_backward", IconName::ArrowLeft)
2363 .icon_size(IconSize::Small)
2364 .on_click({
2365 let view = cx.view().clone();
2366 move |_, cx| view.update(cx, Self::navigate_backward)
2367 })
2368 .disabled(!self.can_navigate_backward())
2369 .tooltip({
2370 let focus_handle = focus_handle.clone();
2371 move |cx| Tooltip::for_action_in("Go Back", &GoBack, &focus_handle, cx)
2372 });
2373
2374 let navigate_forward = IconButton::new("navigate_forward", IconName::ArrowRight)
2375 .icon_size(IconSize::Small)
2376 .on_click({
2377 let view = cx.view().clone();
2378 move |_, cx| view.update(cx, Self::navigate_forward)
2379 })
2380 .disabled(!self.can_navigate_forward())
2381 .tooltip({
2382 let focus_handle = focus_handle.clone();
2383 move |cx| Tooltip::for_action_in("Go Forward", &GoForward, &focus_handle, cx)
2384 });
2385
2386 let mut tab_items = self
2387 .items
2388 .iter()
2389 .enumerate()
2390 .zip(tab_details(&self.items, cx))
2391 .map(|((ix, item), detail)| self.render_tab(ix, &**item, detail, &focus_handle, cx))
2392 .collect::<Vec<_>>();
2393 let tab_count = tab_items.len();
2394 let unpinned_tabs = tab_items.split_off(self.pinned_tab_count);
2395 let pinned_tabs = tab_items;
2396 TabBar::new("tab_bar")
2397 .when(
2398 self.display_nav_history_buttons.unwrap_or_default(),
2399 |tab_bar| {
2400 tab_bar
2401 .start_child(navigate_backward)
2402 .start_child(navigate_forward)
2403 },
2404 )
2405 .map(|tab_bar| {
2406 let render_tab_buttons = self.render_tab_bar_buttons.clone();
2407 let (left_children, right_children) = render_tab_buttons(self, cx);
2408
2409 tab_bar
2410 .start_children(left_children)
2411 .end_children(right_children)
2412 })
2413 .children(pinned_tabs.len().ne(&0).then(|| {
2414 h_flex()
2415 .children(pinned_tabs)
2416 .border_r_2()
2417 .border_color(cx.theme().colors().border)
2418 }))
2419 .child(
2420 h_flex()
2421 .id("unpinned tabs")
2422 .overflow_x_scroll()
2423 .w_full()
2424 .track_scroll(&self.tab_bar_scroll_handle)
2425 .children(unpinned_tabs)
2426 .child(
2427 div()
2428 .id("tab_bar_drop_target")
2429 .min_w_6()
2430 // HACK: This empty child is currently necessary to force the drop target to appear
2431 // despite us setting a min width above.
2432 .child("")
2433 .h_full()
2434 .flex_grow()
2435 .drag_over::<DraggedTab>(|bar, _, cx| {
2436 bar.bg(cx.theme().colors().drop_target_background)
2437 })
2438 .drag_over::<DraggedSelection>(|bar, _, cx| {
2439 bar.bg(cx.theme().colors().drop_target_background)
2440 })
2441 .on_drop(cx.listener(move |this, dragged_tab: &DraggedTab, cx| {
2442 this.drag_split_direction = None;
2443 this.handle_tab_drop(dragged_tab, this.items.len(), cx)
2444 }))
2445 .on_drop(cx.listener(move |this, selection: &DraggedSelection, cx| {
2446 this.drag_split_direction = None;
2447 this.handle_project_entry_drop(
2448 &selection.active_selection.entry_id,
2449 Some(tab_count),
2450 cx,
2451 )
2452 }))
2453 .on_drop(cx.listener(move |this, paths, cx| {
2454 this.drag_split_direction = None;
2455 this.handle_external_paths_drop(paths, cx)
2456 }))
2457 .on_click(cx.listener(move |this, event: &ClickEvent, cx| {
2458 if event.up.click_count == 2 {
2459 cx.dispatch_action(
2460 this.double_click_dispatch_action.boxed_clone(),
2461 )
2462 }
2463 })),
2464 ),
2465 )
2466 }
2467
2468 pub fn render_menu_overlay(menu: &View<ContextMenu>) -> Div {
2469 div().absolute().bottom_0().right_0().size_0().child(
2470 deferred(
2471 anchored()
2472 .anchor(AnchorCorner::TopRight)
2473 .child(menu.clone()),
2474 )
2475 .with_priority(1),
2476 )
2477 }
2478
2479 pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
2480 self.zoomed = zoomed;
2481 cx.notify();
2482 }
2483
2484 pub fn is_zoomed(&self) -> bool {
2485 self.zoomed
2486 }
2487
2488 fn handle_drag_move<T: 'static>(
2489 &mut self,
2490 event: &DragMoveEvent<T>,
2491 cx: &mut ViewContext<Self>,
2492 ) {
2493 let can_split_predicate = self.can_split_predicate.take();
2494 let can_split = match &can_split_predicate {
2495 Some(can_split_predicate) => can_split_predicate(self, event.dragged_item(), cx),
2496 None => false,
2497 };
2498 self.can_split_predicate = can_split_predicate;
2499 if !can_split {
2500 return;
2501 }
2502
2503 let rect = event.bounds.size;
2504
2505 let size = event.bounds.size.width.min(event.bounds.size.height)
2506 * WorkspaceSettings::get_global(cx).drop_target_size;
2507
2508 let relative_cursor = Point::new(
2509 event.event.position.x - event.bounds.left(),
2510 event.event.position.y - event.bounds.top(),
2511 );
2512
2513 let direction = if relative_cursor.x < size
2514 || relative_cursor.x > rect.width - size
2515 || relative_cursor.y < size
2516 || relative_cursor.y > rect.height - size
2517 {
2518 [
2519 SplitDirection::Up,
2520 SplitDirection::Right,
2521 SplitDirection::Down,
2522 SplitDirection::Left,
2523 ]
2524 .iter()
2525 .min_by_key(|side| match side {
2526 SplitDirection::Up => relative_cursor.y,
2527 SplitDirection::Right => rect.width - relative_cursor.x,
2528 SplitDirection::Down => rect.height - relative_cursor.y,
2529 SplitDirection::Left => relative_cursor.x,
2530 })
2531 .cloned()
2532 } else {
2533 None
2534 };
2535
2536 if direction != self.drag_split_direction {
2537 self.drag_split_direction = direction;
2538 }
2539 }
2540
2541 fn handle_tab_drop(
2542 &mut self,
2543 dragged_tab: &DraggedTab,
2544 ix: usize,
2545 cx: &mut ViewContext<'_, Self>,
2546 ) {
2547 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2548 if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_tab, cx) {
2549 return;
2550 }
2551 }
2552 let mut to_pane = cx.view().clone();
2553 let split_direction = self.drag_split_direction;
2554 let item_id = dragged_tab.item.item_id();
2555 if let Some(preview_item_id) = self.preview_item_id {
2556 if item_id == preview_item_id {
2557 self.set_preview_item_id(None, cx);
2558 }
2559 }
2560
2561 let from_pane = dragged_tab.pane.clone();
2562 self.workspace
2563 .update(cx, |_, cx| {
2564 cx.defer(move |workspace, cx| {
2565 if let Some(split_direction) = split_direction {
2566 to_pane = workspace.split_pane(to_pane, split_direction, cx);
2567 }
2568 let old_ix = from_pane.read(cx).index_for_item_id(item_id);
2569 let old_len = to_pane.read(cx).items.len();
2570 move_item(&from_pane, &to_pane, item_id, ix, cx);
2571 if to_pane == from_pane {
2572 if let Some(old_index) = old_ix {
2573 to_pane.update(cx, |this, _| {
2574 if old_index < this.pinned_tab_count
2575 && (ix == this.items.len() || ix > this.pinned_tab_count)
2576 {
2577 this.pinned_tab_count -= 1;
2578 } else if this.has_pinned_tabs()
2579 && old_index >= this.pinned_tab_count
2580 && ix < this.pinned_tab_count
2581 {
2582 this.pinned_tab_count += 1;
2583 }
2584 });
2585 }
2586 } else {
2587 to_pane.update(cx, |this, _| {
2588 if this.items.len() > old_len // Did we not deduplicate on drag?
2589 && this.has_pinned_tabs()
2590 && ix < this.pinned_tab_count
2591 {
2592 this.pinned_tab_count += 1;
2593 }
2594 });
2595 from_pane.update(cx, |this, _| {
2596 if let Some(index) = old_ix {
2597 if this.pinned_tab_count > index {
2598 this.pinned_tab_count -= 1;
2599 }
2600 }
2601 })
2602 }
2603 });
2604 })
2605 .log_err();
2606 }
2607
2608 fn handle_dragged_selection_drop(
2609 &mut self,
2610 dragged_selection: &DraggedSelection,
2611 dragged_onto: Option<usize>,
2612 cx: &mut ViewContext<'_, Self>,
2613 ) {
2614 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2615 if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_selection, cx) {
2616 return;
2617 }
2618 }
2619 self.handle_project_entry_drop(
2620 &dragged_selection.active_selection.entry_id,
2621 dragged_onto,
2622 cx,
2623 );
2624 }
2625
2626 fn handle_project_entry_drop(
2627 &mut self,
2628 project_entry_id: &ProjectEntryId,
2629 target: Option<usize>,
2630 cx: &mut ViewContext<'_, Self>,
2631 ) {
2632 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2633 if let ControlFlow::Break(()) = custom_drop_handle(self, project_entry_id, cx) {
2634 return;
2635 }
2636 }
2637 let mut to_pane = cx.view().clone();
2638 let split_direction = self.drag_split_direction;
2639 let project_entry_id = *project_entry_id;
2640 self.workspace
2641 .update(cx, |_, cx| {
2642 cx.defer(move |workspace, cx| {
2643 if let Some(path) = workspace
2644 .project()
2645 .read(cx)
2646 .path_for_entry(project_entry_id, cx)
2647 {
2648 let load_path_task = workspace.load_path(path, cx);
2649 cx.spawn(|workspace, mut cx| async move {
2650 if let Some((project_entry_id, build_item)) =
2651 load_path_task.await.notify_async_err(&mut cx)
2652 {
2653 let (to_pane, new_item_handle) = workspace
2654 .update(&mut cx, |workspace, cx| {
2655 if let Some(split_direction) = split_direction {
2656 to_pane =
2657 workspace.split_pane(to_pane, split_direction, cx);
2658 }
2659 let new_item_handle = to_pane.update(cx, |pane, cx| {
2660 pane.open_item(
2661 project_entry_id,
2662 true,
2663 false,
2664 target,
2665 cx,
2666 build_item,
2667 )
2668 });
2669 (to_pane, new_item_handle)
2670 })
2671 .log_err()?;
2672 to_pane
2673 .update(&mut cx, |this, cx| {
2674 let Some(index) = this.index_for_item(&*new_item_handle)
2675 else {
2676 return;
2677 };
2678
2679 if target.map_or(false, |target| this.is_tab_pinned(target))
2680 {
2681 this.pin_tab_at(index, cx);
2682 }
2683 })
2684 .ok()?
2685 }
2686 Some(())
2687 })
2688 .detach();
2689 };
2690 });
2691 })
2692 .log_err();
2693 }
2694
2695 fn handle_external_paths_drop(
2696 &mut self,
2697 paths: &ExternalPaths,
2698 cx: &mut ViewContext<'_, Self>,
2699 ) {
2700 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2701 if let ControlFlow::Break(()) = custom_drop_handle(self, paths, cx) {
2702 return;
2703 }
2704 }
2705 let mut to_pane = cx.view().clone();
2706 let mut split_direction = self.drag_split_direction;
2707 let paths = paths.paths().to_vec();
2708 let is_remote = self
2709 .workspace
2710 .update(cx, |workspace, cx| {
2711 if workspace.project().read(cx).is_via_collab() {
2712 workspace.show_error(
2713 &anyhow::anyhow!("Cannot drop files on a remote project"),
2714 cx,
2715 );
2716 true
2717 } else {
2718 false
2719 }
2720 })
2721 .unwrap_or(true);
2722 if is_remote {
2723 return;
2724 }
2725
2726 self.workspace
2727 .update(cx, |workspace, cx| {
2728 let fs = Arc::clone(workspace.project().read(cx).fs());
2729 cx.spawn(|workspace, mut cx| async move {
2730 let mut is_file_checks = FuturesUnordered::new();
2731 for path in &paths {
2732 is_file_checks.push(fs.is_file(path))
2733 }
2734 let mut has_files_to_open = false;
2735 while let Some(is_file) = is_file_checks.next().await {
2736 if is_file {
2737 has_files_to_open = true;
2738 break;
2739 }
2740 }
2741 drop(is_file_checks);
2742 if !has_files_to_open {
2743 split_direction = None;
2744 }
2745
2746 if let Ok(open_task) = workspace.update(&mut cx, |workspace, cx| {
2747 if let Some(split_direction) = split_direction {
2748 to_pane = workspace.split_pane(to_pane, split_direction, cx);
2749 }
2750 workspace.open_paths(
2751 paths,
2752 OpenVisible::OnlyDirectories,
2753 Some(to_pane.downgrade()),
2754 cx,
2755 )
2756 }) {
2757 let opened_items: Vec<_> = open_task.await;
2758 _ = workspace.update(&mut cx, |workspace, cx| {
2759 for item in opened_items.into_iter().flatten() {
2760 if let Err(e) = item {
2761 workspace.show_error(&e, cx);
2762 }
2763 }
2764 });
2765 }
2766 })
2767 .detach();
2768 })
2769 .log_err();
2770 }
2771
2772 pub fn display_nav_history_buttons(&mut self, display: Option<bool>) {
2773 self.display_nav_history_buttons = display;
2774 }
2775
2776 fn get_non_closeable_item_ids(&self, close_pinned: bool) -> Vec<EntityId> {
2777 if close_pinned {
2778 return vec![];
2779 }
2780
2781 self.items
2782 .iter()
2783 .map(|item| item.item_id())
2784 .filter(|item_id| {
2785 if let Some(ix) = self.index_for_item_id(*item_id) {
2786 self.is_tab_pinned(ix)
2787 } else {
2788 true
2789 }
2790 })
2791 .collect()
2792 }
2793
2794 pub fn drag_split_direction(&self) -> Option<SplitDirection> {
2795 self.drag_split_direction
2796 }
2797
2798 pub fn set_zoom_out_on_close(&mut self, zoom_out_on_close: bool) {
2799 self.zoom_out_on_close = zoom_out_on_close;
2800 }
2801}
2802
2803impl FocusableView for Pane {
2804 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
2805 self.focus_handle.clone()
2806 }
2807}
2808
2809impl Render for Pane {
2810 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2811 let mut key_context = KeyContext::new_with_defaults();
2812 key_context.add("Pane");
2813 if self.active_item().is_none() {
2814 key_context.add("EmptyPane");
2815 }
2816
2817 let should_display_tab_bar = self.should_display_tab_bar.clone();
2818 let display_tab_bar = should_display_tab_bar(cx);
2819 let is_local = self.project.read(cx).is_local();
2820
2821 v_flex()
2822 .key_context(key_context)
2823 .track_focus(&self.focus_handle(cx))
2824 .size_full()
2825 .flex_none()
2826 .overflow_hidden()
2827 .on_action(cx.listener(|pane, _: &AlternateFile, cx| {
2828 pane.alternate_file(cx);
2829 }))
2830 .on_action(cx.listener(|pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx)))
2831 .on_action(cx.listener(|pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx)))
2832 .on_action(cx.listener(|pane, _: &SplitHorizontal, cx| {
2833 pane.split(SplitDirection::horizontal(cx), cx)
2834 }))
2835 .on_action(cx.listener(|pane, _: &SplitVertical, cx| {
2836 pane.split(SplitDirection::vertical(cx), cx)
2837 }))
2838 .on_action(
2839 cx.listener(|pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx)),
2840 )
2841 .on_action(cx.listener(|pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx)))
2842 .on_action(cx.listener(|pane, _: &GoBack, cx| pane.navigate_backward(cx)))
2843 .on_action(cx.listener(|pane, _: &GoForward, cx| pane.navigate_forward(cx)))
2844 .on_action(cx.listener(|pane, _: &JoinIntoNext, cx| pane.join_into_next(cx)))
2845 .on_action(cx.listener(|pane, _: &JoinAll, cx| pane.join_all(cx)))
2846 .on_action(cx.listener(Pane::toggle_zoom))
2847 .on_action(cx.listener(|pane: &mut Pane, action: &ActivateItem, cx| {
2848 pane.activate_item(action.0, true, true, cx);
2849 }))
2850 .on_action(cx.listener(|pane: &mut Pane, _: &ActivateLastItem, cx| {
2851 pane.activate_item(pane.items.len() - 1, true, true, cx);
2852 }))
2853 .on_action(cx.listener(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
2854 pane.activate_prev_item(true, cx);
2855 }))
2856 .on_action(cx.listener(|pane: &mut Pane, _: &ActivateNextItem, cx| {
2857 pane.activate_next_item(true, cx);
2858 }))
2859 .on_action(cx.listener(|pane, _: &SwapItemLeft, cx| pane.swap_item_left(cx)))
2860 .on_action(cx.listener(|pane, _: &SwapItemRight, cx| pane.swap_item_right(cx)))
2861 .on_action(cx.listener(|pane, action, cx| {
2862 pane.toggle_pin_tab(action, cx);
2863 }))
2864 .when(PreviewTabsSettings::get_global(cx).enabled, |this| {
2865 this.on_action(cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, cx| {
2866 if let Some(active_item_id) = pane.active_item().map(|i| i.item_id()) {
2867 if pane.is_active_preview_item(active_item_id) {
2868 pane.set_preview_item_id(None, cx);
2869 } else {
2870 pane.set_preview_item_id(Some(active_item_id), cx);
2871 }
2872 }
2873 }))
2874 })
2875 .on_action(
2876 cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
2877 if let Some(task) = pane.close_active_item(action, cx) {
2878 task.detach_and_log_err(cx)
2879 }
2880 }),
2881 )
2882 .on_action(
2883 cx.listener(|pane: &mut Self, action: &CloseInactiveItems, cx| {
2884 if let Some(task) = pane.close_inactive_items(action, cx) {
2885 task.detach_and_log_err(cx)
2886 }
2887 }),
2888 )
2889 .on_action(
2890 cx.listener(|pane: &mut Self, action: &CloseCleanItems, cx| {
2891 if let Some(task) = pane.close_clean_items(action, cx) {
2892 task.detach_and_log_err(cx)
2893 }
2894 }),
2895 )
2896 .on_action(
2897 cx.listener(|pane: &mut Self, action: &CloseItemsToTheLeft, cx| {
2898 if let Some(task) = pane.close_items_to_the_left(action, cx) {
2899 task.detach_and_log_err(cx)
2900 }
2901 }),
2902 )
2903 .on_action(
2904 cx.listener(|pane: &mut Self, action: &CloseItemsToTheRight, cx| {
2905 if let Some(task) = pane.close_items_to_the_right(action, cx) {
2906 task.detach_and_log_err(cx)
2907 }
2908 }),
2909 )
2910 .on_action(cx.listener(|pane: &mut Self, action: &CloseAllItems, cx| {
2911 if let Some(task) = pane.close_all_items(action, cx) {
2912 task.detach_and_log_err(cx)
2913 }
2914 }))
2915 .on_action(
2916 cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
2917 if let Some(task) = pane.close_active_item(action, cx) {
2918 task.detach_and_log_err(cx)
2919 }
2920 }),
2921 )
2922 .on_action(
2923 cx.listener(|pane: &mut Self, action: &RevealInProjectPanel, cx| {
2924 let entry_id = action
2925 .entry_id
2926 .map(ProjectEntryId::from_proto)
2927 .or_else(|| pane.active_item()?.project_entry_ids(cx).first().copied());
2928 if let Some(entry_id) = entry_id {
2929 pane.project.update(cx, |_, cx| {
2930 cx.emit(project::Event::RevealInProjectPanel(entry_id))
2931 });
2932 }
2933 }),
2934 )
2935 .when(self.active_item().is_some() && display_tab_bar, |pane| {
2936 pane.child(self.render_tab_bar(cx))
2937 })
2938 .child({
2939 let has_worktrees = self.project.read(cx).worktrees(cx).next().is_some();
2940 // main content
2941 div()
2942 .flex_1()
2943 .relative()
2944 .group("")
2945 .overflow_hidden()
2946 .on_drag_move::<DraggedTab>(cx.listener(Self::handle_drag_move))
2947 .on_drag_move::<DraggedSelection>(cx.listener(Self::handle_drag_move))
2948 .when(is_local, |div| {
2949 div.on_drag_move::<ExternalPaths>(cx.listener(Self::handle_drag_move))
2950 })
2951 .map(|div| {
2952 if let Some(item) = self.active_item() {
2953 div.v_flex()
2954 .size_full()
2955 .overflow_hidden()
2956 .child(self.toolbar.clone())
2957 .child(item.to_any())
2958 } else {
2959 let placeholder = div.h_flex().size_full().justify_center();
2960 if has_worktrees {
2961 placeholder
2962 } else {
2963 placeholder.child(
2964 Label::new("Open a file or project to get started.")
2965 .color(Color::Muted),
2966 )
2967 }
2968 }
2969 })
2970 .child(
2971 // drag target
2972 div()
2973 .invisible()
2974 .absolute()
2975 .bg(cx.theme().colors().drop_target_background)
2976 .group_drag_over::<DraggedTab>("", |style| style.visible())
2977 .group_drag_over::<DraggedSelection>("", |style| style.visible())
2978 .when(is_local, |div| {
2979 div.group_drag_over::<ExternalPaths>("", |style| style.visible())
2980 })
2981 .when_some(self.can_drop_predicate.clone(), |this, p| {
2982 this.can_drop(move |a, cx| p(a, cx))
2983 })
2984 .on_drop(cx.listener(move |this, dragged_tab, cx| {
2985 this.handle_tab_drop(dragged_tab, this.active_item_index(), cx)
2986 }))
2987 .on_drop(cx.listener(move |this, selection: &DraggedSelection, cx| {
2988 this.handle_dragged_selection_drop(selection, None, cx)
2989 }))
2990 .on_drop(cx.listener(move |this, paths, cx| {
2991 this.handle_external_paths_drop(paths, cx)
2992 }))
2993 .map(|div| {
2994 let size = DefiniteLength::Fraction(0.5);
2995 match self.drag_split_direction {
2996 None => div.top_0().right_0().bottom_0().left_0(),
2997 Some(SplitDirection::Up) => {
2998 div.top_0().left_0().right_0().h(size)
2999 }
3000 Some(SplitDirection::Down) => {
3001 div.left_0().bottom_0().right_0().h(size)
3002 }
3003 Some(SplitDirection::Left) => {
3004 div.top_0().left_0().bottom_0().w(size)
3005 }
3006 Some(SplitDirection::Right) => {
3007 div.top_0().bottom_0().right_0().w(size)
3008 }
3009 }
3010 }),
3011 )
3012 })
3013 .on_mouse_down(
3014 MouseButton::Navigate(NavigationDirection::Back),
3015 cx.listener(|pane, _, cx| {
3016 if let Some(workspace) = pane.workspace.upgrade() {
3017 let pane = cx.view().downgrade();
3018 cx.window_context().defer(move |cx| {
3019 workspace.update(cx, |workspace, cx| {
3020 workspace.go_back(pane, cx).detach_and_log_err(cx)
3021 })
3022 })
3023 }
3024 }),
3025 )
3026 .on_mouse_down(
3027 MouseButton::Navigate(NavigationDirection::Forward),
3028 cx.listener(|pane, _, cx| {
3029 if let Some(workspace) = pane.workspace.upgrade() {
3030 let pane = cx.view().downgrade();
3031 cx.window_context().defer(move |cx| {
3032 workspace.update(cx, |workspace, cx| {
3033 workspace.go_forward(pane, cx).detach_and_log_err(cx)
3034 })
3035 })
3036 }
3037 }),
3038 )
3039 }
3040}
3041
3042impl ItemNavHistory {
3043 pub fn push<D: 'static + Send + Any>(&mut self, data: Option<D>, cx: &mut WindowContext) {
3044 self.history
3045 .push(data, self.item.clone(), self.is_preview, cx);
3046 }
3047
3048 pub fn pop_backward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
3049 self.history.pop(NavigationMode::GoingBack, cx)
3050 }
3051
3052 pub fn pop_forward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
3053 self.history.pop(NavigationMode::GoingForward, cx)
3054 }
3055}
3056
3057impl NavHistory {
3058 pub fn for_each_entry(
3059 &self,
3060 cx: &AppContext,
3061 mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
3062 ) {
3063 let borrowed_history = self.0.lock();
3064 borrowed_history
3065 .forward_stack
3066 .iter()
3067 .chain(borrowed_history.backward_stack.iter())
3068 .chain(borrowed_history.closed_stack.iter())
3069 .for_each(|entry| {
3070 if let Some(project_and_abs_path) =
3071 borrowed_history.paths_by_item.get(&entry.item.id())
3072 {
3073 f(entry, project_and_abs_path.clone());
3074 } else if let Some(item) = entry.item.upgrade() {
3075 if let Some(path) = item.project_path(cx) {
3076 f(entry, (path, None));
3077 }
3078 }
3079 })
3080 }
3081
3082 pub fn set_mode(&mut self, mode: NavigationMode) {
3083 self.0.lock().mode = mode;
3084 }
3085
3086 pub fn mode(&self) -> NavigationMode {
3087 self.0.lock().mode
3088 }
3089
3090 pub fn disable(&mut self) {
3091 self.0.lock().mode = NavigationMode::Disabled;
3092 }
3093
3094 pub fn enable(&mut self) {
3095 self.0.lock().mode = NavigationMode::Normal;
3096 }
3097
3098 pub fn pop(&mut self, mode: NavigationMode, cx: &mut WindowContext) -> Option<NavigationEntry> {
3099 let mut state = self.0.lock();
3100 let entry = match mode {
3101 NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
3102 return None
3103 }
3104 NavigationMode::GoingBack => &mut state.backward_stack,
3105 NavigationMode::GoingForward => &mut state.forward_stack,
3106 NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
3107 }
3108 .pop_back();
3109 if entry.is_some() {
3110 state.did_update(cx);
3111 }
3112 entry
3113 }
3114
3115 pub fn push<D: 'static + Send + Any>(
3116 &mut self,
3117 data: Option<D>,
3118 item: Arc<dyn WeakItemHandle>,
3119 is_preview: bool,
3120 cx: &mut WindowContext,
3121 ) {
3122 let state = &mut *self.0.lock();
3123 match state.mode {
3124 NavigationMode::Disabled => {}
3125 NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
3126 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3127 state.backward_stack.pop_front();
3128 }
3129 state.backward_stack.push_back(NavigationEntry {
3130 item,
3131 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3132 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3133 is_preview,
3134 });
3135 state.forward_stack.clear();
3136 }
3137 NavigationMode::GoingBack => {
3138 if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3139 state.forward_stack.pop_front();
3140 }
3141 state.forward_stack.push_back(NavigationEntry {
3142 item,
3143 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3144 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3145 is_preview,
3146 });
3147 }
3148 NavigationMode::GoingForward => {
3149 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3150 state.backward_stack.pop_front();
3151 }
3152 state.backward_stack.push_back(NavigationEntry {
3153 item,
3154 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3155 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3156 is_preview,
3157 });
3158 }
3159 NavigationMode::ClosingItem => {
3160 if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3161 state.closed_stack.pop_front();
3162 }
3163 state.closed_stack.push_back(NavigationEntry {
3164 item,
3165 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3166 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3167 is_preview,
3168 });
3169 }
3170 }
3171 state.did_update(cx);
3172 }
3173
3174 pub fn remove_item(&mut self, item_id: EntityId) {
3175 let mut state = self.0.lock();
3176 state.paths_by_item.remove(&item_id);
3177 state
3178 .backward_stack
3179 .retain(|entry| entry.item.id() != item_id);
3180 state
3181 .forward_stack
3182 .retain(|entry| entry.item.id() != item_id);
3183 state
3184 .closed_stack
3185 .retain(|entry| entry.item.id() != item_id);
3186 }
3187
3188 pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option<PathBuf>)> {
3189 self.0.lock().paths_by_item.get(&item_id).cloned()
3190 }
3191}
3192
3193impl NavHistoryState {
3194 pub fn did_update(&self, cx: &mut WindowContext) {
3195 if let Some(pane) = self.pane.upgrade() {
3196 cx.defer(move |cx| {
3197 pane.update(cx, |pane, cx| pane.history_updated(cx));
3198 });
3199 }
3200 }
3201}
3202
3203fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
3204 let path = buffer_path
3205 .as_ref()
3206 .and_then(|p| {
3207 p.path
3208 .to_str()
3209 .and_then(|s| if s.is_empty() { None } else { Some(s) })
3210 })
3211 .unwrap_or("This buffer");
3212 let path = truncate_and_remove_front(path, 80);
3213 format!("{path} contains unsaved edits. Do you want to save it?")
3214}
3215
3216pub fn tab_details(items: &[Box<dyn ItemHandle>], cx: &AppContext) -> Vec<usize> {
3217 let mut tab_details = items.iter().map(|_| 0).collect::<Vec<_>>();
3218 let mut tab_descriptions = HashMap::default();
3219 let mut done = false;
3220 while !done {
3221 done = true;
3222
3223 // Store item indices by their tab description.
3224 for (ix, (item, detail)) in items.iter().zip(&tab_details).enumerate() {
3225 if let Some(description) = item.tab_description(*detail, cx) {
3226 if *detail == 0
3227 || Some(&description) != item.tab_description(detail - 1, cx).as_ref()
3228 {
3229 tab_descriptions
3230 .entry(description)
3231 .or_insert(Vec::new())
3232 .push(ix);
3233 }
3234 }
3235 }
3236
3237 // If two or more items have the same tab description, increase their level
3238 // of detail and try again.
3239 for (_, item_ixs) in tab_descriptions.drain() {
3240 if item_ixs.len() > 1 {
3241 done = false;
3242 for ix in item_ixs {
3243 tab_details[ix] += 1;
3244 }
3245 }
3246 }
3247 }
3248
3249 tab_details
3250}
3251
3252pub fn render_item_indicator(item: Box<dyn ItemHandle>, cx: &WindowContext) -> Option<Indicator> {
3253 maybe!({
3254 let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
3255 (true, _) => Color::Warning,
3256 (_, true) => Color::Accent,
3257 (false, false) => return None,
3258 };
3259
3260 Some(Indicator::dot().color(indicator_color))
3261 })
3262}
3263
3264impl Render for DraggedTab {
3265 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3266 let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
3267 let label = self.item.tab_content(
3268 TabContentParams {
3269 detail: Some(self.detail),
3270 selected: false,
3271 preview: false,
3272 },
3273 cx,
3274 );
3275 Tab::new("")
3276 .selected(self.is_active)
3277 .child(label)
3278 .render(cx)
3279 .font(ui_font)
3280 }
3281}
3282
3283#[cfg(test)]
3284mod tests {
3285 use super::*;
3286 use crate::item::test::{TestItem, TestProjectItem};
3287 use gpui::{TestAppContext, VisualTestContext};
3288 use project::FakeFs;
3289 use settings::SettingsStore;
3290 use theme::LoadThemes;
3291
3292 #[gpui::test]
3293 async fn test_remove_active_empty(cx: &mut TestAppContext) {
3294 init_test(cx);
3295 let fs = FakeFs::new(cx.executor());
3296
3297 let project = Project::test(fs, None, cx).await;
3298 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3299 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3300
3301 pane.update(cx, |pane, cx| {
3302 assert!(pane
3303 .close_active_item(&CloseActiveItem { save_intent: None }, cx)
3304 .is_none())
3305 });
3306 }
3307
3308 #[gpui::test]
3309 async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
3310 init_test(cx);
3311 let fs = FakeFs::new(cx.executor());
3312
3313 let project = Project::test(fs, None, cx).await;
3314 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3315 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3316
3317 // 1. Add with a destination index
3318 // a. Add before the active item
3319 set_labeled_items(&pane, ["A", "B*", "C"], cx);
3320 pane.update(cx, |pane, cx| {
3321 pane.add_item(
3322 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3323 false,
3324 false,
3325 Some(0),
3326 cx,
3327 );
3328 });
3329 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
3330
3331 // b. Add after the active item
3332 set_labeled_items(&pane, ["A", "B*", "C"], cx);
3333 pane.update(cx, |pane, cx| {
3334 pane.add_item(
3335 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3336 false,
3337 false,
3338 Some(2),
3339 cx,
3340 );
3341 });
3342 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3343
3344 // c. Add at the end of the item list (including off the length)
3345 set_labeled_items(&pane, ["A", "B*", "C"], cx);
3346 pane.update(cx, |pane, cx| {
3347 pane.add_item(
3348 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3349 false,
3350 false,
3351 Some(5),
3352 cx,
3353 );
3354 });
3355 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3356
3357 // 2. Add without a destination index
3358 // a. Add with active item at the start of the item list
3359 set_labeled_items(&pane, ["A*", "B", "C"], cx);
3360 pane.update(cx, |pane, cx| {
3361 pane.add_item(
3362 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3363 false,
3364 false,
3365 None,
3366 cx,
3367 );
3368 });
3369 set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
3370
3371 // b. Add with active item at the end of the item list
3372 set_labeled_items(&pane, ["A", "B", "C*"], cx);
3373 pane.update(cx, |pane, cx| {
3374 pane.add_item(
3375 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3376 false,
3377 false,
3378 None,
3379 cx,
3380 );
3381 });
3382 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3383 }
3384
3385 #[gpui::test]
3386 async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
3387 init_test(cx);
3388 let fs = FakeFs::new(cx.executor());
3389
3390 let project = Project::test(fs, None, cx).await;
3391 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3392 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3393
3394 // 1. Add with a destination index
3395 // 1a. Add before the active item
3396 let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3397 pane.update(cx, |pane, cx| {
3398 pane.add_item(d, false, false, Some(0), cx);
3399 });
3400 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
3401
3402 // 1b. Add after the active item
3403 let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3404 pane.update(cx, |pane, cx| {
3405 pane.add_item(d, false, false, Some(2), cx);
3406 });
3407 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3408
3409 // 1c. Add at the end of the item list (including off the length)
3410 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3411 pane.update(cx, |pane, cx| {
3412 pane.add_item(a, false, false, Some(5), cx);
3413 });
3414 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3415
3416 // 1d. Add same item to active index
3417 let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3418 pane.update(cx, |pane, cx| {
3419 pane.add_item(b, false, false, Some(1), cx);
3420 });
3421 assert_item_labels(&pane, ["A", "B*", "C"], cx);
3422
3423 // 1e. Add item to index after same item in last position
3424 let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3425 pane.update(cx, |pane, cx| {
3426 pane.add_item(c, false, false, Some(2), cx);
3427 });
3428 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3429
3430 // 2. Add without a destination index
3431 // 2a. Add with active item at the start of the item list
3432 let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
3433 pane.update(cx, |pane, cx| {
3434 pane.add_item(d, false, false, None, cx);
3435 });
3436 assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
3437
3438 // 2b. Add with active item at the end of the item list
3439 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
3440 pane.update(cx, |pane, cx| {
3441 pane.add_item(a, false, false, None, cx);
3442 });
3443 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3444
3445 // 2c. Add active item to active item at end of list
3446 let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
3447 pane.update(cx, |pane, cx| {
3448 pane.add_item(c, false, false, None, cx);
3449 });
3450 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3451
3452 // 2d. Add active item to active item at start of list
3453 let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
3454 pane.update(cx, |pane, cx| {
3455 pane.add_item(a, false, false, None, cx);
3456 });
3457 assert_item_labels(&pane, ["A*", "B", "C"], cx);
3458 }
3459
3460 #[gpui::test]
3461 async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
3462 init_test(cx);
3463 let fs = FakeFs::new(cx.executor());
3464
3465 let project = Project::test(fs, None, cx).await;
3466 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3467 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3468
3469 // singleton view
3470 pane.update(cx, |pane, cx| {
3471 pane.add_item(
3472 Box::new(cx.new_view(|cx| {
3473 TestItem::new(cx)
3474 .with_singleton(true)
3475 .with_label("buffer 1")
3476 .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
3477 })),
3478 false,
3479 false,
3480 None,
3481 cx,
3482 );
3483 });
3484 assert_item_labels(&pane, ["buffer 1*"], cx);
3485
3486 // new singleton view with the same project entry
3487 pane.update(cx, |pane, cx| {
3488 pane.add_item(
3489 Box::new(cx.new_view(|cx| {
3490 TestItem::new(cx)
3491 .with_singleton(true)
3492 .with_label("buffer 1")
3493 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3494 })),
3495 false,
3496 false,
3497 None,
3498 cx,
3499 );
3500 });
3501 assert_item_labels(&pane, ["buffer 1*"], cx);
3502
3503 // new singleton view with different project entry
3504 pane.update(cx, |pane, cx| {
3505 pane.add_item(
3506 Box::new(cx.new_view(|cx| {
3507 TestItem::new(cx)
3508 .with_singleton(true)
3509 .with_label("buffer 2")
3510 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
3511 })),
3512 false,
3513 false,
3514 None,
3515 cx,
3516 );
3517 });
3518 assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
3519
3520 // new multibuffer view with the same project entry
3521 pane.update(cx, |pane, cx| {
3522 pane.add_item(
3523 Box::new(cx.new_view(|cx| {
3524 TestItem::new(cx)
3525 .with_singleton(false)
3526 .with_label("multibuffer 1")
3527 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3528 })),
3529 false,
3530 false,
3531 None,
3532 cx,
3533 );
3534 });
3535 assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
3536
3537 // another multibuffer view with the same project entry
3538 pane.update(cx, |pane, cx| {
3539 pane.add_item(
3540 Box::new(cx.new_view(|cx| {
3541 TestItem::new(cx)
3542 .with_singleton(false)
3543 .with_label("multibuffer 1b")
3544 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3545 })),
3546 false,
3547 false,
3548 None,
3549 cx,
3550 );
3551 });
3552 assert_item_labels(
3553 &pane,
3554 ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
3555 cx,
3556 );
3557 }
3558
3559 #[gpui::test]
3560 async fn test_remove_item_ordering_history(cx: &mut TestAppContext) {
3561 init_test(cx);
3562 let fs = FakeFs::new(cx.executor());
3563
3564 let project = Project::test(fs, None, cx).await;
3565 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3566 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3567
3568 add_labeled_item(&pane, "A", false, cx);
3569 add_labeled_item(&pane, "B", false, cx);
3570 add_labeled_item(&pane, "C", false, cx);
3571 add_labeled_item(&pane, "D", false, cx);
3572 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3573
3574 pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
3575 add_labeled_item(&pane, "1", false, cx);
3576 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
3577
3578 pane.update(cx, |pane, cx| {
3579 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3580 })
3581 .unwrap()
3582 .await
3583 .unwrap();
3584 assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
3585
3586 pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
3587 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3588
3589 pane.update(cx, |pane, cx| {
3590 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3591 })
3592 .unwrap()
3593 .await
3594 .unwrap();
3595 assert_item_labels(&pane, ["A", "B*", "C"], cx);
3596
3597 pane.update(cx, |pane, cx| {
3598 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3599 })
3600 .unwrap()
3601 .await
3602 .unwrap();
3603 assert_item_labels(&pane, ["A", "C*"], cx);
3604
3605 pane.update(cx, |pane, cx| {
3606 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3607 })
3608 .unwrap()
3609 .await
3610 .unwrap();
3611 assert_item_labels(&pane, ["A*"], cx);
3612 }
3613
3614 #[gpui::test]
3615 async fn test_remove_item_ordering_neighbour(cx: &mut TestAppContext) {
3616 init_test(cx);
3617 cx.update_global::<SettingsStore, ()>(|s, cx| {
3618 s.update_user_settings::<ItemSettings>(cx, |s| {
3619 s.activate_on_close = Some(ActivateOnClose::Neighbour);
3620 });
3621 });
3622 let fs = FakeFs::new(cx.executor());
3623
3624 let project = Project::test(fs, None, cx).await;
3625 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3626 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3627
3628 add_labeled_item(&pane, "A", false, cx);
3629 add_labeled_item(&pane, "B", false, cx);
3630 add_labeled_item(&pane, "C", false, cx);
3631 add_labeled_item(&pane, "D", false, cx);
3632 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3633
3634 pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
3635 add_labeled_item(&pane, "1", false, cx);
3636 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
3637
3638 pane.update(cx, |pane, cx| {
3639 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3640 })
3641 .unwrap()
3642 .await
3643 .unwrap();
3644 assert_item_labels(&pane, ["A", "B", "C*", "D"], cx);
3645
3646 pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
3647 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3648
3649 pane.update(cx, |pane, cx| {
3650 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3651 })
3652 .unwrap()
3653 .await
3654 .unwrap();
3655 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3656
3657 pane.update(cx, |pane, cx| {
3658 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3659 })
3660 .unwrap()
3661 .await
3662 .unwrap();
3663 assert_item_labels(&pane, ["A", "B*"], cx);
3664
3665 pane.update(cx, |pane, cx| {
3666 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3667 })
3668 .unwrap()
3669 .await
3670 .unwrap();
3671 assert_item_labels(&pane, ["A*"], cx);
3672 }
3673
3674 #[gpui::test]
3675 async fn test_remove_item_ordering_left_neighbour(cx: &mut TestAppContext) {
3676 init_test(cx);
3677 cx.update_global::<SettingsStore, ()>(|s, cx| {
3678 s.update_user_settings::<ItemSettings>(cx, |s| {
3679 s.activate_on_close = Some(ActivateOnClose::LeftNeighbour);
3680 });
3681 });
3682 let fs = FakeFs::new(cx.executor());
3683
3684 let project = Project::test(fs, None, cx).await;
3685 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3686 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3687
3688 add_labeled_item(&pane, "A", false, cx);
3689 add_labeled_item(&pane, "B", false, cx);
3690 add_labeled_item(&pane, "C", false, cx);
3691 add_labeled_item(&pane, "D", false, cx);
3692 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3693
3694 pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
3695 add_labeled_item(&pane, "1", false, cx);
3696 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
3697
3698 pane.update(cx, |pane, cx| {
3699 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3700 })
3701 .unwrap()
3702 .await
3703 .unwrap();
3704 assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
3705
3706 pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
3707 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3708
3709 pane.update(cx, |pane, cx| {
3710 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3711 })
3712 .unwrap()
3713 .await
3714 .unwrap();
3715 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3716
3717 pane.update(cx, |pane, cx| pane.activate_item(0, false, false, cx));
3718 assert_item_labels(&pane, ["A*", "B", "C"], cx);
3719
3720 pane.update(cx, |pane, cx| {
3721 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3722 })
3723 .unwrap()
3724 .await
3725 .unwrap();
3726 assert_item_labels(&pane, ["B*", "C"], cx);
3727
3728 pane.update(cx, |pane, cx| {
3729 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3730 })
3731 .unwrap()
3732 .await
3733 .unwrap();
3734 assert_item_labels(&pane, ["C*"], cx);
3735 }
3736
3737 #[gpui::test]
3738 async fn test_close_inactive_items(cx: &mut TestAppContext) {
3739 init_test(cx);
3740 let fs = FakeFs::new(cx.executor());
3741
3742 let project = Project::test(fs, None, cx).await;
3743 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3744 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3745
3746 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
3747
3748 pane.update(cx, |pane, cx| {
3749 pane.close_inactive_items(
3750 &CloseInactiveItems {
3751 save_intent: None,
3752 close_pinned: false,
3753 },
3754 cx,
3755 )
3756 })
3757 .unwrap()
3758 .await
3759 .unwrap();
3760 assert_item_labels(&pane, ["C*"], cx);
3761 }
3762
3763 #[gpui::test]
3764 async fn test_close_clean_items(cx: &mut TestAppContext) {
3765 init_test(cx);
3766 let fs = FakeFs::new(cx.executor());
3767
3768 let project = Project::test(fs, None, cx).await;
3769 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3770 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3771
3772 add_labeled_item(&pane, "A", true, cx);
3773 add_labeled_item(&pane, "B", false, cx);
3774 add_labeled_item(&pane, "C", true, cx);
3775 add_labeled_item(&pane, "D", false, cx);
3776 add_labeled_item(&pane, "E", false, cx);
3777 assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
3778
3779 pane.update(cx, |pane, cx| {
3780 pane.close_clean_items(
3781 &CloseCleanItems {
3782 close_pinned: false,
3783 },
3784 cx,
3785 )
3786 })
3787 .unwrap()
3788 .await
3789 .unwrap();
3790 assert_item_labels(&pane, ["A^", "C*^"], cx);
3791 }
3792
3793 #[gpui::test]
3794 async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
3795 init_test(cx);
3796 let fs = FakeFs::new(cx.executor());
3797
3798 let project = Project::test(fs, None, cx).await;
3799 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3800 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3801
3802 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
3803
3804 pane.update(cx, |pane, cx| {
3805 pane.close_items_to_the_left(
3806 &CloseItemsToTheLeft {
3807 close_pinned: false,
3808 },
3809 cx,
3810 )
3811 })
3812 .unwrap()
3813 .await
3814 .unwrap();
3815 assert_item_labels(&pane, ["C*", "D", "E"], cx);
3816 }
3817
3818 #[gpui::test]
3819 async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
3820 init_test(cx);
3821 let fs = FakeFs::new(cx.executor());
3822
3823 let project = Project::test(fs, None, cx).await;
3824 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3825 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3826
3827 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
3828
3829 pane.update(cx, |pane, cx| {
3830 pane.close_items_to_the_right(
3831 &CloseItemsToTheRight {
3832 close_pinned: false,
3833 },
3834 cx,
3835 )
3836 })
3837 .unwrap()
3838 .await
3839 .unwrap();
3840 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3841 }
3842
3843 #[gpui::test]
3844 async fn test_close_all_items(cx: &mut TestAppContext) {
3845 init_test(cx);
3846 let fs = FakeFs::new(cx.executor());
3847
3848 let project = Project::test(fs, None, cx).await;
3849 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3850 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3851
3852 let item_a = add_labeled_item(&pane, "A", false, cx);
3853 add_labeled_item(&pane, "B", false, cx);
3854 add_labeled_item(&pane, "C", false, cx);
3855 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3856
3857 pane.update(cx, |pane, cx| {
3858 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
3859 pane.pin_tab_at(ix, cx);
3860 pane.close_all_items(
3861 &CloseAllItems {
3862 save_intent: None,
3863 close_pinned: false,
3864 },
3865 cx,
3866 )
3867 })
3868 .unwrap()
3869 .await
3870 .unwrap();
3871 assert_item_labels(&pane, ["A*"], cx);
3872
3873 pane.update(cx, |pane, cx| {
3874 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
3875 pane.unpin_tab_at(ix, cx);
3876 pane.close_all_items(
3877 &CloseAllItems {
3878 save_intent: None,
3879 close_pinned: false,
3880 },
3881 cx,
3882 )
3883 })
3884 .unwrap()
3885 .await
3886 .unwrap();
3887
3888 assert_item_labels(&pane, [], cx);
3889
3890 add_labeled_item(&pane, "A", true, cx).update(cx, |item, cx| {
3891 item.project_items
3892 .push(TestProjectItem::new(1, "A.txt", cx))
3893 });
3894 add_labeled_item(&pane, "B", true, cx).update(cx, |item, cx| {
3895 item.project_items
3896 .push(TestProjectItem::new(2, "B.txt", cx))
3897 });
3898 add_labeled_item(&pane, "C", true, cx).update(cx, |item, cx| {
3899 item.project_items
3900 .push(TestProjectItem::new(3, "C.txt", cx))
3901 });
3902 assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
3903
3904 let save = pane
3905 .update(cx, |pane, cx| {
3906 pane.close_all_items(
3907 &CloseAllItems {
3908 save_intent: None,
3909 close_pinned: false,
3910 },
3911 cx,
3912 )
3913 })
3914 .unwrap();
3915
3916 cx.executor().run_until_parked();
3917 cx.simulate_prompt_answer(2);
3918 save.await.unwrap();
3919 assert_item_labels(&pane, [], cx);
3920
3921 add_labeled_item(&pane, "A", true, cx);
3922 add_labeled_item(&pane, "B", true, cx);
3923 add_labeled_item(&pane, "C", true, cx);
3924 assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
3925 let save = pane
3926 .update(cx, |pane, cx| {
3927 pane.close_all_items(
3928 &CloseAllItems {
3929 save_intent: None,
3930 close_pinned: false,
3931 },
3932 cx,
3933 )
3934 })
3935 .unwrap();
3936
3937 cx.executor().run_until_parked();
3938 cx.simulate_prompt_answer(2);
3939 save.await.unwrap();
3940 assert_item_labels(&pane, [], cx);
3941 }
3942
3943 #[gpui::test]
3944 async fn test_close_all_items_including_pinned(cx: &mut TestAppContext) {
3945 init_test(cx);
3946 let fs = FakeFs::new(cx.executor());
3947
3948 let project = Project::test(fs, None, cx).await;
3949 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3950 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3951
3952 let item_a = add_labeled_item(&pane, "A", false, cx);
3953 add_labeled_item(&pane, "B", false, cx);
3954 add_labeled_item(&pane, "C", false, cx);
3955 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3956
3957 pane.update(cx, |pane, cx| {
3958 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
3959 pane.pin_tab_at(ix, cx);
3960 pane.close_all_items(
3961 &CloseAllItems {
3962 save_intent: None,
3963 close_pinned: true,
3964 },
3965 cx,
3966 )
3967 })
3968 .unwrap()
3969 .await
3970 .unwrap();
3971 assert_item_labels(&pane, [], cx);
3972 }
3973
3974 fn init_test(cx: &mut TestAppContext) {
3975 cx.update(|cx| {
3976 let settings_store = SettingsStore::test(cx);
3977 cx.set_global(settings_store);
3978 theme::init(LoadThemes::JustBase, cx);
3979 crate::init_settings(cx);
3980 Project::init_settings(cx);
3981 });
3982 }
3983
3984 fn add_labeled_item(
3985 pane: &View<Pane>,
3986 label: &str,
3987 is_dirty: bool,
3988 cx: &mut VisualTestContext,
3989 ) -> Box<View<TestItem>> {
3990 pane.update(cx, |pane, cx| {
3991 let labeled_item = Box::new(
3992 cx.new_view(|cx| TestItem::new(cx).with_label(label).with_dirty(is_dirty)),
3993 );
3994 pane.add_item(labeled_item.clone(), false, false, None, cx);
3995 labeled_item
3996 })
3997 }
3998
3999 fn set_labeled_items<const COUNT: usize>(
4000 pane: &View<Pane>,
4001 labels: [&str; COUNT],
4002 cx: &mut VisualTestContext,
4003 ) -> [Box<View<TestItem>>; COUNT] {
4004 pane.update(cx, |pane, cx| {
4005 pane.items.clear();
4006 let mut active_item_index = 0;
4007
4008 let mut index = 0;
4009 let items = labels.map(|mut label| {
4010 if label.ends_with('*') {
4011 label = label.trim_end_matches('*');
4012 active_item_index = index;
4013 }
4014
4015 let labeled_item = Box::new(cx.new_view(|cx| TestItem::new(cx).with_label(label)));
4016 pane.add_item(labeled_item.clone(), false, false, None, cx);
4017 index += 1;
4018 labeled_item
4019 });
4020
4021 pane.activate_item(active_item_index, false, false, cx);
4022
4023 items
4024 })
4025 }
4026
4027 // Assert the item label, with the active item label suffixed with a '*'
4028 #[track_caller]
4029 fn assert_item_labels<const COUNT: usize>(
4030 pane: &View<Pane>,
4031 expected_states: [&str; COUNT],
4032 cx: &mut VisualTestContext,
4033 ) {
4034 let actual_states = pane.update(cx, |pane, cx| {
4035 pane.items
4036 .iter()
4037 .enumerate()
4038 .map(|(ix, item)| {
4039 let mut state = item
4040 .to_any()
4041 .downcast::<TestItem>()
4042 .unwrap()
4043 .read(cx)
4044 .label
4045 .clone();
4046 if ix == pane.active_item_index {
4047 state.push('*');
4048 }
4049 if item.is_dirty(cx) {
4050 state.push('^');
4051 }
4052 state
4053 })
4054 .collect::<Vec<_>>()
4055 });
4056 assert_eq!(
4057 actual_states, expected_states,
4058 "pane items do not match expectation"
4059 );
4060 }
4061}