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