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