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