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