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