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