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 let format = if let AutosaveSetting::AfterDelay { .. } =
1408 WorkspaceSettings::get_global(cx).autosave
1409 {
1410 false
1411 } else {
1412 true
1413 };
1414 if Self::can_autosave_item(item, cx) {
1415 item.save(format, project, cx)
1416 } else {
1417 Task::ready(Ok(()))
1418 }
1419 }
1420
1421 pub fn focus(&mut self, cx: &mut ViewContext<Pane>) {
1422 cx.focus(&self.focus_handle);
1423 }
1424
1425 pub fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
1426 if let Some(active_item) = self.active_item() {
1427 let focus_handle = active_item.focus_handle(cx);
1428 cx.focus(&focus_handle);
1429 }
1430 }
1431
1432 pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext<Self>) {
1433 cx.emit(Event::Split(direction));
1434 }
1435
1436 pub fn toolbar(&self) -> &View<Toolbar> {
1437 &self.toolbar
1438 }
1439
1440 pub fn handle_deleted_project_item(
1441 &mut self,
1442 entry_id: ProjectEntryId,
1443 cx: &mut ViewContext<Pane>,
1444 ) -> Option<()> {
1445 let (item_index_to_delete, item_id) = self.items().enumerate().find_map(|(i, item)| {
1446 if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
1447 Some((i, item.item_id()))
1448 } else {
1449 None
1450 }
1451 })?;
1452
1453 self.remove_item(item_index_to_delete, false, true, cx);
1454 self.nav_history.remove_item(item_id);
1455
1456 Some(())
1457 }
1458
1459 fn update_toolbar(&mut self, cx: &mut ViewContext<Self>) {
1460 let active_item = self
1461 .items
1462 .get(self.active_item_index)
1463 .map(|item| item.as_ref());
1464 self.toolbar.update(cx, |toolbar, cx| {
1465 toolbar.set_active_item(active_item, cx);
1466 });
1467 }
1468
1469 fn update_status_bar(&mut self, cx: &mut ViewContext<Self>) {
1470 let workspace = self.workspace.clone();
1471 let pane = cx.view().clone();
1472
1473 cx.window_context().defer(move |cx| {
1474 let Ok(status_bar) = workspace.update(cx, |workspace, _| workspace.status_bar.clone())
1475 else {
1476 return;
1477 };
1478
1479 status_bar.update(cx, move |status_bar, cx| {
1480 status_bar.set_active_pane(&pane, cx);
1481 });
1482 });
1483 }
1484
1485 fn render_tab(
1486 &self,
1487 ix: usize,
1488 item: &Box<dyn ItemHandle>,
1489 detail: usize,
1490 cx: &mut ViewContext<'_, Pane>,
1491 ) -> impl IntoElement {
1492 let is_active = ix == self.active_item_index;
1493 let is_preview = self
1494 .preview_item_id
1495 .map(|id| id == item.item_id())
1496 .unwrap_or(false);
1497
1498 let label = item.tab_content(
1499 TabContentParams {
1500 detail: Some(detail),
1501 selected: is_active,
1502 preview: is_preview,
1503 },
1504 cx,
1505 );
1506 let close_side = &ItemSettings::get_global(cx).close_position;
1507 let indicator = render_item_indicator(item.boxed_clone(), cx);
1508 let item_id = item.item_id();
1509 let is_first_item = ix == 0;
1510 let is_last_item = ix == self.items.len() - 1;
1511 let position_relative_to_active_item = ix.cmp(&self.active_item_index);
1512
1513 let tab = Tab::new(ix)
1514 .position(if is_first_item {
1515 TabPosition::First
1516 } else if is_last_item {
1517 TabPosition::Last
1518 } else {
1519 TabPosition::Middle(position_relative_to_active_item)
1520 })
1521 .close_side(match close_side {
1522 ClosePosition::Left => ui::TabCloseSide::Start,
1523 ClosePosition::Right => ui::TabCloseSide::End,
1524 })
1525 .selected(is_active)
1526 .on_click(
1527 cx.listener(move |pane: &mut Self, _, cx| pane.activate_item(ix, true, true, cx)),
1528 )
1529 // TODO: This should be a click listener with the middle mouse button instead of a mouse down listener.
1530 .on_mouse_down(
1531 MouseButton::Middle,
1532 cx.listener(move |pane, _event, cx| {
1533 pane.close_item_by_id(item_id, SaveIntent::Close, cx)
1534 .detach_and_log_err(cx);
1535 }),
1536 )
1537 .on_mouse_down(
1538 MouseButton::Left,
1539 cx.listener(move |pane, event: &MouseDownEvent, cx| {
1540 if let Some(id) = pane.preview_item_id {
1541 if id == item_id && event.click_count > 1 {
1542 pane.set_preview_item_id(None, cx);
1543 }
1544 }
1545 }),
1546 )
1547 .on_drag(
1548 DraggedTab {
1549 item: item.boxed_clone(),
1550 pane: cx.view().clone(),
1551 detail,
1552 is_active,
1553 ix,
1554 },
1555 |tab, cx| cx.new_view(|_| tab.clone()),
1556 )
1557 .drag_over::<DraggedTab>(|tab, _, cx| {
1558 tab.bg(cx.theme().colors().drop_target_background)
1559 })
1560 .drag_over::<ProjectEntryId>(|tab, _, cx| {
1561 tab.bg(cx.theme().colors().drop_target_background)
1562 })
1563 .when_some(self.can_drop_predicate.clone(), |this, p| {
1564 this.can_drop(move |a, cx| p(a, cx))
1565 })
1566 .on_drop(cx.listener(move |this, dragged_tab: &DraggedTab, cx| {
1567 this.drag_split_direction = None;
1568 this.handle_tab_drop(dragged_tab, ix, cx)
1569 }))
1570 .on_drop(cx.listener(move |this, entry_id: &ProjectEntryId, cx| {
1571 this.drag_split_direction = None;
1572 this.handle_project_entry_drop(entry_id, cx)
1573 }))
1574 .on_drop(cx.listener(move |this, paths, cx| {
1575 this.drag_split_direction = None;
1576 this.handle_external_paths_drop(paths, cx)
1577 }))
1578 .when_some(item.tab_tooltip_text(cx), |tab, text| {
1579 tab.tooltip(move |cx| Tooltip::text(text.clone(), cx))
1580 })
1581 .start_slot::<Indicator>(indicator)
1582 .end_slot(
1583 IconButton::new("close tab", IconName::Close)
1584 .shape(IconButtonShape::Square)
1585 .icon_color(Color::Muted)
1586 .size(ButtonSize::None)
1587 .icon_size(IconSize::XSmall)
1588 .on_click(cx.listener(move |pane, _, cx| {
1589 pane.close_item_by_id(item_id, SaveIntent::Close, cx)
1590 .detach_and_log_err(cx);
1591 })),
1592 )
1593 .child(label);
1594
1595 let single_entry_to_resolve = {
1596 let item_entries = self.items[ix].project_entry_ids(cx);
1597 if item_entries.len() == 1 {
1598 Some(item_entries[0])
1599 } else {
1600 None
1601 }
1602 };
1603
1604 let pane = cx.view().downgrade();
1605 right_click_menu(ix).trigger(tab).menu(move |cx| {
1606 let pane = pane.clone();
1607 ContextMenu::build(cx, move |mut menu, cx| {
1608 if let Some(pane) = pane.upgrade() {
1609 menu = menu
1610 .entry(
1611 "Close",
1612 Some(Box::new(CloseActiveItem { save_intent: None })),
1613 cx.handler_for(&pane, move |pane, cx| {
1614 pane.close_item_by_id(item_id, SaveIntent::Close, cx)
1615 .detach_and_log_err(cx);
1616 }),
1617 )
1618 .entry(
1619 "Close Others",
1620 Some(Box::new(CloseInactiveItems { save_intent: None })),
1621 cx.handler_for(&pane, move |pane, cx| {
1622 pane.close_items(cx, SaveIntent::Close, |id| id != item_id)
1623 .detach_and_log_err(cx);
1624 }),
1625 )
1626 .separator()
1627 .entry(
1628 "Close Left",
1629 Some(Box::new(CloseItemsToTheLeft)),
1630 cx.handler_for(&pane, move |pane, cx| {
1631 pane.close_items_to_the_left_by_id(item_id, cx)
1632 .detach_and_log_err(cx);
1633 }),
1634 )
1635 .entry(
1636 "Close Right",
1637 Some(Box::new(CloseItemsToTheRight)),
1638 cx.handler_for(&pane, move |pane, cx| {
1639 pane.close_items_to_the_right_by_id(item_id, cx)
1640 .detach_and_log_err(cx);
1641 }),
1642 )
1643 .separator()
1644 .entry(
1645 "Close Clean",
1646 Some(Box::new(CloseCleanItems)),
1647 cx.handler_for(&pane, move |pane, cx| {
1648 if let Some(task) = pane.close_clean_items(&CloseCleanItems, cx) {
1649 task.detach_and_log_err(cx)
1650 }
1651 }),
1652 )
1653 .entry(
1654 "Close All",
1655 Some(Box::new(CloseAllItems { save_intent: None })),
1656 cx.handler_for(&pane, |pane, cx| {
1657 if let Some(task) =
1658 pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
1659 {
1660 task.detach_and_log_err(cx)
1661 }
1662 }),
1663 );
1664
1665 if let Some(entry) = single_entry_to_resolve {
1666 let parent_abs_path = pane
1667 .update(cx, |pane, cx| {
1668 pane.workspace.update(cx, |workspace, cx| {
1669 let project = workspace.project().read(cx);
1670 project.worktree_for_entry(entry, cx).and_then(|worktree| {
1671 let worktree = worktree.read(cx);
1672 let entry = worktree.entry_for_id(entry)?;
1673 let abs_path = worktree.absolutize(&entry.path).ok()?;
1674 let parent = if entry.is_symlink {
1675 abs_path.canonicalize().ok()?
1676 } else {
1677 abs_path
1678 }
1679 .parent()?
1680 .to_path_buf();
1681 Some(parent)
1682 })
1683 })
1684 })
1685 .ok()
1686 .flatten();
1687
1688 let entry_id = entry.to_proto();
1689 menu = menu
1690 .separator()
1691 .entry(
1692 "Reveal In Project Panel",
1693 Some(Box::new(RevealInProjectPanel {
1694 entry_id: Some(entry_id),
1695 })),
1696 cx.handler_for(&pane, move |pane, cx| {
1697 pane.project.update(cx, |_, cx| {
1698 cx.emit(project::Event::RevealInProjectPanel(
1699 ProjectEntryId::from_proto(entry_id),
1700 ))
1701 });
1702 }),
1703 )
1704 .when_some(parent_abs_path, |menu, abs_path| {
1705 menu.entry(
1706 "Open in Terminal",
1707 Some(Box::new(OpenInTerminal)),
1708 cx.handler_for(&pane, move |_, cx| {
1709 cx.dispatch_action(
1710 OpenTerminal {
1711 working_directory: abs_path.clone(),
1712 }
1713 .boxed_clone(),
1714 );
1715 }),
1716 )
1717 });
1718 }
1719 }
1720
1721 menu
1722 })
1723 })
1724 }
1725
1726 fn render_tab_bar(&mut self, cx: &mut ViewContext<'_, Pane>) -> impl IntoElement {
1727 let navigate_backward = IconButton::new("navigate_backward", IconName::ArrowLeft)
1728 .shape(IconButtonShape::Square)
1729 .icon_size(IconSize::Small)
1730 .on_click({
1731 let view = cx.view().clone();
1732 move |_, cx| view.update(cx, Self::navigate_backward)
1733 })
1734 .disabled(!self.can_navigate_backward())
1735 .tooltip(|cx| Tooltip::for_action("Go Back", &GoBack, cx));
1736
1737 let navigate_forward = IconButton::new("navigate_forward", IconName::ArrowRight)
1738 .shape(IconButtonShape::Square)
1739 .icon_size(IconSize::Small)
1740 .on_click({
1741 let view = cx.view().clone();
1742 move |_, cx| view.update(cx, Self::navigate_forward)
1743 })
1744 .disabled(!self.can_navigate_forward())
1745 .tooltip(|cx| Tooltip::for_action("Go Forward", &GoForward, cx));
1746
1747 TabBar::new("tab_bar")
1748 .track_scroll(self.tab_bar_scroll_handle.clone())
1749 .when(
1750 self.display_nav_history_buttons.unwrap_or_default(),
1751 |tab_bar| tab_bar.start_children(vec![navigate_backward, navigate_forward]),
1752 )
1753 .when(self.has_focus(cx), |tab_bar| {
1754 tab_bar.end_child({
1755 let render_tab_buttons = self.render_tab_bar_buttons.clone();
1756 render_tab_buttons(self, cx)
1757 })
1758 })
1759 .children(
1760 self.items
1761 .iter()
1762 .enumerate()
1763 .zip(tab_details(&self.items, cx))
1764 .map(|((ix, item), detail)| self.render_tab(ix, item, detail, cx)),
1765 )
1766 .child(
1767 div()
1768 .id("tab_bar_drop_target")
1769 .min_w_6()
1770 // HACK: This empty child is currently necessary to force the drop target to appear
1771 // despite us setting a min width above.
1772 .child("")
1773 .h_full()
1774 .flex_grow()
1775 .drag_over::<DraggedTab>(|bar, _, cx| {
1776 bar.bg(cx.theme().colors().drop_target_background)
1777 })
1778 .drag_over::<ProjectEntryId>(|bar, _, cx| {
1779 bar.bg(cx.theme().colors().drop_target_background)
1780 })
1781 .on_drop(cx.listener(move |this, dragged_tab: &DraggedTab, cx| {
1782 this.drag_split_direction = None;
1783 this.handle_tab_drop(dragged_tab, this.items.len(), cx)
1784 }))
1785 .on_drop(cx.listener(move |this, entry_id: &ProjectEntryId, cx| {
1786 this.drag_split_direction = None;
1787 this.handle_project_entry_drop(entry_id, cx)
1788 }))
1789 .on_drop(cx.listener(move |this, paths, cx| {
1790 this.drag_split_direction = None;
1791 this.handle_external_paths_drop(paths, cx)
1792 }))
1793 .on_click(cx.listener(move |this, event: &ClickEvent, cx| {
1794 if event.up.click_count == 2 {
1795 cx.dispatch_action(this.double_click_dispatch_action.boxed_clone())
1796 }
1797 })),
1798 )
1799 }
1800
1801 pub fn render_menu_overlay(menu: &View<ContextMenu>) -> Div {
1802 div().absolute().bottom_0().right_0().size_0().child(
1803 deferred(
1804 anchored()
1805 .anchor(AnchorCorner::TopRight)
1806 .child(menu.clone()),
1807 )
1808 .with_priority(1),
1809 )
1810 }
1811
1812 pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
1813 self.zoomed = zoomed;
1814 cx.notify();
1815 }
1816
1817 pub fn is_zoomed(&self) -> bool {
1818 self.zoomed
1819 }
1820
1821 fn handle_drag_move<T>(&mut self, event: &DragMoveEvent<T>, cx: &mut ViewContext<Self>) {
1822 if !self.can_split {
1823 return;
1824 }
1825
1826 let rect = event.bounds.size;
1827
1828 let size = event.bounds.size.width.min(event.bounds.size.height)
1829 * WorkspaceSettings::get_global(cx).drop_target_size;
1830
1831 let relative_cursor = Point::new(
1832 event.event.position.x - event.bounds.left(),
1833 event.event.position.y - event.bounds.top(),
1834 );
1835
1836 let direction = if relative_cursor.x < size
1837 || relative_cursor.x > rect.width - size
1838 || relative_cursor.y < size
1839 || relative_cursor.y > rect.height - size
1840 {
1841 [
1842 SplitDirection::Up,
1843 SplitDirection::Right,
1844 SplitDirection::Down,
1845 SplitDirection::Left,
1846 ]
1847 .iter()
1848 .min_by_key(|side| match side {
1849 SplitDirection::Up => relative_cursor.y,
1850 SplitDirection::Right => rect.width - relative_cursor.x,
1851 SplitDirection::Down => rect.height - relative_cursor.y,
1852 SplitDirection::Left => relative_cursor.x,
1853 })
1854 .cloned()
1855 } else {
1856 None
1857 };
1858
1859 if direction != self.drag_split_direction {
1860 self.drag_split_direction = direction;
1861 }
1862 }
1863
1864 fn handle_tab_drop(
1865 &mut self,
1866 dragged_tab: &DraggedTab,
1867 ix: usize,
1868 cx: &mut ViewContext<'_, Self>,
1869 ) {
1870 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
1871 if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_tab, cx) {
1872 return;
1873 }
1874 }
1875 let mut to_pane = cx.view().clone();
1876 let split_direction = self.drag_split_direction;
1877 let item_id = dragged_tab.item.item_id();
1878 if let Some(preview_item_id) = self.preview_item_id {
1879 if item_id == preview_item_id {
1880 self.set_preview_item_id(None, cx);
1881 }
1882 }
1883
1884 let from_pane = dragged_tab.pane.clone();
1885 self.workspace
1886 .update(cx, |_, cx| {
1887 cx.defer(move |workspace, cx| {
1888 if let Some(split_direction) = split_direction {
1889 to_pane = workspace.split_pane(to_pane, split_direction, cx);
1890 }
1891 workspace.move_item(from_pane, to_pane, item_id, ix, cx);
1892 });
1893 })
1894 .log_err();
1895 }
1896
1897 fn handle_project_entry_drop(
1898 &mut self,
1899 project_entry_id: &ProjectEntryId,
1900 cx: &mut ViewContext<'_, Self>,
1901 ) {
1902 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
1903 if let ControlFlow::Break(()) = custom_drop_handle(self, project_entry_id, cx) {
1904 return;
1905 }
1906 }
1907 let mut to_pane = cx.view().clone();
1908 let split_direction = self.drag_split_direction;
1909 let project_entry_id = *project_entry_id;
1910 self.workspace
1911 .update(cx, |_, cx| {
1912 cx.defer(move |workspace, cx| {
1913 if let Some(path) = workspace
1914 .project()
1915 .read(cx)
1916 .path_for_entry(project_entry_id, cx)
1917 {
1918 if let Some(split_direction) = split_direction {
1919 to_pane = workspace.split_pane(to_pane, split_direction, cx);
1920 }
1921 workspace
1922 .open_path(path, Some(to_pane.downgrade()), true, cx)
1923 .detach_and_log_err(cx);
1924 }
1925 });
1926 })
1927 .log_err();
1928 }
1929
1930 fn handle_external_paths_drop(
1931 &mut self,
1932 paths: &ExternalPaths,
1933 cx: &mut ViewContext<'_, Self>,
1934 ) {
1935 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
1936 if let ControlFlow::Break(()) = custom_drop_handle(self, paths, cx) {
1937 return;
1938 }
1939 }
1940 let mut to_pane = cx.view().clone();
1941 let mut split_direction = self.drag_split_direction;
1942 let paths = paths.paths().to_vec();
1943 let is_remote = self
1944 .workspace
1945 .update(cx, |workspace, cx| {
1946 if workspace.project().read(cx).is_remote() {
1947 workspace.show_error(
1948 &anyhow::anyhow!("Cannot drop files on a remote project"),
1949 cx,
1950 );
1951 true
1952 } else {
1953 false
1954 }
1955 })
1956 .unwrap_or(true);
1957 if is_remote {
1958 return;
1959 }
1960
1961 self.workspace
1962 .update(cx, |workspace, cx| {
1963 let fs = Arc::clone(workspace.project().read(cx).fs());
1964 cx.spawn(|workspace, mut cx| async move {
1965 let mut is_file_checks = FuturesUnordered::new();
1966 for path in &paths {
1967 is_file_checks.push(fs.is_file(path))
1968 }
1969 let mut has_files_to_open = false;
1970 while let Some(is_file) = is_file_checks.next().await {
1971 if is_file {
1972 has_files_to_open = true;
1973 break;
1974 }
1975 }
1976 drop(is_file_checks);
1977 if !has_files_to_open {
1978 split_direction = None;
1979 }
1980
1981 if let Some(open_task) = workspace
1982 .update(&mut cx, |workspace, cx| {
1983 if let Some(split_direction) = split_direction {
1984 to_pane = workspace.split_pane(to_pane, split_direction, cx);
1985 }
1986 workspace.open_paths(
1987 paths,
1988 OpenVisible::OnlyDirectories,
1989 Some(to_pane.downgrade()),
1990 cx,
1991 )
1992 })
1993 .ok()
1994 {
1995 let _opened_items: Vec<_> = open_task.await;
1996 }
1997 })
1998 .detach();
1999 })
2000 .log_err();
2001 }
2002
2003 pub fn display_nav_history_buttons(&mut self, display: Option<bool>) {
2004 self.display_nav_history_buttons = display;
2005 }
2006}
2007
2008impl FocusableView for Pane {
2009 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
2010 self.focus_handle.clone()
2011 }
2012}
2013
2014impl Render for Pane {
2015 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2016 let mut key_context = KeyContext::new_with_defaults();
2017 key_context.add("Pane");
2018 if self.active_item().is_none() {
2019 key_context.add("EmptyPane");
2020 }
2021
2022 let should_display_tab_bar = self.should_display_tab_bar.clone();
2023 let display_tab_bar = should_display_tab_bar(cx);
2024
2025 v_flex()
2026 .key_context(key_context)
2027 .track_focus(&self.focus_handle)
2028 .size_full()
2029 .flex_none()
2030 .overflow_hidden()
2031 .on_action(cx.listener(|pane, _: &AlternateFile, cx| {
2032 pane.alternate_file(cx);
2033 }))
2034 .on_action(cx.listener(|pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx)))
2035 .on_action(cx.listener(|pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx)))
2036 .on_action(
2037 cx.listener(|pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx)),
2038 )
2039 .on_action(cx.listener(|pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx)))
2040 .on_action(cx.listener(|pane, _: &GoBack, cx| pane.navigate_backward(cx)))
2041 .on_action(cx.listener(|pane, _: &GoForward, cx| pane.navigate_forward(cx)))
2042 .on_action(cx.listener(Pane::toggle_zoom))
2043 .on_action(cx.listener(|pane: &mut Pane, action: &ActivateItem, cx| {
2044 pane.activate_item(action.0, true, true, cx);
2045 }))
2046 .on_action(cx.listener(|pane: &mut Pane, _: &ActivateLastItem, cx| {
2047 pane.activate_item(pane.items.len() - 1, true, true, cx);
2048 }))
2049 .on_action(cx.listener(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
2050 pane.activate_prev_item(true, cx);
2051 }))
2052 .on_action(cx.listener(|pane: &mut Pane, _: &ActivateNextItem, cx| {
2053 pane.activate_next_item(true, cx);
2054 }))
2055 .when(PreviewTabsSettings::get_global(cx).enabled, |this| {
2056 this.on_action(cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, cx| {
2057 if let Some(active_item_id) = pane.active_item().map(|i| i.item_id()) {
2058 if pane.is_active_preview_item(active_item_id) {
2059 pane.set_preview_item_id(None, cx);
2060 } else {
2061 pane.set_preview_item_id(Some(active_item_id), cx);
2062 }
2063 }
2064 }))
2065 })
2066 .on_action(
2067 cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
2068 if let Some(task) = pane.close_active_item(action, cx) {
2069 task.detach_and_log_err(cx)
2070 }
2071 }),
2072 )
2073 .on_action(
2074 cx.listener(|pane: &mut Self, action: &CloseInactiveItems, cx| {
2075 if let Some(task) = pane.close_inactive_items(action, cx) {
2076 task.detach_and_log_err(cx)
2077 }
2078 }),
2079 )
2080 .on_action(
2081 cx.listener(|pane: &mut Self, action: &CloseCleanItems, cx| {
2082 if let Some(task) = pane.close_clean_items(action, cx) {
2083 task.detach_and_log_err(cx)
2084 }
2085 }),
2086 )
2087 .on_action(
2088 cx.listener(|pane: &mut Self, action: &CloseItemsToTheLeft, cx| {
2089 if let Some(task) = pane.close_items_to_the_left(action, cx) {
2090 task.detach_and_log_err(cx)
2091 }
2092 }),
2093 )
2094 .on_action(
2095 cx.listener(|pane: &mut Self, action: &CloseItemsToTheRight, cx| {
2096 if let Some(task) = pane.close_items_to_the_right(action, cx) {
2097 task.detach_and_log_err(cx)
2098 }
2099 }),
2100 )
2101 .on_action(cx.listener(|pane: &mut Self, action: &CloseAllItems, cx| {
2102 if let Some(task) = pane.close_all_items(action, cx) {
2103 task.detach_and_log_err(cx)
2104 }
2105 }))
2106 .on_action(
2107 cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
2108 if let Some(task) = pane.close_active_item(action, cx) {
2109 task.detach_and_log_err(cx)
2110 }
2111 }),
2112 )
2113 .on_action(
2114 cx.listener(|pane: &mut Self, action: &RevealInProjectPanel, cx| {
2115 let entry_id = action
2116 .entry_id
2117 .map(ProjectEntryId::from_proto)
2118 .or_else(|| pane.active_item()?.project_entry_ids(cx).first().copied());
2119 if let Some(entry_id) = entry_id {
2120 pane.project.update(cx, |_, cx| {
2121 cx.emit(project::Event::RevealInProjectPanel(entry_id))
2122 });
2123 }
2124 }),
2125 )
2126 .when(self.active_item().is_some() && display_tab_bar, |pane| {
2127 pane.child(self.render_tab_bar(cx))
2128 })
2129 .child({
2130 let has_worktrees = self.project.read(cx).worktrees().next().is_some();
2131 // main content
2132 div()
2133 .flex_1()
2134 .relative()
2135 .group("")
2136 .on_drag_move::<DraggedTab>(cx.listener(Self::handle_drag_move))
2137 .on_drag_move::<ProjectEntryId>(cx.listener(Self::handle_drag_move))
2138 .on_drag_move::<ExternalPaths>(cx.listener(Self::handle_drag_move))
2139 .map(|div| {
2140 if let Some(item) = self.active_item() {
2141 div.v_flex()
2142 .child(self.toolbar.clone())
2143 .child(item.to_any())
2144 } else {
2145 let placeholder = div.h_flex().size_full().justify_center();
2146 if has_worktrees {
2147 placeholder
2148 } else {
2149 placeholder.child(
2150 Label::new("Open a file or project to get started.")
2151 .color(Color::Muted),
2152 )
2153 }
2154 }
2155 })
2156 .child(
2157 // drag target
2158 div()
2159 .invisible()
2160 .absolute()
2161 .bg(cx.theme().colors().drop_target_background)
2162 .group_drag_over::<DraggedTab>("", |style| style.visible())
2163 .group_drag_over::<ProjectEntryId>("", |style| style.visible())
2164 .group_drag_over::<ExternalPaths>("", |style| style.visible())
2165 .when_some(self.can_drop_predicate.clone(), |this, p| {
2166 this.can_drop(move |a, cx| p(a, cx))
2167 })
2168 .on_drop(cx.listener(move |this, dragged_tab, cx| {
2169 this.handle_tab_drop(dragged_tab, this.active_item_index(), cx)
2170 }))
2171 .on_drop(cx.listener(move |this, entry_id, cx| {
2172 this.handle_project_entry_drop(entry_id, cx)
2173 }))
2174 .on_drop(cx.listener(move |this, paths, cx| {
2175 this.handle_external_paths_drop(paths, cx)
2176 }))
2177 .map(|div| {
2178 let size = DefiniteLength::Fraction(0.5);
2179 match self.drag_split_direction {
2180 None => div.top_0().right_0().bottom_0().left_0(),
2181 Some(SplitDirection::Up) => {
2182 div.top_0().left_0().right_0().h(size)
2183 }
2184 Some(SplitDirection::Down) => {
2185 div.left_0().bottom_0().right_0().h(size)
2186 }
2187 Some(SplitDirection::Left) => {
2188 div.top_0().left_0().bottom_0().w(size)
2189 }
2190 Some(SplitDirection::Right) => {
2191 div.top_0().bottom_0().right_0().w(size)
2192 }
2193 }
2194 }),
2195 )
2196 })
2197 .on_mouse_down(
2198 MouseButton::Navigate(NavigationDirection::Back),
2199 cx.listener(|pane, _, cx| {
2200 if let Some(workspace) = pane.workspace.upgrade() {
2201 let pane = cx.view().downgrade();
2202 cx.window_context().defer(move |cx| {
2203 workspace.update(cx, |workspace, cx| {
2204 workspace.go_back(pane, cx).detach_and_log_err(cx)
2205 })
2206 })
2207 }
2208 }),
2209 )
2210 .on_mouse_down(
2211 MouseButton::Navigate(NavigationDirection::Forward),
2212 cx.listener(|pane, _, cx| {
2213 if let Some(workspace) = pane.workspace.upgrade() {
2214 let pane = cx.view().downgrade();
2215 cx.window_context().defer(move |cx| {
2216 workspace.update(cx, |workspace, cx| {
2217 workspace.go_forward(pane, cx).detach_and_log_err(cx)
2218 })
2219 })
2220 }
2221 }),
2222 )
2223 }
2224}
2225
2226impl ItemNavHistory {
2227 pub fn push<D: 'static + Send + Any>(&mut self, data: Option<D>, cx: &mut WindowContext) {
2228 self.history
2229 .push(data, self.item.clone(), self.is_preview, cx);
2230 }
2231
2232 pub fn pop_backward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
2233 self.history.pop(NavigationMode::GoingBack, cx)
2234 }
2235
2236 pub fn pop_forward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
2237 self.history.pop(NavigationMode::GoingForward, cx)
2238 }
2239}
2240
2241impl NavHistory {
2242 pub fn for_each_entry(
2243 &self,
2244 cx: &AppContext,
2245 mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
2246 ) {
2247 let borrowed_history = self.0.lock();
2248 borrowed_history
2249 .forward_stack
2250 .iter()
2251 .chain(borrowed_history.backward_stack.iter())
2252 .chain(borrowed_history.closed_stack.iter())
2253 .for_each(|entry| {
2254 if let Some(project_and_abs_path) =
2255 borrowed_history.paths_by_item.get(&entry.item.id())
2256 {
2257 f(entry, project_and_abs_path.clone());
2258 } else if let Some(item) = entry.item.upgrade() {
2259 if let Some(path) = item.project_path(cx) {
2260 f(entry, (path, None));
2261 }
2262 }
2263 })
2264 }
2265
2266 pub fn set_mode(&mut self, mode: NavigationMode) {
2267 self.0.lock().mode = mode;
2268 }
2269
2270 pub fn mode(&self) -> NavigationMode {
2271 self.0.lock().mode
2272 }
2273
2274 pub fn disable(&mut self) {
2275 self.0.lock().mode = NavigationMode::Disabled;
2276 }
2277
2278 pub fn enable(&mut self) {
2279 self.0.lock().mode = NavigationMode::Normal;
2280 }
2281
2282 pub fn pop(&mut self, mode: NavigationMode, cx: &mut WindowContext) -> Option<NavigationEntry> {
2283 let mut state = self.0.lock();
2284 let entry = match mode {
2285 NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
2286 return None
2287 }
2288 NavigationMode::GoingBack => &mut state.backward_stack,
2289 NavigationMode::GoingForward => &mut state.forward_stack,
2290 NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
2291 }
2292 .pop_back();
2293 if entry.is_some() {
2294 state.did_update(cx);
2295 }
2296 entry
2297 }
2298
2299 pub fn push<D: 'static + Send + Any>(
2300 &mut self,
2301 data: Option<D>,
2302 item: Arc<dyn WeakItemHandle>,
2303 is_preview: bool,
2304 cx: &mut WindowContext,
2305 ) {
2306 let state = &mut *self.0.lock();
2307 match state.mode {
2308 NavigationMode::Disabled => {}
2309 NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
2310 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2311 state.backward_stack.pop_front();
2312 }
2313 state.backward_stack.push_back(NavigationEntry {
2314 item,
2315 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2316 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2317 is_preview,
2318 });
2319 state.forward_stack.clear();
2320 }
2321 NavigationMode::GoingBack => {
2322 if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2323 state.forward_stack.pop_front();
2324 }
2325 state.forward_stack.push_back(NavigationEntry {
2326 item,
2327 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2328 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2329 is_preview,
2330 });
2331 }
2332 NavigationMode::GoingForward => {
2333 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2334 state.backward_stack.pop_front();
2335 }
2336 state.backward_stack.push_back(NavigationEntry {
2337 item,
2338 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2339 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2340 is_preview,
2341 });
2342 }
2343 NavigationMode::ClosingItem => {
2344 if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2345 state.closed_stack.pop_front();
2346 }
2347 state.closed_stack.push_back(NavigationEntry {
2348 item,
2349 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2350 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2351 is_preview,
2352 });
2353 }
2354 }
2355 state.did_update(cx);
2356 }
2357
2358 pub fn remove_item(&mut self, item_id: EntityId) {
2359 let mut state = self.0.lock();
2360 state.paths_by_item.remove(&item_id);
2361 state
2362 .backward_stack
2363 .retain(|entry| entry.item.id() != item_id);
2364 state
2365 .forward_stack
2366 .retain(|entry| entry.item.id() != item_id);
2367 state
2368 .closed_stack
2369 .retain(|entry| entry.item.id() != item_id);
2370 }
2371
2372 pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option<PathBuf>)> {
2373 self.0.lock().paths_by_item.get(&item_id).cloned()
2374 }
2375}
2376
2377impl NavHistoryState {
2378 pub fn did_update(&self, cx: &mut WindowContext) {
2379 if let Some(pane) = self.pane.upgrade() {
2380 cx.defer(move |cx| {
2381 pane.update(cx, |pane, cx| pane.history_updated(cx));
2382 });
2383 }
2384 }
2385}
2386
2387fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
2388 let path = buffer_path
2389 .as_ref()
2390 .and_then(|p| {
2391 p.path
2392 .to_str()
2393 .and_then(|s| if s == "" { None } else { Some(s) })
2394 })
2395 .unwrap_or("This buffer");
2396 let path = truncate_and_remove_front(path, 80);
2397 format!("{path} contains unsaved edits. Do you want to save it?")
2398}
2399
2400pub fn tab_details(items: &Vec<Box<dyn ItemHandle>>, cx: &AppContext) -> Vec<usize> {
2401 let mut tab_details = items.iter().map(|_| 0).collect::<Vec<_>>();
2402 let mut tab_descriptions = HashMap::default();
2403 let mut done = false;
2404 while !done {
2405 done = true;
2406
2407 // Store item indices by their tab description.
2408 for (ix, (item, detail)) in items.iter().zip(&tab_details).enumerate() {
2409 if let Some(description) = item.tab_description(*detail, cx) {
2410 if *detail == 0
2411 || Some(&description) != item.tab_description(detail - 1, cx).as_ref()
2412 {
2413 tab_descriptions
2414 .entry(description)
2415 .or_insert(Vec::new())
2416 .push(ix);
2417 }
2418 }
2419 }
2420
2421 // If two or more items have the same tab description, increase their level
2422 // of detail and try again.
2423 for (_, item_ixs) in tab_descriptions.drain() {
2424 if item_ixs.len() > 1 {
2425 done = false;
2426 for ix in item_ixs {
2427 tab_details[ix] += 1;
2428 }
2429 }
2430 }
2431 }
2432
2433 tab_details
2434}
2435
2436pub fn render_item_indicator(item: Box<dyn ItemHandle>, cx: &WindowContext) -> Option<Indicator> {
2437 maybe!({
2438 let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
2439 (true, _) => Color::Warning,
2440 (_, true) => Color::Accent,
2441 (false, false) => return None,
2442 };
2443
2444 Some(Indicator::dot().color(indicator_color))
2445 })
2446}
2447
2448#[cfg(test)]
2449mod tests {
2450 use super::*;
2451 use crate::item::test::{TestItem, TestProjectItem};
2452 use gpui::{TestAppContext, VisualTestContext};
2453 use project::FakeFs;
2454 use settings::SettingsStore;
2455 use theme::LoadThemes;
2456
2457 #[gpui::test]
2458 async fn test_remove_active_empty(cx: &mut TestAppContext) {
2459 init_test(cx);
2460 let fs = FakeFs::new(cx.executor());
2461
2462 let project = Project::test(fs, None, cx).await;
2463 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2464 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2465
2466 pane.update(cx, |pane, cx| {
2467 assert!(pane
2468 .close_active_item(&CloseActiveItem { save_intent: None }, cx)
2469 .is_none())
2470 });
2471 }
2472
2473 #[gpui::test]
2474 async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
2475 init_test(cx);
2476 let fs = FakeFs::new(cx.executor());
2477
2478 let project = Project::test(fs, None, cx).await;
2479 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2480 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2481
2482 // 1. Add with a destination index
2483 // a. Add before the active item
2484 set_labeled_items(&pane, ["A", "B*", "C"], cx);
2485 pane.update(cx, |pane, cx| {
2486 pane.add_item(
2487 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2488 false,
2489 false,
2490 Some(0),
2491 cx,
2492 );
2493 });
2494 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
2495
2496 // b. Add after the active item
2497 set_labeled_items(&pane, ["A", "B*", "C"], cx);
2498 pane.update(cx, |pane, cx| {
2499 pane.add_item(
2500 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2501 false,
2502 false,
2503 Some(2),
2504 cx,
2505 );
2506 });
2507 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
2508
2509 // c. Add at the end of the item list (including off the length)
2510 set_labeled_items(&pane, ["A", "B*", "C"], cx);
2511 pane.update(cx, |pane, cx| {
2512 pane.add_item(
2513 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2514 false,
2515 false,
2516 Some(5),
2517 cx,
2518 );
2519 });
2520 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2521
2522 // 2. Add without a destination index
2523 // a. Add with active item at the start of the item list
2524 set_labeled_items(&pane, ["A*", "B", "C"], cx);
2525 pane.update(cx, |pane, cx| {
2526 pane.add_item(
2527 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2528 false,
2529 false,
2530 None,
2531 cx,
2532 );
2533 });
2534 set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
2535
2536 // b. Add with active item at the end of the item list
2537 set_labeled_items(&pane, ["A", "B", "C*"], cx);
2538 pane.update(cx, |pane, cx| {
2539 pane.add_item(
2540 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2541 false,
2542 false,
2543 None,
2544 cx,
2545 );
2546 });
2547 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2548 }
2549
2550 #[gpui::test]
2551 async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
2552 init_test(cx);
2553 let fs = FakeFs::new(cx.executor());
2554
2555 let project = Project::test(fs, None, cx).await;
2556 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2557 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2558
2559 // 1. Add with a destination index
2560 // 1a. Add before 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(0), cx);
2564 });
2565 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
2566
2567 // 1b. Add after the active item
2568 let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
2569 pane.update(cx, |pane, cx| {
2570 pane.add_item(d, false, false, Some(2), cx);
2571 });
2572 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
2573
2574 // 1c. Add at the end of the item list (including off the length)
2575 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
2576 pane.update(cx, |pane, cx| {
2577 pane.add_item(a, false, false, Some(5), cx);
2578 });
2579 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
2580
2581 // 1d. Add same item to active index
2582 let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
2583 pane.update(cx, |pane, cx| {
2584 pane.add_item(b, false, false, Some(1), cx);
2585 });
2586 assert_item_labels(&pane, ["A", "B*", "C"], cx);
2587
2588 // 1e. Add item to index after same item in last position
2589 let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
2590 pane.update(cx, |pane, cx| {
2591 pane.add_item(c, false, false, Some(2), cx);
2592 });
2593 assert_item_labels(&pane, ["A", "B", "C*"], cx);
2594
2595 // 2. Add without a destination index
2596 // 2a. Add with active item at the start of the item list
2597 let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
2598 pane.update(cx, |pane, cx| {
2599 pane.add_item(d, false, false, None, cx);
2600 });
2601 assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
2602
2603 // 2b. Add with active item at the end of the item list
2604 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
2605 pane.update(cx, |pane, cx| {
2606 pane.add_item(a, false, false, None, cx);
2607 });
2608 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
2609
2610 // 2c. Add active item to active item at end of list
2611 let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
2612 pane.update(cx, |pane, cx| {
2613 pane.add_item(c, false, false, None, cx);
2614 });
2615 assert_item_labels(&pane, ["A", "B", "C*"], cx);
2616
2617 // 2d. Add active item to active item at start of list
2618 let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
2619 pane.update(cx, |pane, cx| {
2620 pane.add_item(a, false, false, None, cx);
2621 });
2622 assert_item_labels(&pane, ["A*", "B", "C"], cx);
2623 }
2624
2625 #[gpui::test]
2626 async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
2627 init_test(cx);
2628 let fs = FakeFs::new(cx.executor());
2629
2630 let project = Project::test(fs, None, cx).await;
2631 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2632 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2633
2634 // singleton view
2635 pane.update(cx, |pane, cx| {
2636 pane.add_item(
2637 Box::new(cx.new_view(|cx| {
2638 TestItem::new(cx)
2639 .with_singleton(true)
2640 .with_label("buffer 1")
2641 .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
2642 })),
2643 false,
2644 false,
2645 None,
2646 cx,
2647 );
2648 });
2649 assert_item_labels(&pane, ["buffer 1*"], cx);
2650
2651 // new singleton view with the same project entry
2652 pane.update(cx, |pane, cx| {
2653 pane.add_item(
2654 Box::new(cx.new_view(|cx| {
2655 TestItem::new(cx)
2656 .with_singleton(true)
2657 .with_label("buffer 1")
2658 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
2659 })),
2660 false,
2661 false,
2662 None,
2663 cx,
2664 );
2665 });
2666 assert_item_labels(&pane, ["buffer 1*"], cx);
2667
2668 // new singleton view with different project entry
2669 pane.update(cx, |pane, cx| {
2670 pane.add_item(
2671 Box::new(cx.new_view(|cx| {
2672 TestItem::new(cx)
2673 .with_singleton(true)
2674 .with_label("buffer 2")
2675 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
2676 })),
2677 false,
2678 false,
2679 None,
2680 cx,
2681 );
2682 });
2683 assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
2684
2685 // new multibuffer view with the same project entry
2686 pane.update(cx, |pane, cx| {
2687 pane.add_item(
2688 Box::new(cx.new_view(|cx| {
2689 TestItem::new(cx)
2690 .with_singleton(false)
2691 .with_label("multibuffer 1")
2692 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
2693 })),
2694 false,
2695 false,
2696 None,
2697 cx,
2698 );
2699 });
2700 assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
2701
2702 // another multibuffer view with the same project entry
2703 pane.update(cx, |pane, cx| {
2704 pane.add_item(
2705 Box::new(cx.new_view(|cx| {
2706 TestItem::new(cx)
2707 .with_singleton(false)
2708 .with_label("multibuffer 1b")
2709 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
2710 })),
2711 false,
2712 false,
2713 None,
2714 cx,
2715 );
2716 });
2717 assert_item_labels(
2718 &pane,
2719 ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
2720 cx,
2721 );
2722 }
2723
2724 #[gpui::test]
2725 async fn test_remove_item_ordering(cx: &mut TestAppContext) {
2726 init_test(cx);
2727 let fs = FakeFs::new(cx.executor());
2728
2729 let project = Project::test(fs, None, cx).await;
2730 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2731 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2732
2733 add_labeled_item(&pane, "A", false, cx);
2734 add_labeled_item(&pane, "B", false, cx);
2735 add_labeled_item(&pane, "C", false, cx);
2736 add_labeled_item(&pane, "D", false, cx);
2737 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2738
2739 pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
2740 add_labeled_item(&pane, "1", false, cx);
2741 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
2742
2743 pane.update(cx, |pane, cx| {
2744 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
2745 })
2746 .unwrap()
2747 .await
2748 .unwrap();
2749 assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
2750
2751 pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
2752 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2753
2754 pane.update(cx, |pane, cx| {
2755 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
2756 })
2757 .unwrap()
2758 .await
2759 .unwrap();
2760 assert_item_labels(&pane, ["A", "B*", "C"], cx);
2761
2762 pane.update(cx, |pane, cx| {
2763 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
2764 })
2765 .unwrap()
2766 .await
2767 .unwrap();
2768 assert_item_labels(&pane, ["A", "C*"], cx);
2769
2770 pane.update(cx, |pane, cx| {
2771 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
2772 })
2773 .unwrap()
2774 .await
2775 .unwrap();
2776 assert_item_labels(&pane, ["A*"], cx);
2777 }
2778
2779 #[gpui::test]
2780 async fn test_close_inactive_items(cx: &mut TestAppContext) {
2781 init_test(cx);
2782 let fs = FakeFs::new(cx.executor());
2783
2784 let project = Project::test(fs, None, cx).await;
2785 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2786 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2787
2788 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
2789
2790 pane.update(cx, |pane, cx| {
2791 pane.close_inactive_items(&CloseInactiveItems { save_intent: None }, cx)
2792 })
2793 .unwrap()
2794 .await
2795 .unwrap();
2796 assert_item_labels(&pane, ["C*"], cx);
2797 }
2798
2799 #[gpui::test]
2800 async fn test_close_clean_items(cx: &mut TestAppContext) {
2801 init_test(cx);
2802 let fs = FakeFs::new(cx.executor());
2803
2804 let project = Project::test(fs, None, cx).await;
2805 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2806 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2807
2808 add_labeled_item(&pane, "A", true, cx);
2809 add_labeled_item(&pane, "B", false, cx);
2810 add_labeled_item(&pane, "C", true, cx);
2811 add_labeled_item(&pane, "D", false, cx);
2812 add_labeled_item(&pane, "E", false, cx);
2813 assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
2814
2815 pane.update(cx, |pane, cx| pane.close_clean_items(&CloseCleanItems, cx))
2816 .unwrap()
2817 .await
2818 .unwrap();
2819 assert_item_labels(&pane, ["A^", "C*^"], cx);
2820 }
2821
2822 #[gpui::test]
2823 async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
2824 init_test(cx);
2825 let fs = FakeFs::new(cx.executor());
2826
2827 let project = Project::test(fs, None, cx).await;
2828 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2829 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2830
2831 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
2832
2833 pane.update(cx, |pane, cx| {
2834 pane.close_items_to_the_left(&CloseItemsToTheLeft, cx)
2835 })
2836 .unwrap()
2837 .await
2838 .unwrap();
2839 assert_item_labels(&pane, ["C*", "D", "E"], cx);
2840 }
2841
2842 #[gpui::test]
2843 async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
2844 init_test(cx);
2845 let fs = FakeFs::new(cx.executor());
2846
2847 let project = Project::test(fs, None, cx).await;
2848 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2849 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2850
2851 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
2852
2853 pane.update(cx, |pane, cx| {
2854 pane.close_items_to_the_right(&CloseItemsToTheRight, cx)
2855 })
2856 .unwrap()
2857 .await
2858 .unwrap();
2859 assert_item_labels(&pane, ["A", "B", "C*"], cx);
2860 }
2861
2862 #[gpui::test]
2863 async fn test_close_all_items(cx: &mut TestAppContext) {
2864 init_test(cx);
2865 let fs = FakeFs::new(cx.executor());
2866
2867 let project = Project::test(fs, None, cx).await;
2868 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2869 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2870
2871 add_labeled_item(&pane, "A", false, cx);
2872 add_labeled_item(&pane, "B", false, cx);
2873 add_labeled_item(&pane, "C", false, cx);
2874 assert_item_labels(&pane, ["A", "B", "C*"], cx);
2875
2876 pane.update(cx, |pane, cx| {
2877 pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
2878 })
2879 .unwrap()
2880 .await
2881 .unwrap();
2882 assert_item_labels(&pane, [], cx);
2883
2884 add_labeled_item(&pane, "A", true, cx);
2885 add_labeled_item(&pane, "B", true, cx);
2886 add_labeled_item(&pane, "C", true, cx);
2887 assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
2888
2889 let save = pane
2890 .update(cx, |pane, cx| {
2891 pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
2892 })
2893 .unwrap();
2894
2895 cx.executor().run_until_parked();
2896 cx.simulate_prompt_answer(2);
2897 save.await.unwrap();
2898 assert_item_labels(&pane, [], cx);
2899 }
2900
2901 fn init_test(cx: &mut TestAppContext) {
2902 cx.update(|cx| {
2903 let settings_store = SettingsStore::test(cx);
2904 cx.set_global(settings_store);
2905 theme::init(LoadThemes::JustBase, cx);
2906 crate::init_settings(cx);
2907 Project::init_settings(cx);
2908 });
2909 }
2910
2911 fn add_labeled_item(
2912 pane: &View<Pane>,
2913 label: &str,
2914 is_dirty: bool,
2915 cx: &mut VisualTestContext,
2916 ) -> Box<View<TestItem>> {
2917 pane.update(cx, |pane, cx| {
2918 let labeled_item = Box::new(
2919 cx.new_view(|cx| TestItem::new(cx).with_label(label).with_dirty(is_dirty)),
2920 );
2921 pane.add_item(labeled_item.clone(), false, false, None, cx);
2922 labeled_item
2923 })
2924 }
2925
2926 fn set_labeled_items<const COUNT: usize>(
2927 pane: &View<Pane>,
2928 labels: [&str; COUNT],
2929 cx: &mut VisualTestContext,
2930 ) -> [Box<View<TestItem>>; COUNT] {
2931 pane.update(cx, |pane, cx| {
2932 pane.items.clear();
2933 let mut active_item_index = 0;
2934
2935 let mut index = 0;
2936 let items = labels.map(|mut label| {
2937 if label.ends_with('*') {
2938 label = label.trim_end_matches('*');
2939 active_item_index = index;
2940 }
2941
2942 let labeled_item = Box::new(cx.new_view(|cx| TestItem::new(cx).with_label(label)));
2943 pane.add_item(labeled_item.clone(), false, false, None, cx);
2944 index += 1;
2945 labeled_item
2946 });
2947
2948 pane.activate_item(active_item_index, false, false, cx);
2949
2950 items
2951 })
2952 }
2953
2954 // Assert the item label, with the active item label suffixed with a '*'
2955 fn assert_item_labels<const COUNT: usize>(
2956 pane: &View<Pane>,
2957 expected_states: [&str; COUNT],
2958 cx: &mut VisualTestContext,
2959 ) {
2960 pane.update(cx, |pane, cx| {
2961 let actual_states = pane
2962 .items
2963 .iter()
2964 .enumerate()
2965 .map(|(ix, item)| {
2966 let mut state = item
2967 .to_any()
2968 .downcast::<TestItem>()
2969 .unwrap()
2970 .read(cx)
2971 .label
2972 .clone();
2973 if ix == pane.active_item_index {
2974 state.push('*');
2975 }
2976 if item.is_dirty(cx) {
2977 state.push('^');
2978 }
2979 state
2980 })
2981 .collect::<Vec<_>>();
2982
2983 assert_eq!(
2984 actual_states, expected_states,
2985 "pane items do not match expectation"
2986 );
2987 })
2988 }
2989}
2990
2991impl Render for DraggedTab {
2992 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2993 let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
2994 let label = self.item.tab_content(
2995 TabContentParams {
2996 detail: Some(self.detail),
2997 selected: false,
2998 preview: false,
2999 },
3000 cx,
3001 );
3002 Tab::new("")
3003 .selected(self.is_active)
3004 .child(label)
3005 .render(cx)
3006 .font_family(ui_font)
3007 }
3008}