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