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