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