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