1use crate::{
2 item::{
3 ClosePosition, Item, ItemHandle, ItemSettings, PreviewTabsSettings, TabContentParams,
4 WeakItemHandle,
5 },
6 toolbar::Toolbar,
7 workspace_settings::{AutosaveSetting, TabBarSettings, WorkspaceSettings},
8 CloseWindow, NewCenterTerminal, NewFile, NewSearch, OpenInTerminal, OpenTerminal, OpenVisible,
9 SplitDirection, ToggleZoom, Workspace,
10};
11use anyhow::Result;
12use collections::{BTreeSet, HashMap, HashSet, VecDeque};
13use futures::{stream::FuturesUnordered, StreamExt};
14use gpui::{
15 actions, anchored, deferred, impl_actions, prelude::*, Action, AnchorCorner, AnyElement,
16 AppContext, AsyncWindowContext, ClickEvent, DismissEvent, Div, DragMoveEvent, EntityId,
17 EventEmitter, ExternalPaths, FocusHandle, FocusOutEvent, FocusableView, KeyContext, Model,
18 MouseButton, MouseDownEvent, NavigationDirection, Pixels, Point, PromptLevel, Render,
19 ScrollHandle, Subscription, Task, View, ViewContext, VisualContext, WeakFocusHandle, WeakView,
20 WindowContext,
21};
22use itertools::Itertools;
23use parking_lot::Mutex;
24use project::{Project, ProjectEntryId, ProjectPath, WorktreeId};
25use serde::Deserialize;
26use settings::{Settings, SettingsStore};
27use std::{
28 any::Any,
29 cmp, fmt, mem,
30 ops::ControlFlow,
31 path::PathBuf,
32 rc::Rc,
33 sync::{
34 atomic::{AtomicUsize, Ordering},
35 Arc,
36 },
37};
38use theme::ThemeSettings;
39
40use ui::{
41 prelude::*, right_click_menu, ButtonSize, Color, IconButton, IconButtonShape, IconName,
42 IconSize, Indicator, Label, Tab, TabBar, TabPosition, Tooltip,
43};
44use ui::{v_flex, ContextMenu};
45use util::{debug_panic, maybe, truncate_and_remove_front, ResultExt};
46
47/// A selected entry in e.g. project panel.
48#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
49pub struct SelectedEntry {
50 pub worktree_id: WorktreeId,
51 pub entry_id: ProjectEntryId,
52}
53
54/// A group of selected entries from project panel.
55#[derive(Debug)]
56pub struct DraggedSelection {
57 pub active_selection: SelectedEntry,
58 pub marked_selections: Arc<BTreeSet<SelectedEntry>>,
59}
60
61impl DraggedSelection {
62 pub fn items<'a>(&'a self) -> Box<dyn Iterator<Item = &'a SelectedEntry> + 'a> {
63 if self.marked_selections.contains(&self.active_selection) {
64 Box::new(self.marked_selections.iter())
65 } else {
66 Box::new(std::iter::once(&self.active_selection))
67 }
68 }
69}
70
71#[derive(PartialEq, Clone, Copy, Deserialize, Debug)]
72#[serde(rename_all = "camelCase")]
73pub enum SaveIntent {
74 /// write all files (even if unchanged)
75 /// prompt before overwriting on-disk changes
76 Save,
77 /// same as Save, but without auto formatting
78 SaveWithoutFormat,
79 /// write any files that have local changes
80 /// prompt before overwriting on-disk changes
81 SaveAll,
82 /// always prompt for a new path
83 SaveAs,
84 /// prompt "you have unsaved changes" before writing
85 Close,
86 /// write all dirty files, don't prompt on conflict
87 Overwrite,
88 /// skip all save-related behavior
89 Skip,
90}
91
92#[derive(Clone, Deserialize, PartialEq, Debug)]
93pub struct ActivateItem(pub usize);
94
95#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
96#[serde(rename_all = "camelCase")]
97pub struct CloseActiveItem {
98 pub save_intent: Option<SaveIntent>,
99}
100
101#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
102#[serde(rename_all = "camelCase")]
103pub struct CloseInactiveItems {
104 pub save_intent: Option<SaveIntent>,
105}
106
107#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
108#[serde(rename_all = "camelCase")]
109pub struct CloseAllItems {
110 pub save_intent: Option<SaveIntent>,
111}
112
113#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
114#[serde(rename_all = "camelCase")]
115pub struct RevealInProjectPanel {
116 pub entry_id: Option<u64>,
117}
118
119#[derive(PartialEq, Clone, Deserialize)]
120pub struct DeploySearch {
121 #[serde(default)]
122 pub replace_enabled: bool,
123}
124
125impl_actions!(
126 pane,
127 [
128 CloseAllItems,
129 CloseActiveItem,
130 CloseInactiveItems,
131 ActivateItem,
132 RevealInProjectPanel,
133 DeploySearch,
134 ]
135);
136
137actions!(
138 pane,
139 [
140 ActivatePrevItem,
141 ActivateNextItem,
142 ActivateLastItem,
143 AlternateFile,
144 CloseCleanItems,
145 CloseItemsToTheLeft,
146 CloseItemsToTheRight,
147 GoBack,
148 GoForward,
149 ReopenClosedItem,
150 SplitLeft,
151 SplitUp,
152 SplitRight,
153 SplitDown,
154 TogglePreviewTab,
155 ]
156);
157
158impl DeploySearch {
159 pub fn find() -> Self {
160 Self {
161 replace_enabled: false,
162 }
163 }
164}
165
166const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
167
168pub enum Event {
169 AddItem { item: Box<dyn ItemHandle> },
170 ActivateItem { local: bool },
171 Remove,
172 RemoveItem { item_id: EntityId },
173 Split(SplitDirection),
174 ChangeItemTitle,
175 Focus,
176 ZoomIn,
177 ZoomOut,
178}
179
180impl fmt::Debug for Event {
181 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
182 match self {
183 Event::AddItem { item } => f
184 .debug_struct("AddItem")
185 .field("item", &item.item_id())
186 .finish(),
187 Event::ActivateItem { local } => f
188 .debug_struct("ActivateItem")
189 .field("local", local)
190 .finish(),
191 Event::Remove => f.write_str("Remove"),
192 Event::RemoveItem { item_id } => f
193 .debug_struct("RemoveItem")
194 .field("item_id", item_id)
195 .finish(),
196 Event::Split(direction) => f
197 .debug_struct("Split")
198 .field("direction", direction)
199 .finish(),
200 Event::ChangeItemTitle => f.write_str("ChangeItemTitle"),
201 Event::Focus => f.write_str("Focus"),
202 Event::ZoomIn => f.write_str("ZoomIn"),
203 Event::ZoomOut => f.write_str("ZoomOut"),
204 }
205 }
206}
207
208/// A container for 0 to many items that are open in the workspace.
209/// Treats all items uniformly via the [`ItemHandle`] trait, whether it's an editor, search results multibuffer, terminal or something else,
210/// responsible for managing item tabs, focus and zoom states and drag and drop features.
211/// Can be split, see `PaneGroup` for more details.
212pub struct Pane {
213 alternate_file_items: (
214 Option<Box<dyn WeakItemHandle>>,
215 Option<Box<dyn WeakItemHandle>>,
216 ),
217 focus_handle: FocusHandle,
218 items: Vec<Box<dyn ItemHandle>>,
219 activation_history: Vec<ActivationHistoryEntry>,
220 next_activation_timestamp: Arc<AtomicUsize>,
221 zoomed: bool,
222 was_focused: bool,
223 active_item_index: usize,
224 preview_item_id: Option<EntityId>,
225 last_focus_handle_by_item: HashMap<EntityId, WeakFocusHandle>,
226 nav_history: NavHistory,
227 toolbar: View<Toolbar>,
228 pub new_item_menu: Option<View<ContextMenu>>,
229 split_item_menu: Option<View<ContextMenu>>,
230 // tab_context_menu: View<ContextMenu>,
231 pub(crate) workspace: WeakView<Workspace>,
232 project: Model<Project>,
233 drag_split_direction: Option<SplitDirection>,
234 can_drop_predicate: Option<Arc<dyn Fn(&dyn Any, &mut WindowContext) -> bool>>,
235 custom_drop_handle:
236 Option<Arc<dyn Fn(&mut Pane, &dyn Any, &mut ViewContext<Pane>) -> ControlFlow<(), ()>>>,
237 can_split: bool,
238 should_display_tab_bar: Rc<dyn Fn(&ViewContext<Pane>) -> bool>,
239 render_tab_bar_buttons: Rc<dyn Fn(&mut Pane, &mut ViewContext<Pane>) -> AnyElement>,
240 _subscriptions: Vec<Subscription>,
241 tab_bar_scroll_handle: ScrollHandle,
242 /// Is None if navigation buttons are permanently turned off (and should not react to setting changes).
243 /// Otherwise, when `display_nav_history_buttons` is Some, it determines whether nav buttons should be displayed.
244 display_nav_history_buttons: Option<bool>,
245 double_click_dispatch_action: Box<dyn Action>,
246 save_modals_spawned: HashSet<EntityId>,
247}
248
249pub struct ActivationHistoryEntry {
250 pub entity_id: EntityId,
251 pub timestamp: usize,
252}
253
254pub struct ItemNavHistory {
255 history: NavHistory,
256 item: Arc<dyn WeakItemHandle>,
257 is_preview: bool,
258}
259
260#[derive(Clone)]
261pub struct NavHistory(Arc<Mutex<NavHistoryState>>);
262
263struct NavHistoryState {
264 mode: NavigationMode,
265 backward_stack: VecDeque<NavigationEntry>,
266 forward_stack: VecDeque<NavigationEntry>,
267 closed_stack: VecDeque<NavigationEntry>,
268 paths_by_item: HashMap<EntityId, (ProjectPath, Option<PathBuf>)>,
269 pane: WeakView<Pane>,
270 next_timestamp: Arc<AtomicUsize>,
271}
272
273#[derive(Debug, Copy, Clone)]
274pub enum NavigationMode {
275 Normal,
276 GoingBack,
277 GoingForward,
278 ClosingItem,
279 ReopeningClosedItem,
280 Disabled,
281}
282
283impl Default for NavigationMode {
284 fn default() -> Self {
285 Self::Normal
286 }
287}
288
289pub struct NavigationEntry {
290 pub item: Arc<dyn WeakItemHandle>,
291 pub data: Option<Box<dyn Any + Send>>,
292 pub timestamp: usize,
293 pub is_preview: bool,
294}
295
296#[derive(Clone)]
297pub struct DraggedTab {
298 pub pane: View<Pane>,
299 pub item: Box<dyn ItemHandle>,
300 pub ix: usize,
301 pub detail: usize,
302 pub is_active: bool,
303}
304
305impl EventEmitter<Event> for Pane {}
306
307impl Pane {
308 pub fn new(
309 workspace: WeakView<Workspace>,
310 project: Model<Project>,
311 next_timestamp: Arc<AtomicUsize>,
312 can_drop_predicate: Option<Arc<dyn Fn(&dyn Any, &mut WindowContext) -> bool + 'static>>,
313 double_click_dispatch_action: Box<dyn Action>,
314 cx: &mut ViewContext<Self>,
315 ) -> Self {
316 let focus_handle = cx.focus_handle();
317
318 let subscriptions = vec![
319 cx.on_focus(&focus_handle, Pane::focus_in),
320 cx.on_focus_in(&focus_handle, Pane::focus_in),
321 cx.on_focus_out(&focus_handle, Pane::focus_out),
322 cx.observe_global::<SettingsStore>(Self::settings_changed),
323 ];
324
325 let handle = cx.view().downgrade();
326 Self {
327 alternate_file_items: (None, None),
328 focus_handle,
329 items: Vec::new(),
330 activation_history: Vec::new(),
331 next_activation_timestamp: next_timestamp.clone(),
332 was_focused: false,
333 zoomed: false,
334 active_item_index: 0,
335 preview_item_id: None,
336 last_focus_handle_by_item: Default::default(),
337 nav_history: NavHistory(Arc::new(Mutex::new(NavHistoryState {
338 mode: NavigationMode::Normal,
339 backward_stack: Default::default(),
340 forward_stack: Default::default(),
341 closed_stack: Default::default(),
342 paths_by_item: Default::default(),
343 pane: handle.clone(),
344 next_timestamp,
345 }))),
346 toolbar: cx.new_view(|_| Toolbar::new()),
347 new_item_menu: None,
348 split_item_menu: None,
349 tab_bar_scroll_handle: ScrollHandle::new(),
350 drag_split_direction: None,
351 workspace,
352 project,
353 can_drop_predicate,
354 custom_drop_handle: None,
355 can_split: true,
356 should_display_tab_bar: Rc::new(|cx| TabBarSettings::get_global(cx).show),
357 render_tab_bar_buttons: Rc::new(move |pane, cx| {
358 // Ideally we would return a vec of elements here to pass directly to the [TabBar]'s
359 // `end_slot`, but due to needing a view here that isn't possible.
360 h_flex()
361 // Instead we need to replicate the spacing from the [TabBar]'s `end_slot` here.
362 .gap(Spacing::Small.rems(cx))
363 .child(
364 IconButton::new("plus", IconName::Plus)
365 .icon_size(IconSize::Small)
366 .on_click(cx.listener(|pane, _, cx| {
367 let menu = ContextMenu::build(cx, |menu, _| {
368 menu.action("New File", NewFile.boxed_clone())
369 .action("New Terminal", NewCenterTerminal.boxed_clone())
370 .action("New Search", NewSearch.boxed_clone())
371 });
372 cx.subscribe(&menu, |pane, _, _: &DismissEvent, cx| {
373 pane.focus(cx);
374 pane.new_item_menu = None;
375 })
376 .detach();
377 pane.new_item_menu = Some(menu);
378 }))
379 .tooltip(|cx| Tooltip::text("New...", cx)),
380 )
381 .when_some(pane.new_item_menu.as_ref(), |el, new_item_menu| {
382 el.child(Self::render_menu_overlay(new_item_menu))
383 })
384 .child(
385 IconButton::new("split", IconName::Split)
386 .icon_size(IconSize::Small)
387 .on_click(cx.listener(|pane, _, cx| {
388 let menu = ContextMenu::build(cx, |menu, _| {
389 menu.action("Split Right", SplitRight.boxed_clone())
390 .action("Split Left", SplitLeft.boxed_clone())
391 .action("Split Up", SplitUp.boxed_clone())
392 .action("Split Down", SplitDown.boxed_clone())
393 });
394 cx.subscribe(&menu, |pane, _, _: &DismissEvent, cx| {
395 pane.focus(cx);
396 pane.split_item_menu = None;
397 })
398 .detach();
399 pane.split_item_menu = Some(menu);
400 }))
401 .tooltip(|cx| Tooltip::text("Split Pane", cx)),
402 )
403 .child({
404 let zoomed = pane.is_zoomed();
405 IconButton::new("toggle_zoom", IconName::Maximize)
406 .icon_size(IconSize::Small)
407 .selected(zoomed)
408 .selected_icon(IconName::Minimize)
409 .on_click(cx.listener(|pane, _, cx| {
410 pane.toggle_zoom(&crate::ToggleZoom, cx);
411 }))
412 .tooltip(move |cx| {
413 Tooltip::for_action(
414 if zoomed { "Zoom Out" } else { "Zoom In" },
415 &ToggleZoom,
416 cx,
417 )
418 })
419 })
420 .when_some(pane.split_item_menu.as_ref(), |el, split_item_menu| {
421 el.child(Self::render_menu_overlay(split_item_menu))
422 })
423 .into_any_element()
424 }),
425 display_nav_history_buttons: Some(
426 TabBarSettings::get_global(cx).show_nav_history_buttons,
427 ),
428 _subscriptions: subscriptions,
429 double_click_dispatch_action,
430 save_modals_spawned: HashSet::default(),
431 }
432 }
433
434 fn alternate_file(&mut self, cx: &mut ViewContext<Pane>) {
435 let (_, alternative) = &self.alternate_file_items;
436 if let Some(alternative) = alternative {
437 let existing = self
438 .items()
439 .find_position(|item| item.item_id() == alternative.id());
440 if let Some((ix, _)) = existing {
441 self.activate_item(ix, true, true, cx);
442 } else {
443 if let Some(upgraded) = alternative.upgrade() {
444 self.add_item(upgraded, true, true, None, cx);
445 }
446 }
447 }
448 }
449
450 pub fn track_alternate_file_items(&mut self) {
451 if let Some(item) = self.active_item().map(|item| item.downgrade_item()) {
452 let (current, _) = &self.alternate_file_items;
453 match current {
454 Some(current) => {
455 if current.id() != item.id() {
456 self.alternate_file_items =
457 (Some(item), self.alternate_file_items.0.take());
458 }
459 }
460 None => {
461 self.alternate_file_items = (Some(item), None);
462 }
463 }
464 }
465 }
466
467 pub fn has_focus(&self, cx: &WindowContext) -> bool {
468 // We not only check whether our focus handle contains focus, but also
469 // whether the active_item might have focus, because we might have just activated an item
470 // but that hasn't rendered yet.
471 // So before the next render, we might have transferred focus
472 // to the item and `focus_handle.contains_focus` returns false because the `active_item`
473 // is not hooked up to us in the dispatch tree.
474 self.focus_handle.contains_focused(cx)
475 || self
476 .active_item()
477 .map_or(false, |item| item.focus_handle(cx).contains_focused(cx))
478 }
479
480 fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
481 if !self.was_focused {
482 self.was_focused = true;
483 cx.emit(Event::Focus);
484 cx.notify();
485 }
486
487 self.toolbar.update(cx, |toolbar, cx| {
488 toolbar.focus_changed(true, cx);
489 });
490
491 if let Some(active_item) = self.active_item() {
492 if self.focus_handle.is_focused(cx) {
493 // Pane was focused directly. We need to either focus a view inside the active item,
494 // or focus the active item itself
495 if let Some(weak_last_focus_handle) =
496 self.last_focus_handle_by_item.get(&active_item.item_id())
497 {
498 if let Some(focus_handle) = weak_last_focus_handle.upgrade() {
499 focus_handle.focus(cx);
500 return;
501 }
502 }
503
504 active_item.focus_handle(cx).focus(cx);
505 } else if let Some(focused) = cx.focused() {
506 if !self.context_menu_focused(cx) {
507 self.last_focus_handle_by_item
508 .insert(active_item.item_id(), focused.downgrade());
509 }
510 }
511 }
512 }
513
514 fn context_menu_focused(&self, cx: &mut ViewContext<Self>) -> bool {
515 self.new_item_menu
516 .as_ref()
517 .or(self.split_item_menu.as_ref())
518 .map_or(false, |menu| menu.focus_handle(cx).is_focused(cx))
519 }
520
521 fn focus_out(&mut self, _event: FocusOutEvent, cx: &mut ViewContext<Self>) {
522 self.was_focused = false;
523 self.toolbar.update(cx, |toolbar, cx| {
524 toolbar.focus_changed(false, cx);
525 });
526 cx.notify();
527 }
528
529 fn settings_changed(&mut self, cx: &mut ViewContext<Self>) {
530 if let Some(display_nav_history_buttons) = self.display_nav_history_buttons.as_mut() {
531 *display_nav_history_buttons = TabBarSettings::get_global(cx).show_nav_history_buttons;
532 }
533 if !PreviewTabsSettings::get_global(cx).enabled {
534 self.preview_item_id = None;
535 }
536 cx.notify();
537 }
538
539 pub fn active_item_index(&self) -> usize {
540 self.active_item_index
541 }
542
543 pub fn activation_history(&self) -> &[ActivationHistoryEntry] {
544 &self.activation_history
545 }
546
547 pub fn set_should_display_tab_bar<F>(&mut self, should_display_tab_bar: F)
548 where
549 F: 'static + Fn(&ViewContext<Pane>) -> bool,
550 {
551 self.should_display_tab_bar = Rc::new(should_display_tab_bar);
552 }
553
554 pub fn set_can_split(&mut self, can_split: bool, cx: &mut ViewContext<Self>) {
555 self.can_split = can_split;
556 cx.notify();
557 }
558
559 pub fn set_can_navigate(&mut self, can_navigate: bool, cx: &mut ViewContext<Self>) {
560 self.toolbar.update(cx, |toolbar, cx| {
561 toolbar.set_can_navigate(can_navigate, cx);
562 });
563 cx.notify();
564 }
565
566 pub fn set_render_tab_bar_buttons<F>(&mut self, cx: &mut ViewContext<Self>, render: F)
567 where
568 F: 'static + Fn(&mut Pane, &mut ViewContext<Pane>) -> AnyElement,
569 {
570 self.render_tab_bar_buttons = Rc::new(render);
571 cx.notify();
572 }
573
574 pub fn set_custom_drop_handle<F>(&mut self, cx: &mut ViewContext<Self>, handle: F)
575 where
576 F: 'static + Fn(&mut Pane, &dyn Any, &mut ViewContext<Pane>) -> ControlFlow<(), ()>,
577 {
578 self.custom_drop_handle = Some(Arc::new(handle));
579 cx.notify();
580 }
581
582 pub fn nav_history_for_item<T: Item>(&self, item: &View<T>) -> ItemNavHistory {
583 ItemNavHistory {
584 history: self.nav_history.clone(),
585 item: Arc::new(item.downgrade()),
586 is_preview: self.preview_item_id == Some(item.item_id()),
587 }
588 }
589
590 pub fn nav_history(&self) -> &NavHistory {
591 &self.nav_history
592 }
593
594 pub fn nav_history_mut(&mut self) -> &mut NavHistory {
595 &mut self.nav_history
596 }
597
598 pub fn disable_history(&mut self) {
599 self.nav_history.disable();
600 }
601
602 pub fn enable_history(&mut self) {
603 self.nav_history.enable();
604 }
605
606 pub fn can_navigate_backward(&self) -> bool {
607 !self.nav_history.0.lock().backward_stack.is_empty()
608 }
609
610 pub fn can_navigate_forward(&self) -> bool {
611 !self.nav_history.0.lock().forward_stack.is_empty()
612 }
613
614 fn navigate_backward(&mut self, cx: &mut ViewContext<Self>) {
615 if let Some(workspace) = self.workspace.upgrade() {
616 let pane = cx.view().downgrade();
617 cx.window_context().defer(move |cx| {
618 workspace.update(cx, |workspace, cx| {
619 workspace.go_back(pane, cx).detach_and_log_err(cx)
620 })
621 })
622 }
623 }
624
625 fn navigate_forward(&mut self, cx: &mut ViewContext<Self>) {
626 if let Some(workspace) = self.workspace.upgrade() {
627 let pane = cx.view().downgrade();
628 cx.window_context().defer(move |cx| {
629 workspace.update(cx, |workspace, cx| {
630 workspace.go_forward(pane, cx).detach_and_log_err(cx)
631 })
632 })
633 }
634 }
635
636 fn history_updated(&mut self, cx: &mut ViewContext<Self>) {
637 self.toolbar.update(cx, |_, cx| cx.notify());
638 }
639
640 pub fn preview_item_id(&self) -> Option<EntityId> {
641 self.preview_item_id
642 }
643
644 fn preview_item_idx(&self) -> Option<usize> {
645 if let Some(preview_item_id) = self.preview_item_id {
646 self.items
647 .iter()
648 .position(|item| item.item_id() == preview_item_id)
649 } else {
650 None
651 }
652 }
653
654 pub fn is_active_preview_item(&self, item_id: EntityId) -> bool {
655 self.preview_item_id == Some(item_id)
656 }
657
658 /// Marks the item with the given ID as the preview item.
659 /// This will be ignored if the global setting `preview_tabs` is disabled.
660 pub fn set_preview_item_id(&mut self, item_id: Option<EntityId>, cx: &AppContext) {
661 if PreviewTabsSettings::get_global(cx).enabled {
662 self.preview_item_id = item_id;
663 }
664 }
665
666 pub fn handle_item_edit(&mut self, item_id: EntityId, cx: &AppContext) {
667 if let Some(preview_item_id) = self.preview_item_id {
668 if preview_item_id == item_id {
669 self.set_preview_item_id(None, cx)
670 }
671 }
672 }
673
674 pub(crate) fn open_item(
675 &mut self,
676 project_entry_id: Option<ProjectEntryId>,
677 focus_item: bool,
678 allow_preview: bool,
679 cx: &mut ViewContext<Self>,
680 build_item: impl FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
681 ) -> Box<dyn ItemHandle> {
682 let mut existing_item = None;
683 if let Some(project_entry_id) = project_entry_id {
684 for (index, item) in self.items.iter().enumerate() {
685 if item.is_singleton(cx)
686 && item.project_entry_ids(cx).as_slice() == [project_entry_id]
687 {
688 let item = item.boxed_clone();
689 existing_item = Some((index, item));
690 break;
691 }
692 }
693 }
694
695 if let Some((index, existing_item)) = existing_item {
696 // If the item is already open, and the item is a preview item
697 // and we are not allowing items to open as preview, mark the item as persistent.
698 if let Some(preview_item_id) = self.preview_item_id {
699 if let Some(tab) = self.items.get(index) {
700 if tab.item_id() == preview_item_id && !allow_preview {
701 self.set_preview_item_id(None, cx);
702 }
703 }
704 }
705
706 self.activate_item(index, focus_item, focus_item, cx);
707 existing_item
708 } else {
709 // If the item is being opened as preview and we have an existing preview tab,
710 // open the new item in the position of the existing preview tab.
711 let destination_index = if allow_preview {
712 self.close_current_preview_item(cx)
713 } else {
714 None
715 };
716
717 let new_item = build_item(cx);
718
719 if allow_preview {
720 self.set_preview_item_id(Some(new_item.item_id()), cx);
721 }
722
723 self.add_item(new_item.clone(), true, focus_item, destination_index, cx);
724
725 new_item
726 }
727 }
728
729 pub fn close_current_preview_item(&mut self, cx: &mut ViewContext<Self>) -> Option<usize> {
730 let Some(item_idx) = self.preview_item_idx() else {
731 return None;
732 };
733
734 let prev_active_item_index = self.active_item_index;
735 self.remove_item(item_idx, false, false, cx);
736 self.active_item_index = prev_active_item_index;
737
738 if item_idx < self.items.len() {
739 Some(item_idx)
740 } else {
741 None
742 }
743 }
744
745 pub fn add_item(
746 &mut self,
747 item: Box<dyn ItemHandle>,
748 activate_pane: bool,
749 focus_item: bool,
750 destination_index: Option<usize>,
751 cx: &mut ViewContext<Self>,
752 ) {
753 if item.is_singleton(cx) {
754 if let Some(&entry_id) = item.project_entry_ids(cx).get(0) {
755 let project = self.project.read(cx);
756 if let Some(project_path) = project.path_for_entry(entry_id, cx) {
757 let abs_path = project.absolute_path(&project_path, cx);
758 self.nav_history
759 .0
760 .lock()
761 .paths_by_item
762 .insert(item.item_id(), (project_path, abs_path));
763 }
764 }
765 }
766 // If no destination index is specified, add or move the item after the active item.
767 let mut insertion_index = {
768 cmp::min(
769 if let Some(destination_index) = destination_index {
770 destination_index
771 } else {
772 self.active_item_index + 1
773 },
774 self.items.len(),
775 )
776 };
777
778 // Does the item already exist?
779 let project_entry_id = if item.is_singleton(cx) {
780 item.project_entry_ids(cx).get(0).copied()
781 } else {
782 None
783 };
784
785 let existing_item_index = self.items.iter().position(|existing_item| {
786 if existing_item.item_id() == item.item_id() {
787 true
788 } else if existing_item.is_singleton(cx) {
789 existing_item
790 .project_entry_ids(cx)
791 .get(0)
792 .map_or(false, |existing_entry_id| {
793 Some(existing_entry_id) == project_entry_id.as_ref()
794 })
795 } else {
796 false
797 }
798 });
799
800 if let Some(existing_item_index) = existing_item_index {
801 // If the item already exists, move it to the desired destination and activate it
802
803 if existing_item_index != insertion_index {
804 let existing_item_is_active = existing_item_index == self.active_item_index;
805
806 // If the caller didn't specify a destination and the added item is already
807 // the active one, don't move it
808 if existing_item_is_active && destination_index.is_none() {
809 insertion_index = existing_item_index;
810 } else {
811 self.items.remove(existing_item_index);
812 if existing_item_index < self.active_item_index {
813 self.active_item_index -= 1;
814 }
815 insertion_index = insertion_index.min(self.items.len());
816
817 self.items.insert(insertion_index, item.clone());
818
819 if existing_item_is_active {
820 self.active_item_index = insertion_index;
821 } else if insertion_index <= self.active_item_index {
822 self.active_item_index += 1;
823 }
824 }
825
826 cx.notify();
827 }
828
829 self.activate_item(insertion_index, activate_pane, focus_item, cx);
830 } else {
831 self.items.insert(insertion_index, item.clone());
832
833 if insertion_index <= self.active_item_index
834 && self.preview_item_idx() != Some(self.active_item_index)
835 {
836 self.active_item_index += 1;
837 }
838
839 self.activate_item(insertion_index, activate_pane, focus_item, cx);
840 cx.notify();
841 }
842
843 cx.emit(Event::AddItem { item });
844 }
845
846 pub fn items_len(&self) -> usize {
847 self.items.len()
848 }
849
850 pub fn items(&self) -> impl DoubleEndedIterator<Item = &Box<dyn ItemHandle>> {
851 self.items.iter()
852 }
853
854 pub fn items_of_type<T: Render>(&self) -> impl '_ + Iterator<Item = View<T>> {
855 self.items
856 .iter()
857 .filter_map(|item| item.to_any().downcast().ok())
858 }
859
860 pub fn active_item(&self) -> Option<Box<dyn ItemHandle>> {
861 self.items.get(self.active_item_index).cloned()
862 }
863
864 pub fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Point<Pixels>> {
865 self.items
866 .get(self.active_item_index)?
867 .pixel_position_of_cursor(cx)
868 }
869
870 pub fn item_for_entry(
871 &self,
872 entry_id: ProjectEntryId,
873 cx: &AppContext,
874 ) -> Option<Box<dyn ItemHandle>> {
875 self.items.iter().find_map(|item| {
876 if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
877 Some(item.boxed_clone())
878 } else {
879 None
880 }
881 })
882 }
883
884 pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option<usize> {
885 self.items
886 .iter()
887 .position(|i| i.item_id() == item.item_id())
888 }
889
890 pub fn item_for_index(&self, ix: usize) -> Option<&dyn ItemHandle> {
891 self.items.get(ix).map(|i| i.as_ref())
892 }
893
894 pub fn toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext<Self>) {
895 if self.zoomed {
896 cx.emit(Event::ZoomOut);
897 } else if !self.items.is_empty() {
898 if !self.focus_handle.contains_focused(cx) {
899 cx.focus_self();
900 }
901 cx.emit(Event::ZoomIn);
902 }
903 }
904
905 pub fn activate_item(
906 &mut self,
907 index: usize,
908 activate_pane: bool,
909 focus_item: bool,
910 cx: &mut ViewContext<Self>,
911 ) {
912 use NavigationMode::{GoingBack, GoingForward};
913
914 if index < self.items.len() {
915 let prev_active_item_ix = mem::replace(&mut self.active_item_index, index);
916 if prev_active_item_ix != self.active_item_index
917 || matches!(self.nav_history.mode(), GoingBack | GoingForward)
918 {
919 if let Some(prev_item) = self.items.get(prev_active_item_ix) {
920 prev_item.deactivated(cx);
921 }
922 }
923 cx.emit(Event::ActivateItem {
924 local: activate_pane,
925 });
926
927 if let Some(newly_active_item) = self.items.get(index) {
928 self.activation_history
929 .retain(|entry| entry.entity_id != newly_active_item.item_id());
930 self.activation_history.push(ActivationHistoryEntry {
931 entity_id: newly_active_item.item_id(),
932 timestamp: self
933 .next_activation_timestamp
934 .fetch_add(1, Ordering::SeqCst),
935 });
936 }
937
938 self.update_toolbar(cx);
939 self.update_status_bar(cx);
940
941 if focus_item {
942 self.focus_active_item(cx);
943 }
944
945 self.tab_bar_scroll_handle.scroll_to_item(index);
946 cx.notify();
947 }
948 }
949
950 pub fn activate_prev_item(&mut self, activate_pane: bool, cx: &mut ViewContext<Self>) {
951 let mut index = self.active_item_index;
952 if index > 0 {
953 index -= 1;
954 } else if !self.items.is_empty() {
955 index = self.items.len() - 1;
956 }
957 self.activate_item(index, activate_pane, activate_pane, cx);
958 }
959
960 pub fn activate_next_item(&mut self, activate_pane: bool, cx: &mut ViewContext<Self>) {
961 let mut index = self.active_item_index;
962 if index + 1 < self.items.len() {
963 index += 1;
964 } else {
965 index = 0;
966 }
967 self.activate_item(index, activate_pane, activate_pane, cx);
968 }
969
970 pub fn close_active_item(
971 &mut self,
972 action: &CloseActiveItem,
973 cx: &mut ViewContext<Self>,
974 ) -> Option<Task<Result<()>>> {
975 if self.items.is_empty() {
976 // Close the window when there's no active items to close, if configured
977 if WorkspaceSettings::get_global(cx)
978 .when_closing_with_no_tabs
979 .should_close()
980 {
981 cx.dispatch_action(Box::new(CloseWindow));
982 }
983
984 return None;
985 }
986 let active_item_id = self.items[self.active_item_index].item_id();
987 Some(self.close_item_by_id(
988 active_item_id,
989 action.save_intent.unwrap_or(SaveIntent::Close),
990 cx,
991 ))
992 }
993
994 pub fn close_item_by_id(
995 &mut self,
996 item_id_to_close: EntityId,
997 save_intent: SaveIntent,
998 cx: &mut ViewContext<Self>,
999 ) -> Task<Result<()>> {
1000 self.close_items(cx, save_intent, move |view_id| view_id == item_id_to_close)
1001 }
1002
1003 pub fn close_inactive_items(
1004 &mut self,
1005 action: &CloseInactiveItems,
1006 cx: &mut ViewContext<Self>,
1007 ) -> Option<Task<Result<()>>> {
1008 if self.items.is_empty() {
1009 return None;
1010 }
1011
1012 let active_item_id = self.items[self.active_item_index].item_id();
1013 Some(self.close_items(
1014 cx,
1015 action.save_intent.unwrap_or(SaveIntent::Close),
1016 move |item_id| item_id != active_item_id,
1017 ))
1018 }
1019
1020 pub fn close_clean_items(
1021 &mut self,
1022 _: &CloseCleanItems,
1023 cx: &mut ViewContext<Self>,
1024 ) -> Option<Task<Result<()>>> {
1025 let item_ids: Vec<_> = self
1026 .items()
1027 .filter(|item| !item.is_dirty(cx))
1028 .map(|item| item.item_id())
1029 .collect();
1030 Some(self.close_items(cx, SaveIntent::Close, move |item_id| {
1031 item_ids.contains(&item_id)
1032 }))
1033 }
1034
1035 pub fn close_items_to_the_left(
1036 &mut self,
1037 _: &CloseItemsToTheLeft,
1038 cx: &mut ViewContext<Self>,
1039 ) -> Option<Task<Result<()>>> {
1040 if self.items.is_empty() {
1041 return None;
1042 }
1043 let active_item_id = self.items[self.active_item_index].item_id();
1044 Some(self.close_items_to_the_left_by_id(active_item_id, cx))
1045 }
1046
1047 pub fn close_items_to_the_left_by_id(
1048 &mut self,
1049 item_id: EntityId,
1050 cx: &mut ViewContext<Self>,
1051 ) -> Task<Result<()>> {
1052 let item_ids: Vec<_> = self
1053 .items()
1054 .take_while(|item| item.item_id() != item_id)
1055 .map(|item| item.item_id())
1056 .collect();
1057 self.close_items(cx, SaveIntent::Close, move |item_id| {
1058 item_ids.contains(&item_id)
1059 })
1060 }
1061
1062 pub fn close_items_to_the_right(
1063 &mut self,
1064 _: &CloseItemsToTheRight,
1065 cx: &mut ViewContext<Self>,
1066 ) -> Option<Task<Result<()>>> {
1067 if self.items.is_empty() {
1068 return None;
1069 }
1070 let active_item_id = self.items[self.active_item_index].item_id();
1071 Some(self.close_items_to_the_right_by_id(active_item_id, cx))
1072 }
1073
1074 pub fn close_items_to_the_right_by_id(
1075 &mut self,
1076 item_id: EntityId,
1077 cx: &mut ViewContext<Self>,
1078 ) -> Task<Result<()>> {
1079 let item_ids: Vec<_> = self
1080 .items()
1081 .rev()
1082 .take_while(|item| item.item_id() != item_id)
1083 .map(|item| item.item_id())
1084 .collect();
1085 self.close_items(cx, SaveIntent::Close, move |item_id| {
1086 item_ids.contains(&item_id)
1087 })
1088 }
1089
1090 pub fn close_all_items(
1091 &mut self,
1092 action: &CloseAllItems,
1093 cx: &mut ViewContext<Self>,
1094 ) -> Option<Task<Result<()>>> {
1095 if self.items.is_empty() {
1096 return None;
1097 }
1098
1099 Some(
1100 self.close_items(cx, action.save_intent.unwrap_or(SaveIntent::Close), |_| {
1101 true
1102 }),
1103 )
1104 }
1105
1106 pub(super) fn file_names_for_prompt(
1107 items: &mut dyn Iterator<Item = &Box<dyn ItemHandle>>,
1108 all_dirty_items: usize,
1109 cx: &AppContext,
1110 ) -> (String, String) {
1111 /// Quantity of item paths displayed in prompt prior to cutoff..
1112 const FILE_NAMES_CUTOFF_POINT: usize = 10;
1113 let mut file_names: Vec<_> = items
1114 .filter_map(|item| {
1115 item.project_path(cx).and_then(|project_path| {
1116 project_path
1117 .path
1118 .file_name()
1119 .and_then(|name| name.to_str().map(ToOwned::to_owned))
1120 })
1121 })
1122 .take(FILE_NAMES_CUTOFF_POINT)
1123 .collect();
1124 let should_display_followup_text =
1125 all_dirty_items > FILE_NAMES_CUTOFF_POINT || file_names.len() != all_dirty_items;
1126 if should_display_followup_text {
1127 let not_shown_files = all_dirty_items - file_names.len();
1128 if not_shown_files == 1 {
1129 file_names.push(".. 1 file not shown".into());
1130 } else {
1131 file_names.push(format!(".. {} files not shown", not_shown_files));
1132 }
1133 }
1134 (
1135 format!(
1136 "Do you want to save changes to the following {} files?",
1137 all_dirty_items
1138 ),
1139 file_names.join("\n"),
1140 )
1141 }
1142
1143 pub fn close_items(
1144 &mut self,
1145 cx: &mut ViewContext<Pane>,
1146 mut save_intent: SaveIntent,
1147 should_close: impl Fn(EntityId) -> bool,
1148 ) -> Task<Result<()>> {
1149 // Find the items to close.
1150 let mut items_to_close = Vec::new();
1151 let mut dirty_items = Vec::new();
1152 for item in &self.items {
1153 if should_close(item.item_id()) {
1154 items_to_close.push(item.boxed_clone());
1155 if item.is_dirty(cx) {
1156 dirty_items.push(item.boxed_clone());
1157 }
1158 }
1159 }
1160
1161 let active_item_id = self.active_item().map(|item| item.item_id());
1162
1163 items_to_close.sort_by_key(|item| {
1164 // Put the currently active item at the end, because if the currently active item is not closed last
1165 // closing the currently active item will cause the focus to switch to another item
1166 // This will cause Zed to expand the content of the currently active item
1167 active_item_id.filter(|&id| id == item.item_id()).is_some()
1168 // If a buffer is open both in a singleton editor and in a multibuffer, make sure
1169 // to focus the singleton buffer when prompting to save that buffer, as opposed
1170 // to focusing the multibuffer, because this gives the user a more clear idea
1171 // of what content they would be saving.
1172 || !item.is_singleton(cx)
1173 });
1174
1175 let workspace = self.workspace.clone();
1176 cx.spawn(|pane, mut cx| async move {
1177 if save_intent == SaveIntent::Close && dirty_items.len() > 1 {
1178 let answer = pane.update(&mut cx, |_, cx| {
1179 let (prompt, detail) =
1180 Self::file_names_for_prompt(&mut dirty_items.iter(), dirty_items.len(), cx);
1181 cx.prompt(
1182 PromptLevel::Warning,
1183 &prompt,
1184 Some(&detail),
1185 &["Save all", "Discard all", "Cancel"],
1186 )
1187 })?;
1188 match answer.await {
1189 Ok(0) => save_intent = SaveIntent::SaveAll,
1190 Ok(1) => save_intent = SaveIntent::Skip,
1191 _ => {}
1192 }
1193 }
1194 let mut saved_project_items_ids = HashSet::default();
1195 for item in items_to_close.clone() {
1196 // Find the item's current index and its set of project item models. Avoid
1197 // storing these in advance, in case they have changed since this task
1198 // was started.
1199 let (item_ix, mut project_item_ids) = pane.update(&mut cx, |pane, cx| {
1200 (pane.index_for_item(&*item), item.project_item_model_ids(cx))
1201 })?;
1202 let item_ix = if let Some(ix) = item_ix {
1203 ix
1204 } else {
1205 continue;
1206 };
1207
1208 // Check if this view has any project items that are not open anywhere else
1209 // in the workspace, AND that the user has not already been prompted to save.
1210 // If there are any such project entries, prompt the user to save this item.
1211 let project = workspace.update(&mut cx, |workspace, cx| {
1212 for item in workspace.items(cx) {
1213 if !items_to_close
1214 .iter()
1215 .any(|item_to_close| item_to_close.item_id() == item.item_id())
1216 {
1217 let other_project_item_ids = item.project_item_model_ids(cx);
1218 project_item_ids.retain(|id| !other_project_item_ids.contains(id));
1219 }
1220 }
1221 workspace.project().clone()
1222 })?;
1223 let should_save = project_item_ids
1224 .iter()
1225 .any(|id| saved_project_items_ids.insert(*id));
1226
1227 if should_save
1228 && !Self::save_item(
1229 project.clone(),
1230 &pane,
1231 item_ix,
1232 &*item,
1233 save_intent,
1234 &mut cx,
1235 )
1236 .await?
1237 {
1238 break;
1239 }
1240
1241 // Remove the item from the pane.
1242 pane.update(&mut cx, |pane, cx| {
1243 if let Some(item_ix) = pane
1244 .items
1245 .iter()
1246 .position(|i| i.item_id() == item.item_id())
1247 {
1248 pane.remove_item(item_ix, false, true, cx);
1249 }
1250 })
1251 .ok();
1252 }
1253
1254 pane.update(&mut cx, |_, cx| cx.notify()).ok();
1255 Ok(())
1256 })
1257 }
1258
1259 pub fn remove_item(
1260 &mut self,
1261 item_index: usize,
1262 activate_pane: bool,
1263 close_pane_if_empty: bool,
1264 cx: &mut ViewContext<Self>,
1265 ) {
1266 self.activation_history
1267 .retain(|entry| entry.entity_id != self.items[item_index].item_id());
1268
1269 if item_index == self.active_item_index {
1270 let index_to_activate = self
1271 .activation_history
1272 .pop()
1273 .and_then(|last_activated_item| {
1274 self.items.iter().enumerate().find_map(|(index, item)| {
1275 (item.item_id() == last_activated_item.entity_id).then_some(index)
1276 })
1277 })
1278 // We didn't have a valid activation history entry, so fallback
1279 // to activating the item to the left
1280 .unwrap_or_else(|| item_index.min(self.items.len()).saturating_sub(1));
1281
1282 let should_activate = activate_pane || self.has_focus(cx);
1283 if self.items.len() == 1 && should_activate {
1284 self.focus_handle.focus(cx);
1285 } else {
1286 self.activate_item(index_to_activate, should_activate, should_activate, cx);
1287 }
1288 }
1289
1290 let item = self.items.remove(item_index);
1291
1292 cx.emit(Event::RemoveItem {
1293 item_id: item.item_id(),
1294 });
1295 if self.items.is_empty() {
1296 item.deactivated(cx);
1297 if close_pane_if_empty {
1298 self.update_toolbar(cx);
1299 cx.emit(Event::Remove);
1300 }
1301 }
1302
1303 if item_index < self.active_item_index {
1304 self.active_item_index -= 1;
1305 }
1306
1307 let mode = self.nav_history.mode();
1308 self.nav_history.set_mode(NavigationMode::ClosingItem);
1309 item.deactivated(cx);
1310 self.nav_history.set_mode(mode);
1311
1312 if self.is_active_preview_item(item.item_id()) {
1313 self.set_preview_item_id(None, cx);
1314 }
1315
1316 if let Some(path) = item.project_path(cx) {
1317 let abs_path = self
1318 .nav_history
1319 .0
1320 .lock()
1321 .paths_by_item
1322 .get(&item.item_id())
1323 .and_then(|(_, abs_path)| abs_path.clone());
1324
1325 self.nav_history
1326 .0
1327 .lock()
1328 .paths_by_item
1329 .insert(item.item_id(), (path, abs_path));
1330 } else {
1331 self.nav_history
1332 .0
1333 .lock()
1334 .paths_by_item
1335 .remove(&item.item_id());
1336 }
1337
1338 if self.items.is_empty() && close_pane_if_empty && self.zoomed {
1339 cx.emit(Event::ZoomOut);
1340 }
1341
1342 cx.notify();
1343 }
1344
1345 pub async fn save_item(
1346 project: Model<Project>,
1347 pane: &WeakView<Pane>,
1348 item_ix: usize,
1349 item: &dyn ItemHandle,
1350 save_intent: SaveIntent,
1351 cx: &mut AsyncWindowContext,
1352 ) -> Result<bool> {
1353 const CONFLICT_MESSAGE: &str =
1354 "This file has changed on disk since you started editing it. Do you want to overwrite it?";
1355
1356 if save_intent == SaveIntent::Skip {
1357 return Ok(true);
1358 }
1359
1360 let (mut has_conflict, mut is_dirty, mut can_save, can_save_as) = cx.update(|cx| {
1361 (
1362 item.has_conflict(cx),
1363 item.is_dirty(cx),
1364 item.can_save(cx),
1365 item.is_singleton(cx),
1366 )
1367 })?;
1368
1369 // when saving a single buffer, we ignore whether or not it's dirty.
1370 if save_intent == SaveIntent::Save || save_intent == SaveIntent::SaveWithoutFormat {
1371 is_dirty = true;
1372 }
1373
1374 if save_intent == SaveIntent::SaveAs {
1375 is_dirty = true;
1376 has_conflict = false;
1377 can_save = false;
1378 }
1379
1380 if save_intent == SaveIntent::Overwrite {
1381 has_conflict = false;
1382 }
1383
1384 let should_format = save_intent != SaveIntent::SaveWithoutFormat;
1385
1386 if has_conflict && can_save {
1387 let answer = pane.update(cx, |pane, cx| {
1388 pane.activate_item(item_ix, true, true, cx);
1389 cx.prompt(
1390 PromptLevel::Warning,
1391 CONFLICT_MESSAGE,
1392 None,
1393 &["Overwrite", "Discard", "Cancel"],
1394 )
1395 })?;
1396 match answer.await {
1397 Ok(0) => {
1398 pane.update(cx, |_, cx| item.save(should_format, project, cx))?
1399 .await?
1400 }
1401 Ok(1) => pane.update(cx, |_, cx| item.reload(project, cx))?.await?,
1402 _ => return Ok(false),
1403 }
1404 } else if is_dirty && (can_save || can_save_as) {
1405 if save_intent == SaveIntent::Close {
1406 let will_autosave = cx.update(|cx| {
1407 matches!(
1408 WorkspaceSettings::get_global(cx).autosave,
1409 AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange
1410 ) && Self::can_autosave_item(item, cx)
1411 })?;
1412 if !will_autosave {
1413 let item_id = item.item_id();
1414 let answer_task = pane.update(cx, |pane, cx| {
1415 if pane.save_modals_spawned.insert(item_id) {
1416 pane.activate_item(item_ix, true, true, cx);
1417 let prompt = dirty_message_for(item.project_path(cx));
1418 Some(cx.prompt(
1419 PromptLevel::Warning,
1420 &prompt,
1421 None,
1422 &["Save", "Don't Save", "Cancel"],
1423 ))
1424 } else {
1425 None
1426 }
1427 })?;
1428 if let Some(answer_task) = answer_task {
1429 let answer = answer_task.await;
1430 pane.update(cx, |pane, _| {
1431 if !pane.save_modals_spawned.remove(&item_id) {
1432 debug_panic!(
1433 "save modal was not present in spawned modals after awaiting for its answer"
1434 )
1435 }
1436 })?;
1437 match answer {
1438 Ok(0) => {}
1439 Ok(1) => return Ok(true), // Don't save this file
1440 _ => return Ok(false), // Cancel
1441 }
1442 } else {
1443 return Ok(false);
1444 }
1445 }
1446 }
1447
1448 if can_save {
1449 pane.update(cx, |_, cx| item.save(should_format, project, cx))?
1450 .await?;
1451 } else if can_save_as {
1452 let abs_path = pane.update(cx, |pane, cx| {
1453 pane.workspace
1454 .update(cx, |workspace, cx| workspace.prompt_for_new_path(cx))
1455 })??;
1456 if let Some(abs_path) = abs_path.await.ok().flatten() {
1457 pane.update(cx, |_, cx| item.save_as(project, abs_path, cx))?
1458 .await?;
1459 } else {
1460 return Ok(false);
1461 }
1462 }
1463 }
1464 Ok(true)
1465 }
1466
1467 fn can_autosave_item(item: &dyn ItemHandle, cx: &AppContext) -> bool {
1468 let is_deleted = item.project_entry_ids(cx).is_empty();
1469 item.is_dirty(cx) && !item.has_conflict(cx) && item.can_save(cx) && !is_deleted
1470 }
1471
1472 pub fn autosave_item(
1473 item: &dyn ItemHandle,
1474 project: Model<Project>,
1475 cx: &mut WindowContext,
1476 ) -> Task<Result<()>> {
1477 let format = if let AutosaveSetting::AfterDelay { .. } =
1478 WorkspaceSettings::get_global(cx).autosave
1479 {
1480 false
1481 } else {
1482 true
1483 };
1484 if Self::can_autosave_item(item, cx) {
1485 item.save(format, project, cx)
1486 } else {
1487 Task::ready(Ok(()))
1488 }
1489 }
1490
1491 pub fn focus(&mut self, cx: &mut ViewContext<Pane>) {
1492 cx.focus(&self.focus_handle);
1493 }
1494
1495 pub fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
1496 if let Some(active_item) = self.active_item() {
1497 let focus_handle = active_item.focus_handle(cx);
1498 cx.focus(&focus_handle);
1499 }
1500 }
1501
1502 pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext<Self>) {
1503 cx.emit(Event::Split(direction));
1504 }
1505
1506 pub fn toolbar(&self) -> &View<Toolbar> {
1507 &self.toolbar
1508 }
1509
1510 pub fn handle_deleted_project_item(
1511 &mut self,
1512 entry_id: ProjectEntryId,
1513 cx: &mut ViewContext<Pane>,
1514 ) -> Option<()> {
1515 let (item_index_to_delete, item_id) = self.items().enumerate().find_map(|(i, item)| {
1516 if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
1517 Some((i, item.item_id()))
1518 } else {
1519 None
1520 }
1521 })?;
1522
1523 self.remove_item(item_index_to_delete, false, true, cx);
1524 self.nav_history.remove_item(item_id);
1525
1526 Some(())
1527 }
1528
1529 fn update_toolbar(&mut self, cx: &mut ViewContext<Self>) {
1530 let active_item = self
1531 .items
1532 .get(self.active_item_index)
1533 .map(|item| item.as_ref());
1534 self.toolbar.update(cx, |toolbar, cx| {
1535 toolbar.set_active_item(active_item, cx);
1536 });
1537 }
1538
1539 fn update_status_bar(&mut self, cx: &mut ViewContext<Self>) {
1540 let workspace = self.workspace.clone();
1541 let pane = cx.view().clone();
1542
1543 cx.window_context().defer(move |cx| {
1544 let Ok(status_bar) = workspace.update(cx, |workspace, _| workspace.status_bar.clone())
1545 else {
1546 return;
1547 };
1548
1549 status_bar.update(cx, move |status_bar, cx| {
1550 status_bar.set_active_pane(&pane, cx);
1551 });
1552 });
1553 }
1554
1555 fn render_tab(
1556 &self,
1557 ix: usize,
1558 item: &Box<dyn ItemHandle>,
1559 detail: usize,
1560 cx: &mut ViewContext<'_, Pane>,
1561 ) -> impl IntoElement {
1562 let is_active = ix == self.active_item_index;
1563 let is_preview = self
1564 .preview_item_id
1565 .map(|id| id == item.item_id())
1566 .unwrap_or(false);
1567
1568 let label = item.tab_content(
1569 TabContentParams {
1570 detail: Some(detail),
1571 selected: is_active,
1572 preview: is_preview,
1573 },
1574 cx,
1575 );
1576 let close_side = &ItemSettings::get_global(cx).close_position;
1577 let indicator = render_item_indicator(item.boxed_clone(), cx);
1578 let item_id = item.item_id();
1579 let is_first_item = ix == 0;
1580 let is_last_item = ix == self.items.len() - 1;
1581 let position_relative_to_active_item = ix.cmp(&self.active_item_index);
1582
1583 let tab = Tab::new(ix)
1584 .position(if is_first_item {
1585 TabPosition::First
1586 } else if is_last_item {
1587 TabPosition::Last
1588 } else {
1589 TabPosition::Middle(position_relative_to_active_item)
1590 })
1591 .close_side(match close_side {
1592 ClosePosition::Left => ui::TabCloseSide::Start,
1593 ClosePosition::Right => ui::TabCloseSide::End,
1594 })
1595 .selected(is_active)
1596 .on_click(
1597 cx.listener(move |pane: &mut Self, _, cx| pane.activate_item(ix, true, true, cx)),
1598 )
1599 // TODO: This should be a click listener with the middle mouse button instead of a mouse down listener.
1600 .on_mouse_down(
1601 MouseButton::Middle,
1602 cx.listener(move |pane, _event, cx| {
1603 pane.close_item_by_id(item_id, SaveIntent::Close, cx)
1604 .detach_and_log_err(cx);
1605 }),
1606 )
1607 .on_mouse_down(
1608 MouseButton::Left,
1609 cx.listener(move |pane, event: &MouseDownEvent, cx| {
1610 if let Some(id) = pane.preview_item_id {
1611 if id == item_id && event.click_count > 1 {
1612 pane.set_preview_item_id(None, cx);
1613 }
1614 }
1615 }),
1616 )
1617 .on_drag(
1618 DraggedTab {
1619 item: item.boxed_clone(),
1620 pane: cx.view().clone(),
1621 detail,
1622 is_active,
1623 ix,
1624 },
1625 |tab, cx| cx.new_view(|_| tab.clone()),
1626 )
1627 .drag_over::<DraggedTab>(|tab, _, cx| {
1628 tab.bg(cx.theme().colors().drop_target_background)
1629 })
1630 .drag_over::<DraggedSelection>(|tab, _, cx| {
1631 tab.bg(cx.theme().colors().drop_target_background)
1632 })
1633 .when_some(self.can_drop_predicate.clone(), |this, p| {
1634 this.can_drop(move |a, cx| p(a, cx))
1635 })
1636 .on_drop(cx.listener(move |this, dragged_tab: &DraggedTab, cx| {
1637 this.drag_split_direction = None;
1638 this.handle_tab_drop(dragged_tab, ix, cx)
1639 }))
1640 .on_drop(cx.listener(move |this, selection: &DraggedSelection, cx| {
1641 this.drag_split_direction = None;
1642 this.handle_project_entry_drop(&selection.active_selection.entry_id, cx)
1643 }))
1644 .on_drop(cx.listener(move |this, paths, cx| {
1645 this.drag_split_direction = None;
1646 this.handle_external_paths_drop(paths, cx)
1647 }))
1648 .when_some(item.tab_tooltip_text(cx), |tab, text| {
1649 tab.tooltip(move |cx| Tooltip::text(text.clone(), cx))
1650 })
1651 .start_slot::<Indicator>(indicator)
1652 .end_slot(
1653 IconButton::new("close tab", IconName::Close)
1654 .shape(IconButtonShape::Square)
1655 .icon_color(Color::Muted)
1656 .size(ButtonSize::None)
1657 .icon_size(IconSize::XSmall)
1658 .on_click(cx.listener(move |pane, _, cx| {
1659 pane.close_item_by_id(item_id, SaveIntent::Close, cx)
1660 .detach_and_log_err(cx);
1661 })),
1662 )
1663 .child(label);
1664
1665 let single_entry_to_resolve = {
1666 let item_entries = self.items[ix].project_entry_ids(cx);
1667 if item_entries.len() == 1 {
1668 Some(item_entries[0])
1669 } else {
1670 None
1671 }
1672 };
1673
1674 let pane = cx.view().downgrade();
1675 right_click_menu(ix).trigger(tab).menu(move |cx| {
1676 let pane = pane.clone();
1677 ContextMenu::build(cx, move |mut menu, cx| {
1678 if let Some(pane) = pane.upgrade() {
1679 menu = menu
1680 .entry(
1681 "Close",
1682 Some(Box::new(CloseActiveItem { save_intent: None })),
1683 cx.handler_for(&pane, move |pane, cx| {
1684 pane.close_item_by_id(item_id, SaveIntent::Close, cx)
1685 .detach_and_log_err(cx);
1686 }),
1687 )
1688 .entry(
1689 "Close Others",
1690 Some(Box::new(CloseInactiveItems { save_intent: None })),
1691 cx.handler_for(&pane, move |pane, cx| {
1692 pane.close_items(cx, SaveIntent::Close, |id| id != item_id)
1693 .detach_and_log_err(cx);
1694 }),
1695 )
1696 .separator()
1697 .entry(
1698 "Close Left",
1699 Some(Box::new(CloseItemsToTheLeft)),
1700 cx.handler_for(&pane, move |pane, cx| {
1701 pane.close_items_to_the_left_by_id(item_id, cx)
1702 .detach_and_log_err(cx);
1703 }),
1704 )
1705 .entry(
1706 "Close Right",
1707 Some(Box::new(CloseItemsToTheRight)),
1708 cx.handler_for(&pane, move |pane, cx| {
1709 pane.close_items_to_the_right_by_id(item_id, cx)
1710 .detach_and_log_err(cx);
1711 }),
1712 )
1713 .separator()
1714 .entry(
1715 "Close Clean",
1716 Some(Box::new(CloseCleanItems)),
1717 cx.handler_for(&pane, move |pane, cx| {
1718 if let Some(task) = pane.close_clean_items(&CloseCleanItems, cx) {
1719 task.detach_and_log_err(cx)
1720 }
1721 }),
1722 )
1723 .entry(
1724 "Close All",
1725 Some(Box::new(CloseAllItems { save_intent: None })),
1726 cx.handler_for(&pane, |pane, cx| {
1727 if let Some(task) =
1728 pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
1729 {
1730 task.detach_and_log_err(cx)
1731 }
1732 }),
1733 );
1734
1735 if let Some(entry) = single_entry_to_resolve {
1736 let parent_abs_path = pane
1737 .update(cx, |pane, cx| {
1738 pane.workspace.update(cx, |workspace, cx| {
1739 let project = workspace.project().read(cx);
1740 project.worktree_for_entry(entry, cx).and_then(|worktree| {
1741 let worktree = worktree.read(cx);
1742 let entry = worktree.entry_for_id(entry)?;
1743 let abs_path = worktree.absolutize(&entry.path).ok()?;
1744 let parent = if entry.is_symlink {
1745 abs_path.canonicalize().ok()?
1746 } else {
1747 abs_path
1748 }
1749 .parent()?
1750 .to_path_buf();
1751 Some(parent)
1752 })
1753 })
1754 })
1755 .ok()
1756 .flatten();
1757
1758 let entry_id = entry.to_proto();
1759 menu = menu
1760 .separator()
1761 .entry(
1762 "Reveal In Project Panel",
1763 Some(Box::new(RevealInProjectPanel {
1764 entry_id: Some(entry_id),
1765 })),
1766 cx.handler_for(&pane, move |pane, cx| {
1767 pane.project.update(cx, |_, cx| {
1768 cx.emit(project::Event::RevealInProjectPanel(
1769 ProjectEntryId::from_proto(entry_id),
1770 ))
1771 });
1772 }),
1773 )
1774 .when_some(parent_abs_path, |menu, abs_path| {
1775 menu.entry(
1776 "Open in Terminal",
1777 Some(Box::new(OpenInTerminal)),
1778 cx.handler_for(&pane, move |_, cx| {
1779 cx.dispatch_action(
1780 OpenTerminal {
1781 working_directory: abs_path.clone(),
1782 }
1783 .boxed_clone(),
1784 );
1785 }),
1786 )
1787 });
1788 }
1789 }
1790
1791 menu
1792 })
1793 })
1794 }
1795
1796 fn render_tab_bar(&mut self, cx: &mut ViewContext<'_, Pane>) -> impl IntoElement {
1797 let navigate_backward = IconButton::new("navigate_backward", IconName::ArrowLeft)
1798 .shape(IconButtonShape::Square)
1799 .icon_size(IconSize::Small)
1800 .on_click({
1801 let view = cx.view().clone();
1802 move |_, cx| view.update(cx, Self::navigate_backward)
1803 })
1804 .disabled(!self.can_navigate_backward())
1805 .tooltip(|cx| Tooltip::for_action("Go Back", &GoBack, cx));
1806
1807 let navigate_forward = IconButton::new("navigate_forward", IconName::ArrowRight)
1808 .shape(IconButtonShape::Square)
1809 .icon_size(IconSize::Small)
1810 .on_click({
1811 let view = cx.view().clone();
1812 move |_, cx| view.update(cx, Self::navigate_forward)
1813 })
1814 .disabled(!self.can_navigate_forward())
1815 .tooltip(|cx| Tooltip::for_action("Go Forward", &GoForward, cx));
1816
1817 TabBar::new("tab_bar")
1818 .track_scroll(self.tab_bar_scroll_handle.clone())
1819 .when(
1820 self.display_nav_history_buttons.unwrap_or_default(),
1821 |tab_bar| tab_bar.start_children(vec![navigate_backward, navigate_forward]),
1822 )
1823 .when(self.has_focus(cx), |tab_bar| {
1824 tab_bar.end_child({
1825 let render_tab_buttons = self.render_tab_bar_buttons.clone();
1826 render_tab_buttons(self, cx)
1827 })
1828 })
1829 .children(
1830 self.items
1831 .iter()
1832 .enumerate()
1833 .zip(tab_details(&self.items, cx))
1834 .map(|((ix, item), detail)| self.render_tab(ix, item, detail, cx)),
1835 )
1836 .child(
1837 div()
1838 .id("tab_bar_drop_target")
1839 .min_w_6()
1840 // HACK: This empty child is currently necessary to force the drop target to appear
1841 // despite us setting a min width above.
1842 .child("")
1843 .h_full()
1844 .flex_grow()
1845 .drag_over::<DraggedTab>(|bar, _, cx| {
1846 bar.bg(cx.theme().colors().drop_target_background)
1847 })
1848 .drag_over::<DraggedSelection>(|bar, _, cx| {
1849 bar.bg(cx.theme().colors().drop_target_background)
1850 })
1851 .on_drop(cx.listener(move |this, dragged_tab: &DraggedTab, cx| {
1852 this.drag_split_direction = None;
1853 this.handle_tab_drop(dragged_tab, this.items.len(), cx)
1854 }))
1855 .on_drop(cx.listener(move |this, selection: &DraggedSelection, cx| {
1856 this.drag_split_direction = None;
1857 this.handle_project_entry_drop(&selection.active_selection.entry_id, cx)
1858 }))
1859 .on_drop(cx.listener(move |this, paths, cx| {
1860 this.drag_split_direction = None;
1861 this.handle_external_paths_drop(paths, cx)
1862 }))
1863 .on_click(cx.listener(move |this, event: &ClickEvent, cx| {
1864 if event.up.click_count == 2 {
1865 cx.dispatch_action(this.double_click_dispatch_action.boxed_clone())
1866 }
1867 })),
1868 )
1869 }
1870
1871 pub fn render_menu_overlay(menu: &View<ContextMenu>) -> Div {
1872 div().absolute().bottom_0().right_0().size_0().child(
1873 deferred(
1874 anchored()
1875 .anchor(AnchorCorner::TopRight)
1876 .child(menu.clone()),
1877 )
1878 .with_priority(1),
1879 )
1880 }
1881
1882 pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
1883 self.zoomed = zoomed;
1884 cx.notify();
1885 }
1886
1887 pub fn is_zoomed(&self) -> bool {
1888 self.zoomed
1889 }
1890
1891 fn handle_drag_move<T>(&mut self, event: &DragMoveEvent<T>, cx: &mut ViewContext<Self>) {
1892 if !self.can_split {
1893 return;
1894 }
1895
1896 let rect = event.bounds.size;
1897
1898 let size = event.bounds.size.width.min(event.bounds.size.height)
1899 * WorkspaceSettings::get_global(cx).drop_target_size;
1900
1901 let relative_cursor = Point::new(
1902 event.event.position.x - event.bounds.left(),
1903 event.event.position.y - event.bounds.top(),
1904 );
1905
1906 let direction = if relative_cursor.x < size
1907 || relative_cursor.x > rect.width - size
1908 || relative_cursor.y < size
1909 || relative_cursor.y > rect.height - size
1910 {
1911 [
1912 SplitDirection::Up,
1913 SplitDirection::Right,
1914 SplitDirection::Down,
1915 SplitDirection::Left,
1916 ]
1917 .iter()
1918 .min_by_key(|side| match side {
1919 SplitDirection::Up => relative_cursor.y,
1920 SplitDirection::Right => rect.width - relative_cursor.x,
1921 SplitDirection::Down => rect.height - relative_cursor.y,
1922 SplitDirection::Left => relative_cursor.x,
1923 })
1924 .cloned()
1925 } else {
1926 None
1927 };
1928
1929 if direction != self.drag_split_direction {
1930 self.drag_split_direction = direction;
1931 }
1932 }
1933
1934 fn handle_tab_drop(
1935 &mut self,
1936 dragged_tab: &DraggedTab,
1937 ix: usize,
1938 cx: &mut ViewContext<'_, Self>,
1939 ) {
1940 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
1941 if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_tab, cx) {
1942 return;
1943 }
1944 }
1945 let mut to_pane = cx.view().clone();
1946 let split_direction = self.drag_split_direction;
1947 let item_id = dragged_tab.item.item_id();
1948 if let Some(preview_item_id) = self.preview_item_id {
1949 if item_id == preview_item_id {
1950 self.set_preview_item_id(None, cx);
1951 }
1952 }
1953
1954 let from_pane = dragged_tab.pane.clone();
1955 self.workspace
1956 .update(cx, |_, cx| {
1957 cx.defer(move |workspace, cx| {
1958 if let Some(split_direction) = split_direction {
1959 to_pane = workspace.split_pane(to_pane, split_direction, cx);
1960 }
1961 workspace.move_item(from_pane, to_pane, item_id, ix, cx);
1962 });
1963 })
1964 .log_err();
1965 }
1966
1967 fn handle_project_entry_drop(
1968 &mut self,
1969 project_entry_id: &ProjectEntryId,
1970 cx: &mut ViewContext<'_, Self>,
1971 ) {
1972 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
1973 if let ControlFlow::Break(()) = custom_drop_handle(self, project_entry_id, cx) {
1974 return;
1975 }
1976 }
1977 let mut to_pane = cx.view().clone();
1978 let split_direction = self.drag_split_direction;
1979 let project_entry_id = *project_entry_id;
1980 self.workspace
1981 .update(cx, |_, cx| {
1982 cx.defer(move |workspace, cx| {
1983 if let Some(path) = workspace
1984 .project()
1985 .read(cx)
1986 .path_for_entry(project_entry_id, cx)
1987 {
1988 if let Some(split_direction) = split_direction {
1989 to_pane = workspace.split_pane(to_pane, split_direction, cx);
1990 }
1991 workspace
1992 .open_path(path, Some(to_pane.downgrade()), true, cx)
1993 .detach_and_log_err(cx);
1994 }
1995 });
1996 })
1997 .log_err();
1998 }
1999
2000 fn handle_external_paths_drop(
2001 &mut self,
2002 paths: &ExternalPaths,
2003 cx: &mut ViewContext<'_, Self>,
2004 ) {
2005 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2006 if let ControlFlow::Break(()) = custom_drop_handle(self, paths, cx) {
2007 return;
2008 }
2009 }
2010 let mut to_pane = cx.view().clone();
2011 let mut split_direction = self.drag_split_direction;
2012 let paths = paths.paths().to_vec();
2013 let is_remote = self
2014 .workspace
2015 .update(cx, |workspace, cx| {
2016 if workspace.project().read(cx).is_remote() {
2017 workspace.show_error(
2018 &anyhow::anyhow!("Cannot drop files on a remote project"),
2019 cx,
2020 );
2021 true
2022 } else {
2023 false
2024 }
2025 })
2026 .unwrap_or(true);
2027 if is_remote {
2028 return;
2029 }
2030
2031 self.workspace
2032 .update(cx, |workspace, cx| {
2033 let fs = Arc::clone(workspace.project().read(cx).fs());
2034 cx.spawn(|workspace, mut cx| async move {
2035 let mut is_file_checks = FuturesUnordered::new();
2036 for path in &paths {
2037 is_file_checks.push(fs.is_file(path))
2038 }
2039 let mut has_files_to_open = false;
2040 while let Some(is_file) = is_file_checks.next().await {
2041 if is_file {
2042 has_files_to_open = true;
2043 break;
2044 }
2045 }
2046 drop(is_file_checks);
2047 if !has_files_to_open {
2048 split_direction = None;
2049 }
2050
2051 if let Some(open_task) = workspace
2052 .update(&mut cx, |workspace, cx| {
2053 if let Some(split_direction) = split_direction {
2054 to_pane = workspace.split_pane(to_pane, split_direction, cx);
2055 }
2056 workspace.open_paths(
2057 paths,
2058 OpenVisible::OnlyDirectories,
2059 Some(to_pane.downgrade()),
2060 cx,
2061 )
2062 })
2063 .ok()
2064 {
2065 let _opened_items: Vec<_> = open_task.await;
2066 }
2067 })
2068 .detach();
2069 })
2070 .log_err();
2071 }
2072
2073 pub fn display_nav_history_buttons(&mut self, display: Option<bool>) {
2074 self.display_nav_history_buttons = display;
2075 }
2076}
2077
2078impl FocusableView for Pane {
2079 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
2080 self.focus_handle.clone()
2081 }
2082}
2083
2084impl Render for Pane {
2085 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2086 let mut key_context = KeyContext::new_with_defaults();
2087 key_context.add("Pane");
2088 if self.active_item().is_none() {
2089 key_context.add("EmptyPane");
2090 }
2091
2092 let should_display_tab_bar = self.should_display_tab_bar.clone();
2093 let display_tab_bar = should_display_tab_bar(cx);
2094
2095 v_flex()
2096 .key_context(key_context)
2097 .track_focus(&self.focus_handle)
2098 .size_full()
2099 .flex_none()
2100 .overflow_hidden()
2101 .on_action(cx.listener(|pane, _: &AlternateFile, cx| {
2102 pane.alternate_file(cx);
2103 }))
2104 .on_action(cx.listener(|pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx)))
2105 .on_action(cx.listener(|pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx)))
2106 .on_action(
2107 cx.listener(|pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx)),
2108 )
2109 .on_action(cx.listener(|pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx)))
2110 .on_action(cx.listener(|pane, _: &GoBack, cx| pane.navigate_backward(cx)))
2111 .on_action(cx.listener(|pane, _: &GoForward, cx| pane.navigate_forward(cx)))
2112 .on_action(cx.listener(Pane::toggle_zoom))
2113 .on_action(cx.listener(|pane: &mut Pane, action: &ActivateItem, cx| {
2114 pane.activate_item(action.0, true, true, cx);
2115 }))
2116 .on_action(cx.listener(|pane: &mut Pane, _: &ActivateLastItem, cx| {
2117 pane.activate_item(pane.items.len() - 1, true, true, cx);
2118 }))
2119 .on_action(cx.listener(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
2120 pane.activate_prev_item(true, cx);
2121 }))
2122 .on_action(cx.listener(|pane: &mut Pane, _: &ActivateNextItem, cx| {
2123 pane.activate_next_item(true, cx);
2124 }))
2125 .when(PreviewTabsSettings::get_global(cx).enabled, |this| {
2126 this.on_action(cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, cx| {
2127 if let Some(active_item_id) = pane.active_item().map(|i| i.item_id()) {
2128 if pane.is_active_preview_item(active_item_id) {
2129 pane.set_preview_item_id(None, cx);
2130 } else {
2131 pane.set_preview_item_id(Some(active_item_id), cx);
2132 }
2133 }
2134 }))
2135 })
2136 .on_action(
2137 cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
2138 if let Some(task) = pane.close_active_item(action, cx) {
2139 task.detach_and_log_err(cx)
2140 }
2141 }),
2142 )
2143 .on_action(
2144 cx.listener(|pane: &mut Self, action: &CloseInactiveItems, cx| {
2145 if let Some(task) = pane.close_inactive_items(action, cx) {
2146 task.detach_and_log_err(cx)
2147 }
2148 }),
2149 )
2150 .on_action(
2151 cx.listener(|pane: &mut Self, action: &CloseCleanItems, cx| {
2152 if let Some(task) = pane.close_clean_items(action, cx) {
2153 task.detach_and_log_err(cx)
2154 }
2155 }),
2156 )
2157 .on_action(
2158 cx.listener(|pane: &mut Self, action: &CloseItemsToTheLeft, cx| {
2159 if let Some(task) = pane.close_items_to_the_left(action, cx) {
2160 task.detach_and_log_err(cx)
2161 }
2162 }),
2163 )
2164 .on_action(
2165 cx.listener(|pane: &mut Self, action: &CloseItemsToTheRight, cx| {
2166 if let Some(task) = pane.close_items_to_the_right(action, cx) {
2167 task.detach_and_log_err(cx)
2168 }
2169 }),
2170 )
2171 .on_action(cx.listener(|pane: &mut Self, action: &CloseAllItems, cx| {
2172 if let Some(task) = pane.close_all_items(action, cx) {
2173 task.detach_and_log_err(cx)
2174 }
2175 }))
2176 .on_action(
2177 cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
2178 if let Some(task) = pane.close_active_item(action, cx) {
2179 task.detach_and_log_err(cx)
2180 }
2181 }),
2182 )
2183 .on_action(
2184 cx.listener(|pane: &mut Self, action: &RevealInProjectPanel, cx| {
2185 let entry_id = action
2186 .entry_id
2187 .map(ProjectEntryId::from_proto)
2188 .or_else(|| pane.active_item()?.project_entry_ids(cx).first().copied());
2189 if let Some(entry_id) = entry_id {
2190 pane.project.update(cx, |_, cx| {
2191 cx.emit(project::Event::RevealInProjectPanel(entry_id))
2192 });
2193 }
2194 }),
2195 )
2196 .when(self.active_item().is_some() && display_tab_bar, |pane| {
2197 pane.child(self.render_tab_bar(cx))
2198 })
2199 .child({
2200 let has_worktrees = self.project.read(cx).worktrees().next().is_some();
2201 // main content
2202 div()
2203 .flex_1()
2204 .relative()
2205 .group("")
2206 .on_drag_move::<DraggedTab>(cx.listener(Self::handle_drag_move))
2207 .on_drag_move::<DraggedSelection>(cx.listener(Self::handle_drag_move))
2208 .on_drag_move::<ExternalPaths>(cx.listener(Self::handle_drag_move))
2209 .map(|div| {
2210 if let Some(item) = self.active_item() {
2211 div.v_flex()
2212 .child(self.toolbar.clone())
2213 .child(item.to_any())
2214 } else {
2215 let placeholder = div.h_flex().size_full().justify_center();
2216 if has_worktrees {
2217 placeholder
2218 } else {
2219 placeholder.child(
2220 Label::new("Open a file or project to get started.")
2221 .color(Color::Muted),
2222 )
2223 }
2224 }
2225 })
2226 .child(
2227 // drag target
2228 div()
2229 .invisible()
2230 .absolute()
2231 .bg(cx.theme().colors().drop_target_background)
2232 .group_drag_over::<DraggedTab>("", |style| style.visible())
2233 .group_drag_over::<DraggedSelection>("", |style| style.visible())
2234 .group_drag_over::<ExternalPaths>("", |style| style.visible())
2235 .when_some(self.can_drop_predicate.clone(), |this, p| {
2236 this.can_drop(move |a, cx| p(a, cx))
2237 })
2238 .on_drop(cx.listener(move |this, dragged_tab, cx| {
2239 this.handle_tab_drop(dragged_tab, this.active_item_index(), cx)
2240 }))
2241 .on_drop(cx.listener(move |this, selection: &DraggedSelection, cx| {
2242 this.handle_project_entry_drop(
2243 &selection.active_selection.entry_id,
2244 cx,
2245 )
2246 }))
2247 .on_drop(cx.listener(move |this, paths, cx| {
2248 this.handle_external_paths_drop(paths, cx)
2249 }))
2250 .map(|div| {
2251 let size = DefiniteLength::Fraction(0.5);
2252 match self.drag_split_direction {
2253 None => div.top_0().right_0().bottom_0().left_0(),
2254 Some(SplitDirection::Up) => {
2255 div.top_0().left_0().right_0().h(size)
2256 }
2257 Some(SplitDirection::Down) => {
2258 div.left_0().bottom_0().right_0().h(size)
2259 }
2260 Some(SplitDirection::Left) => {
2261 div.top_0().left_0().bottom_0().w(size)
2262 }
2263 Some(SplitDirection::Right) => {
2264 div.top_0().bottom_0().right_0().w(size)
2265 }
2266 }
2267 }),
2268 )
2269 })
2270 .on_mouse_down(
2271 MouseButton::Navigate(NavigationDirection::Back),
2272 cx.listener(|pane, _, cx| {
2273 if let Some(workspace) = pane.workspace.upgrade() {
2274 let pane = cx.view().downgrade();
2275 cx.window_context().defer(move |cx| {
2276 workspace.update(cx, |workspace, cx| {
2277 workspace.go_back(pane, cx).detach_and_log_err(cx)
2278 })
2279 })
2280 }
2281 }),
2282 )
2283 .on_mouse_down(
2284 MouseButton::Navigate(NavigationDirection::Forward),
2285 cx.listener(|pane, _, cx| {
2286 if let Some(workspace) = pane.workspace.upgrade() {
2287 let pane = cx.view().downgrade();
2288 cx.window_context().defer(move |cx| {
2289 workspace.update(cx, |workspace, cx| {
2290 workspace.go_forward(pane, cx).detach_and_log_err(cx)
2291 })
2292 })
2293 }
2294 }),
2295 )
2296 }
2297}
2298
2299impl ItemNavHistory {
2300 pub fn push<D: 'static + Send + Any>(&mut self, data: Option<D>, cx: &mut WindowContext) {
2301 self.history
2302 .push(data, self.item.clone(), self.is_preview, cx);
2303 }
2304
2305 pub fn pop_backward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
2306 self.history.pop(NavigationMode::GoingBack, cx)
2307 }
2308
2309 pub fn pop_forward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
2310 self.history.pop(NavigationMode::GoingForward, cx)
2311 }
2312}
2313
2314impl NavHistory {
2315 pub fn for_each_entry(
2316 &self,
2317 cx: &AppContext,
2318 mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
2319 ) {
2320 let borrowed_history = self.0.lock();
2321 borrowed_history
2322 .forward_stack
2323 .iter()
2324 .chain(borrowed_history.backward_stack.iter())
2325 .chain(borrowed_history.closed_stack.iter())
2326 .for_each(|entry| {
2327 if let Some(project_and_abs_path) =
2328 borrowed_history.paths_by_item.get(&entry.item.id())
2329 {
2330 f(entry, project_and_abs_path.clone());
2331 } else if let Some(item) = entry.item.upgrade() {
2332 if let Some(path) = item.project_path(cx) {
2333 f(entry, (path, None));
2334 }
2335 }
2336 })
2337 }
2338
2339 pub fn set_mode(&mut self, mode: NavigationMode) {
2340 self.0.lock().mode = mode;
2341 }
2342
2343 pub fn mode(&self) -> NavigationMode {
2344 self.0.lock().mode
2345 }
2346
2347 pub fn disable(&mut self) {
2348 self.0.lock().mode = NavigationMode::Disabled;
2349 }
2350
2351 pub fn enable(&mut self) {
2352 self.0.lock().mode = NavigationMode::Normal;
2353 }
2354
2355 pub fn pop(&mut self, mode: NavigationMode, cx: &mut WindowContext) -> Option<NavigationEntry> {
2356 let mut state = self.0.lock();
2357 let entry = match mode {
2358 NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
2359 return None
2360 }
2361 NavigationMode::GoingBack => &mut state.backward_stack,
2362 NavigationMode::GoingForward => &mut state.forward_stack,
2363 NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
2364 }
2365 .pop_back();
2366 if entry.is_some() {
2367 state.did_update(cx);
2368 }
2369 entry
2370 }
2371
2372 pub fn push<D: 'static + Send + Any>(
2373 &mut self,
2374 data: Option<D>,
2375 item: Arc<dyn WeakItemHandle>,
2376 is_preview: bool,
2377 cx: &mut WindowContext,
2378 ) {
2379 let state = &mut *self.0.lock();
2380 match state.mode {
2381 NavigationMode::Disabled => {}
2382 NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
2383 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2384 state.backward_stack.pop_front();
2385 }
2386 state.backward_stack.push_back(NavigationEntry {
2387 item,
2388 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2389 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2390 is_preview,
2391 });
2392 state.forward_stack.clear();
2393 }
2394 NavigationMode::GoingBack => {
2395 if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2396 state.forward_stack.pop_front();
2397 }
2398 state.forward_stack.push_back(NavigationEntry {
2399 item,
2400 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2401 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2402 is_preview,
2403 });
2404 }
2405 NavigationMode::GoingForward => {
2406 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2407 state.backward_stack.pop_front();
2408 }
2409 state.backward_stack.push_back(NavigationEntry {
2410 item,
2411 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2412 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2413 is_preview,
2414 });
2415 }
2416 NavigationMode::ClosingItem => {
2417 if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2418 state.closed_stack.pop_front();
2419 }
2420 state.closed_stack.push_back(NavigationEntry {
2421 item,
2422 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2423 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2424 is_preview,
2425 });
2426 }
2427 }
2428 state.did_update(cx);
2429 }
2430
2431 pub fn remove_item(&mut self, item_id: EntityId) {
2432 let mut state = self.0.lock();
2433 state.paths_by_item.remove(&item_id);
2434 state
2435 .backward_stack
2436 .retain(|entry| entry.item.id() != item_id);
2437 state
2438 .forward_stack
2439 .retain(|entry| entry.item.id() != item_id);
2440 state
2441 .closed_stack
2442 .retain(|entry| entry.item.id() != item_id);
2443 }
2444
2445 pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option<PathBuf>)> {
2446 self.0.lock().paths_by_item.get(&item_id).cloned()
2447 }
2448}
2449
2450impl NavHistoryState {
2451 pub fn did_update(&self, cx: &mut WindowContext) {
2452 if let Some(pane) = self.pane.upgrade() {
2453 cx.defer(move |cx| {
2454 pane.update(cx, |pane, cx| pane.history_updated(cx));
2455 });
2456 }
2457 }
2458}
2459
2460fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
2461 let path = buffer_path
2462 .as_ref()
2463 .and_then(|p| {
2464 p.path
2465 .to_str()
2466 .and_then(|s| if s == "" { None } else { Some(s) })
2467 })
2468 .unwrap_or("This buffer");
2469 let path = truncate_and_remove_front(path, 80);
2470 format!("{path} contains unsaved edits. Do you want to save it?")
2471}
2472
2473pub fn tab_details(items: &Vec<Box<dyn ItemHandle>>, cx: &AppContext) -> Vec<usize> {
2474 let mut tab_details = items.iter().map(|_| 0).collect::<Vec<_>>();
2475 let mut tab_descriptions = HashMap::default();
2476 let mut done = false;
2477 while !done {
2478 done = true;
2479
2480 // Store item indices by their tab description.
2481 for (ix, (item, detail)) in items.iter().zip(&tab_details).enumerate() {
2482 if let Some(description) = item.tab_description(*detail, cx) {
2483 if *detail == 0
2484 || Some(&description) != item.tab_description(detail - 1, cx).as_ref()
2485 {
2486 tab_descriptions
2487 .entry(description)
2488 .or_insert(Vec::new())
2489 .push(ix);
2490 }
2491 }
2492 }
2493
2494 // If two or more items have the same tab description, increase their level
2495 // of detail and try again.
2496 for (_, item_ixs) in tab_descriptions.drain() {
2497 if item_ixs.len() > 1 {
2498 done = false;
2499 for ix in item_ixs {
2500 tab_details[ix] += 1;
2501 }
2502 }
2503 }
2504 }
2505
2506 tab_details
2507}
2508
2509pub fn render_item_indicator(item: Box<dyn ItemHandle>, cx: &WindowContext) -> Option<Indicator> {
2510 maybe!({
2511 let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
2512 (true, _) => Color::Warning,
2513 (_, true) => Color::Accent,
2514 (false, false) => return None,
2515 };
2516
2517 Some(Indicator::dot().color(indicator_color))
2518 })
2519}
2520
2521#[cfg(test)]
2522mod tests {
2523 use super::*;
2524 use crate::item::test::{TestItem, TestProjectItem};
2525 use gpui::{TestAppContext, VisualTestContext};
2526 use project::FakeFs;
2527 use settings::SettingsStore;
2528 use theme::LoadThemes;
2529
2530 #[gpui::test]
2531 async fn test_remove_active_empty(cx: &mut TestAppContext) {
2532 init_test(cx);
2533 let fs = FakeFs::new(cx.executor());
2534
2535 let project = Project::test(fs, None, cx).await;
2536 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2537 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2538
2539 pane.update(cx, |pane, cx| {
2540 assert!(pane
2541 .close_active_item(&CloseActiveItem { save_intent: None }, cx)
2542 .is_none())
2543 });
2544 }
2545
2546 #[gpui::test]
2547 async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
2548 init_test(cx);
2549 let fs = FakeFs::new(cx.executor());
2550
2551 let project = Project::test(fs, None, cx).await;
2552 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2553 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2554
2555 // 1. Add with a destination index
2556 // a. Add before the active item
2557 set_labeled_items(&pane, ["A", "B*", "C"], cx);
2558 pane.update(cx, |pane, cx| {
2559 pane.add_item(
2560 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2561 false,
2562 false,
2563 Some(0),
2564 cx,
2565 );
2566 });
2567 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
2568
2569 // b. Add after the active item
2570 set_labeled_items(&pane, ["A", "B*", "C"], cx);
2571 pane.update(cx, |pane, cx| {
2572 pane.add_item(
2573 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2574 false,
2575 false,
2576 Some(2),
2577 cx,
2578 );
2579 });
2580 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
2581
2582 // c. Add at the end of the item list (including off the length)
2583 set_labeled_items(&pane, ["A", "B*", "C"], cx);
2584 pane.update(cx, |pane, cx| {
2585 pane.add_item(
2586 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2587 false,
2588 false,
2589 Some(5),
2590 cx,
2591 );
2592 });
2593 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2594
2595 // 2. Add without a destination index
2596 // a. Add with active item at the start of the item list
2597 set_labeled_items(&pane, ["A*", "B", "C"], cx);
2598 pane.update(cx, |pane, cx| {
2599 pane.add_item(
2600 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2601 false,
2602 false,
2603 None,
2604 cx,
2605 );
2606 });
2607 set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
2608
2609 // b. Add with active item at the end of the item list
2610 set_labeled_items(&pane, ["A", "B", "C*"], cx);
2611 pane.update(cx, |pane, cx| {
2612 pane.add_item(
2613 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2614 false,
2615 false,
2616 None,
2617 cx,
2618 );
2619 });
2620 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2621 }
2622
2623 #[gpui::test]
2624 async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
2625 init_test(cx);
2626 let fs = FakeFs::new(cx.executor());
2627
2628 let project = Project::test(fs, None, cx).await;
2629 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2630 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2631
2632 // 1. Add with a destination index
2633 // 1a. Add before the active item
2634 let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
2635 pane.update(cx, |pane, cx| {
2636 pane.add_item(d, false, false, Some(0), cx);
2637 });
2638 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
2639
2640 // 1b. Add after the active item
2641 let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
2642 pane.update(cx, |pane, cx| {
2643 pane.add_item(d, false, false, Some(2), cx);
2644 });
2645 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
2646
2647 // 1c. Add at the end of the item list (including off the length)
2648 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
2649 pane.update(cx, |pane, cx| {
2650 pane.add_item(a, false, false, Some(5), cx);
2651 });
2652 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
2653
2654 // 1d. Add same item to active index
2655 let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
2656 pane.update(cx, |pane, cx| {
2657 pane.add_item(b, false, false, Some(1), cx);
2658 });
2659 assert_item_labels(&pane, ["A", "B*", "C"], cx);
2660
2661 // 1e. Add item to index after same item in last position
2662 let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
2663 pane.update(cx, |pane, cx| {
2664 pane.add_item(c, false, false, Some(2), cx);
2665 });
2666 assert_item_labels(&pane, ["A", "B", "C*"], cx);
2667
2668 // 2. Add without a destination index
2669 // 2a. Add with active item at the start of the item list
2670 let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
2671 pane.update(cx, |pane, cx| {
2672 pane.add_item(d, false, false, None, cx);
2673 });
2674 assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
2675
2676 // 2b. Add with active item at the end of the item list
2677 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
2678 pane.update(cx, |pane, cx| {
2679 pane.add_item(a, false, false, None, cx);
2680 });
2681 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
2682
2683 // 2c. Add active item to active item at end of list
2684 let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
2685 pane.update(cx, |pane, cx| {
2686 pane.add_item(c, false, false, None, cx);
2687 });
2688 assert_item_labels(&pane, ["A", "B", "C*"], cx);
2689
2690 // 2d. Add active item to active item at start of list
2691 let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
2692 pane.update(cx, |pane, cx| {
2693 pane.add_item(a, false, false, None, cx);
2694 });
2695 assert_item_labels(&pane, ["A*", "B", "C"], cx);
2696 }
2697
2698 #[gpui::test]
2699 async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
2700 init_test(cx);
2701 let fs = FakeFs::new(cx.executor());
2702
2703 let project = Project::test(fs, None, cx).await;
2704 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2705 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2706
2707 // singleton view
2708 pane.update(cx, |pane, cx| {
2709 pane.add_item(
2710 Box::new(cx.new_view(|cx| {
2711 TestItem::new(cx)
2712 .with_singleton(true)
2713 .with_label("buffer 1")
2714 .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
2715 })),
2716 false,
2717 false,
2718 None,
2719 cx,
2720 );
2721 });
2722 assert_item_labels(&pane, ["buffer 1*"], cx);
2723
2724 // new singleton view with the same project entry
2725 pane.update(cx, |pane, cx| {
2726 pane.add_item(
2727 Box::new(cx.new_view(|cx| {
2728 TestItem::new(cx)
2729 .with_singleton(true)
2730 .with_label("buffer 1")
2731 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
2732 })),
2733 false,
2734 false,
2735 None,
2736 cx,
2737 );
2738 });
2739 assert_item_labels(&pane, ["buffer 1*"], cx);
2740
2741 // new singleton view with different project entry
2742 pane.update(cx, |pane, cx| {
2743 pane.add_item(
2744 Box::new(cx.new_view(|cx| {
2745 TestItem::new(cx)
2746 .with_singleton(true)
2747 .with_label("buffer 2")
2748 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
2749 })),
2750 false,
2751 false,
2752 None,
2753 cx,
2754 );
2755 });
2756 assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
2757
2758 // new multibuffer view with the same project entry
2759 pane.update(cx, |pane, cx| {
2760 pane.add_item(
2761 Box::new(cx.new_view(|cx| {
2762 TestItem::new(cx)
2763 .with_singleton(false)
2764 .with_label("multibuffer 1")
2765 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
2766 })),
2767 false,
2768 false,
2769 None,
2770 cx,
2771 );
2772 });
2773 assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
2774
2775 // another multibuffer view with the same project entry
2776 pane.update(cx, |pane, cx| {
2777 pane.add_item(
2778 Box::new(cx.new_view(|cx| {
2779 TestItem::new(cx)
2780 .with_singleton(false)
2781 .with_label("multibuffer 1b")
2782 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
2783 })),
2784 false,
2785 false,
2786 None,
2787 cx,
2788 );
2789 });
2790 assert_item_labels(
2791 &pane,
2792 ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
2793 cx,
2794 );
2795 }
2796
2797 #[gpui::test]
2798 async fn test_remove_item_ordering(cx: &mut TestAppContext) {
2799 init_test(cx);
2800 let fs = FakeFs::new(cx.executor());
2801
2802 let project = Project::test(fs, None, cx).await;
2803 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2804 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2805
2806 add_labeled_item(&pane, "A", false, cx);
2807 add_labeled_item(&pane, "B", false, cx);
2808 add_labeled_item(&pane, "C", false, cx);
2809 add_labeled_item(&pane, "D", false, cx);
2810 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2811
2812 pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
2813 add_labeled_item(&pane, "1", false, cx);
2814 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
2815
2816 pane.update(cx, |pane, cx| {
2817 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
2818 })
2819 .unwrap()
2820 .await
2821 .unwrap();
2822 assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
2823
2824 pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
2825 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2826
2827 pane.update(cx, |pane, cx| {
2828 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
2829 })
2830 .unwrap()
2831 .await
2832 .unwrap();
2833 assert_item_labels(&pane, ["A", "B*", "C"], cx);
2834
2835 pane.update(cx, |pane, cx| {
2836 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
2837 })
2838 .unwrap()
2839 .await
2840 .unwrap();
2841 assert_item_labels(&pane, ["A", "C*"], cx);
2842
2843 pane.update(cx, |pane, cx| {
2844 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
2845 })
2846 .unwrap()
2847 .await
2848 .unwrap();
2849 assert_item_labels(&pane, ["A*"], cx);
2850 }
2851
2852 #[gpui::test]
2853 async fn test_close_inactive_items(cx: &mut TestAppContext) {
2854 init_test(cx);
2855 let fs = FakeFs::new(cx.executor());
2856
2857 let project = Project::test(fs, None, cx).await;
2858 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2859 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2860
2861 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
2862
2863 pane.update(cx, |pane, cx| {
2864 pane.close_inactive_items(&CloseInactiveItems { save_intent: None }, cx)
2865 })
2866 .unwrap()
2867 .await
2868 .unwrap();
2869 assert_item_labels(&pane, ["C*"], cx);
2870 }
2871
2872 #[gpui::test]
2873 async fn test_close_clean_items(cx: &mut TestAppContext) {
2874 init_test(cx);
2875 let fs = FakeFs::new(cx.executor());
2876
2877 let project = Project::test(fs, None, cx).await;
2878 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2879 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2880
2881 add_labeled_item(&pane, "A", true, cx);
2882 add_labeled_item(&pane, "B", false, cx);
2883 add_labeled_item(&pane, "C", true, cx);
2884 add_labeled_item(&pane, "D", false, cx);
2885 add_labeled_item(&pane, "E", false, cx);
2886 assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
2887
2888 pane.update(cx, |pane, cx| pane.close_clean_items(&CloseCleanItems, cx))
2889 .unwrap()
2890 .await
2891 .unwrap();
2892 assert_item_labels(&pane, ["A^", "C*^"], cx);
2893 }
2894
2895 #[gpui::test]
2896 async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
2897 init_test(cx);
2898 let fs = FakeFs::new(cx.executor());
2899
2900 let project = Project::test(fs, None, cx).await;
2901 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2902 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2903
2904 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
2905
2906 pane.update(cx, |pane, cx| {
2907 pane.close_items_to_the_left(&CloseItemsToTheLeft, cx)
2908 })
2909 .unwrap()
2910 .await
2911 .unwrap();
2912 assert_item_labels(&pane, ["C*", "D", "E"], cx);
2913 }
2914
2915 #[gpui::test]
2916 async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
2917 init_test(cx);
2918 let fs = FakeFs::new(cx.executor());
2919
2920 let project = Project::test(fs, None, cx).await;
2921 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2922 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2923
2924 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
2925
2926 pane.update(cx, |pane, cx| {
2927 pane.close_items_to_the_right(&CloseItemsToTheRight, cx)
2928 })
2929 .unwrap()
2930 .await
2931 .unwrap();
2932 assert_item_labels(&pane, ["A", "B", "C*"], cx);
2933 }
2934
2935 #[gpui::test]
2936 async fn test_close_all_items(cx: &mut TestAppContext) {
2937 init_test(cx);
2938 let fs = FakeFs::new(cx.executor());
2939
2940 let project = Project::test(fs, None, cx).await;
2941 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2942 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2943
2944 add_labeled_item(&pane, "A", false, cx);
2945 add_labeled_item(&pane, "B", false, cx);
2946 add_labeled_item(&pane, "C", false, cx);
2947 assert_item_labels(&pane, ["A", "B", "C*"], cx);
2948
2949 pane.update(cx, |pane, cx| {
2950 pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
2951 })
2952 .unwrap()
2953 .await
2954 .unwrap();
2955 assert_item_labels(&pane, [], cx);
2956
2957 add_labeled_item(&pane, "A", true, cx);
2958 add_labeled_item(&pane, "B", true, cx);
2959 add_labeled_item(&pane, "C", true, cx);
2960 assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
2961
2962 let save = pane
2963 .update(cx, |pane, cx| {
2964 pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
2965 })
2966 .unwrap();
2967
2968 cx.executor().run_until_parked();
2969 cx.simulate_prompt_answer(2);
2970 save.await.unwrap();
2971 assert_item_labels(&pane, [], cx);
2972 }
2973
2974 fn init_test(cx: &mut TestAppContext) {
2975 cx.update(|cx| {
2976 let settings_store = SettingsStore::test(cx);
2977 cx.set_global(settings_store);
2978 theme::init(LoadThemes::JustBase, cx);
2979 crate::init_settings(cx);
2980 Project::init_settings(cx);
2981 });
2982 }
2983
2984 fn add_labeled_item(
2985 pane: &View<Pane>,
2986 label: &str,
2987 is_dirty: bool,
2988 cx: &mut VisualTestContext,
2989 ) -> Box<View<TestItem>> {
2990 pane.update(cx, |pane, cx| {
2991 let labeled_item = Box::new(
2992 cx.new_view(|cx| TestItem::new(cx).with_label(label).with_dirty(is_dirty)),
2993 );
2994 pane.add_item(labeled_item.clone(), false, false, None, cx);
2995 labeled_item
2996 })
2997 }
2998
2999 fn set_labeled_items<const COUNT: usize>(
3000 pane: &View<Pane>,
3001 labels: [&str; COUNT],
3002 cx: &mut VisualTestContext,
3003 ) -> [Box<View<TestItem>>; COUNT] {
3004 pane.update(cx, |pane, cx| {
3005 pane.items.clear();
3006 let mut active_item_index = 0;
3007
3008 let mut index = 0;
3009 let items = labels.map(|mut label| {
3010 if label.ends_with('*') {
3011 label = label.trim_end_matches('*');
3012 active_item_index = index;
3013 }
3014
3015 let labeled_item = Box::new(cx.new_view(|cx| TestItem::new(cx).with_label(label)));
3016 pane.add_item(labeled_item.clone(), false, false, None, cx);
3017 index += 1;
3018 labeled_item
3019 });
3020
3021 pane.activate_item(active_item_index, false, false, cx);
3022
3023 items
3024 })
3025 }
3026
3027 // Assert the item label, with the active item label suffixed with a '*'
3028 fn assert_item_labels<const COUNT: usize>(
3029 pane: &View<Pane>,
3030 expected_states: [&str; COUNT],
3031 cx: &mut VisualTestContext,
3032 ) {
3033 pane.update(cx, |pane, cx| {
3034 let actual_states = pane
3035 .items
3036 .iter()
3037 .enumerate()
3038 .map(|(ix, item)| {
3039 let mut state = item
3040 .to_any()
3041 .downcast::<TestItem>()
3042 .unwrap()
3043 .read(cx)
3044 .label
3045 .clone();
3046 if ix == pane.active_item_index {
3047 state.push('*');
3048 }
3049 if item.is_dirty(cx) {
3050 state.push('^');
3051 }
3052 state
3053 })
3054 .collect::<Vec<_>>();
3055
3056 assert_eq!(
3057 actual_states, expected_states,
3058 "pane items do not match expectation"
3059 );
3060 })
3061 }
3062}
3063
3064impl Render for DraggedTab {
3065 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3066 let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
3067 let label = self.item.tab_content(
3068 TabContentParams {
3069 detail: Some(self.detail),
3070 selected: false,
3071 preview: false,
3072 },
3073 cx,
3074 );
3075 Tab::new("")
3076 .selected(self.is_active)
3077 .child(label)
3078 .render(cx)
3079 .font(ui_font)
3080 }
3081}