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