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