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