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