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