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