1use crate::{
2 CloseWindow, NewFile, NewTerminal, OpenInTerminal, OpenOptions, OpenTerminal, OpenVisible,
3 SplitDirection, ToggleFileFinder, ToggleProjectSymbols, ToggleZoom, Workspace,
4 WorkspaceItemBuilder,
5 item::{
6 ActivateOnClose, ClosePosition, Item, ItemHandle, ItemSettings, PreviewTabsSettings,
7 ProjectItemKind, ShowCloseButton, ShowDiagnostics, TabContentParams, TabTooltipContent,
8 WeakItemHandle,
9 },
10 move_item,
11 notifications::NotifyResultExt,
12 toolbar::Toolbar,
13 workspace_settings::{AutosaveSetting, TabBarSettings, WorkspaceSettings},
14};
15use anyhow::Result;
16use collections::{BTreeSet, HashMap, HashSet, VecDeque};
17use futures::{StreamExt, stream::FuturesUnordered};
18use gpui::{
19 Action, AnyElement, App, AsyncWindowContext, ClickEvent, ClipboardItem, Context, Corner, Div,
20 DragMoveEvent, Entity, EntityId, EventEmitter, ExternalPaths, FocusHandle, FocusOutEvent,
21 Focusable, KeyContext, MouseButton, MouseDownEvent, NavigationDirection, Pixels, Point,
22 PromptLevel, Render, ScrollHandle, Subscription, Task, WeakEntity, WeakFocusHandle, Window,
23 actions, anchored, deferred, impl_actions, prelude::*,
24};
25use itertools::Itertools;
26use language::DiagnosticSeverity;
27use parking_lot::Mutex;
28use project::{Project, ProjectEntryId, ProjectPath, WorktreeId};
29use schemars::JsonSchema;
30use serde::Deserialize;
31use settings::{Settings, SettingsStore};
32use std::{
33 any::Any,
34 cmp, fmt, mem,
35 ops::ControlFlow,
36 path::PathBuf,
37 rc::Rc,
38 sync::{
39 Arc,
40 atomic::{AtomicUsize, Ordering},
41 },
42};
43use theme::ThemeSettings;
44use ui::{
45 ButtonSize, Color, ContextMenu, ContextMenuEntry, ContextMenuItem, DecoratedIcon, IconButton,
46 IconButtonShape, IconDecoration, IconDecorationKind, IconName, IconSize, Indicator, Label,
47 PopoverMenu, PopoverMenuHandle, ScrollableHandle, Tab, TabBar, TabPosition, Tooltip,
48 prelude::*, right_click_menu,
49};
50use util::{ResultExt, debug_panic, maybe, truncate_and_remove_front};
51
52/// A selected entry in e.g. project panel.
53#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
54pub struct SelectedEntry {
55 pub worktree_id: WorktreeId,
56 pub entry_id: ProjectEntryId,
57}
58
59/// A group of selected entries from project panel.
60#[derive(Debug)]
61pub struct DraggedSelection {
62 pub active_selection: SelectedEntry,
63 pub marked_selections: Arc<BTreeSet<SelectedEntry>>,
64}
65
66impl DraggedSelection {
67 pub fn items<'a>(&'a self) -> Box<dyn Iterator<Item = &'a SelectedEntry> + 'a> {
68 if self.marked_selections.contains(&self.active_selection) {
69 Box::new(self.marked_selections.iter())
70 } else {
71 Box::new(std::iter::once(&self.active_selection))
72 }
73 }
74}
75
76#[derive(Clone, Copy, PartialEq, Debug, Deserialize, JsonSchema)]
77#[serde(rename_all = "snake_case")]
78pub enum SaveIntent {
79 /// write all files (even if unchanged)
80 /// prompt before overwriting on-disk changes
81 Save,
82 /// same as Save, but without auto formatting
83 SaveWithoutFormat,
84 /// write any files that have local changes
85 /// prompt before overwriting on-disk changes
86 SaveAll,
87 /// always prompt for a new path
88 SaveAs,
89 /// prompt "you have unsaved changes" before writing
90 Close,
91 /// write all dirty files, don't prompt on conflict
92 Overwrite,
93 /// skip all save-related behavior
94 Skip,
95}
96
97#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default)]
98pub struct ActivateItem(pub usize);
99
100#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default)]
101#[serde(deny_unknown_fields)]
102pub struct CloseActiveItem {
103 pub save_intent: Option<SaveIntent>,
104 #[serde(default)]
105 pub close_pinned: bool,
106}
107
108#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default)]
109#[serde(deny_unknown_fields)]
110pub struct CloseInactiveItems {
111 pub save_intent: Option<SaveIntent>,
112 #[serde(default)]
113 pub close_pinned: bool,
114}
115
116#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default)]
117#[serde(deny_unknown_fields)]
118pub struct CloseAllItems {
119 pub save_intent: Option<SaveIntent>,
120 #[serde(default)]
121 pub close_pinned: bool,
122}
123
124#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default)]
125#[serde(deny_unknown_fields)]
126pub struct CloseCleanItems {
127 #[serde(default)]
128 pub close_pinned: bool,
129}
130
131#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default)]
132#[serde(deny_unknown_fields)]
133pub struct CloseItemsToTheRight {
134 #[serde(default)]
135 pub close_pinned: bool,
136}
137
138#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default)]
139#[serde(deny_unknown_fields)]
140pub struct CloseItemsToTheLeft {
141 #[serde(default)]
142 pub close_pinned: bool,
143}
144
145#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default)]
146#[serde(deny_unknown_fields)]
147pub struct RevealInProjectPanel {
148 #[serde(skip)]
149 pub entry_id: Option<u64>,
150}
151
152#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default)]
153#[serde(deny_unknown_fields)]
154pub struct DeploySearch {
155 #[serde(default)]
156 pub replace_enabled: bool,
157 #[serde(default)]
158 pub included_files: Option<String>,
159}
160
161impl_actions!(
162 pane,
163 [
164 CloseAllItems,
165 CloseActiveItem,
166 CloseCleanItems,
167 CloseItemsToTheLeft,
168 CloseItemsToTheRight,
169 CloseInactiveItems,
170 ActivateItem,
171 RevealInProjectPanel,
172 DeploySearch,
173 ]
174);
175
176actions!(
177 pane,
178 [
179 ActivatePreviousItem,
180 ActivateNextItem,
181 ActivateLastItem,
182 AlternateFile,
183 GoBack,
184 GoForward,
185 JoinIntoNext,
186 JoinAll,
187 ReopenClosedItem,
188 SplitLeft,
189 SplitUp,
190 SplitRight,
191 SplitDown,
192 SplitHorizontal,
193 SplitVertical,
194 SwapItemLeft,
195 SwapItemRight,
196 TogglePreviewTab,
197 TogglePinTab,
198 ]
199);
200
201impl DeploySearch {
202 pub fn find() -> Self {
203 Self {
204 replace_enabled: false,
205 included_files: None,
206 }
207 }
208}
209
210const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
211
212pub enum Event {
213 AddItem {
214 item: Box<dyn ItemHandle>,
215 },
216 ActivateItem {
217 local: bool,
218 focus_changed: bool,
219 },
220 Remove {
221 focus_on_pane: Option<Entity<Pane>>,
222 },
223 RemoveItem {
224 idx: usize,
225 },
226 RemovedItem {
227 item: Box<dyn ItemHandle>,
228 },
229 Split(SplitDirection),
230 JoinAll,
231 JoinIntoNext,
232 ChangeItemTitle,
233 Focus,
234 ZoomIn,
235 ZoomOut,
236 UserSavedItem {
237 item: Box<dyn WeakItemHandle>,
238 save_intent: SaveIntent,
239 },
240}
241
242impl fmt::Debug for Event {
243 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
244 match self {
245 Event::AddItem { item } => f
246 .debug_struct("AddItem")
247 .field("item", &item.item_id())
248 .finish(),
249 Event::ActivateItem { local, .. } => f
250 .debug_struct("ActivateItem")
251 .field("local", local)
252 .finish(),
253 Event::Remove { .. } => f.write_str("Remove"),
254 Event::RemoveItem { idx } => f.debug_struct("RemoveItem").field("idx", idx).finish(),
255 Event::RemovedItem { item } => f
256 .debug_struct("RemovedItem")
257 .field("item", &item.item_id())
258 .finish(),
259 Event::Split(direction) => f
260 .debug_struct("Split")
261 .field("direction", direction)
262 .finish(),
263 Event::JoinAll => f.write_str("JoinAll"),
264 Event::JoinIntoNext => f.write_str("JoinIntoNext"),
265 Event::ChangeItemTitle => f.write_str("ChangeItemTitle"),
266 Event::Focus => f.write_str("Focus"),
267 Event::ZoomIn => f.write_str("ZoomIn"),
268 Event::ZoomOut => f.write_str("ZoomOut"),
269 Event::UserSavedItem { item, save_intent } => f
270 .debug_struct("UserSavedItem")
271 .field("item", &item.id())
272 .field("save_intent", save_intent)
273 .finish(),
274 }
275 }
276}
277
278/// A container for 0 to many items that are open in the workspace.
279/// Treats all items uniformly via the [`ItemHandle`] trait, whether it's an editor, search results multibuffer, terminal or something else,
280/// responsible for managing item tabs, focus and zoom states and drag and drop features.
281/// Can be split, see `PaneGroup` for more details.
282pub struct Pane {
283 alternate_file_items: (
284 Option<Box<dyn WeakItemHandle>>,
285 Option<Box<dyn WeakItemHandle>>,
286 ),
287 focus_handle: FocusHandle,
288 items: Vec<Box<dyn ItemHandle>>,
289 activation_history: Vec<ActivationHistoryEntry>,
290 next_activation_timestamp: Arc<AtomicUsize>,
291 zoomed: bool,
292 was_focused: bool,
293 active_item_index: usize,
294 preview_item_id: Option<EntityId>,
295 last_focus_handle_by_item: HashMap<EntityId, WeakFocusHandle>,
296 nav_history: NavHistory,
297 toolbar: Entity<Toolbar>,
298 pub(crate) workspace: WeakEntity<Workspace>,
299 project: WeakEntity<Project>,
300 pub drag_split_direction: Option<SplitDirection>,
301 can_drop_predicate: Option<Arc<dyn Fn(&dyn Any, &mut Window, &mut App) -> bool>>,
302 custom_drop_handle: Option<
303 Arc<dyn Fn(&mut Pane, &dyn Any, &mut Window, &mut Context<Pane>) -> ControlFlow<(), ()>>,
304 >,
305 can_split_predicate:
306 Option<Arc<dyn Fn(&mut Self, &dyn Any, &mut Window, &mut Context<Self>) -> bool>>,
307 should_display_tab_bar: Rc<dyn Fn(&Window, &mut Context<Pane>) -> bool>,
308 render_tab_bar_buttons: Rc<
309 dyn Fn(
310 &mut Pane,
311 &mut Window,
312 &mut Context<Pane>,
313 ) -> (Option<AnyElement>, Option<AnyElement>),
314 >,
315 render_tab_bar: Rc<dyn Fn(&mut Pane, &mut Window, &mut Context<Pane>) -> AnyElement>,
316 show_tab_bar_buttons: bool,
317 _subscriptions: Vec<Subscription>,
318 tab_bar_scroll_handle: ScrollHandle,
319 /// Is None if navigation buttons are permanently turned off (and should not react to setting changes).
320 /// Otherwise, when `display_nav_history_buttons` is Some, it determines whether nav buttons should be displayed.
321 display_nav_history_buttons: Option<bool>,
322 double_click_dispatch_action: Box<dyn Action>,
323 save_modals_spawned: HashSet<EntityId>,
324 close_pane_if_empty: bool,
325 pub new_item_context_menu_handle: PopoverMenuHandle<ContextMenu>,
326 pub split_item_context_menu_handle: PopoverMenuHandle<ContextMenu>,
327 pinned_tab_count: usize,
328 diagnostics: HashMap<ProjectPath, DiagnosticSeverity>,
329 zoom_out_on_close: bool,
330 /// If a certain project item wants to get recreated with specific data, it can persist its data before the recreation here.
331 pub project_item_restoration_data: HashMap<ProjectItemKind, Box<dyn Any + Send>>,
332}
333
334pub struct ActivationHistoryEntry {
335 pub entity_id: EntityId,
336 pub timestamp: usize,
337}
338
339pub struct ItemNavHistory {
340 history: NavHistory,
341 item: Arc<dyn WeakItemHandle>,
342 is_preview: bool,
343}
344
345#[derive(Clone)]
346pub struct NavHistory(Arc<Mutex<NavHistoryState>>);
347
348struct NavHistoryState {
349 mode: NavigationMode,
350 backward_stack: VecDeque<NavigationEntry>,
351 forward_stack: VecDeque<NavigationEntry>,
352 closed_stack: VecDeque<NavigationEntry>,
353 paths_by_item: HashMap<EntityId, (ProjectPath, Option<PathBuf>)>,
354 pane: WeakEntity<Pane>,
355 next_timestamp: Arc<AtomicUsize>,
356}
357
358#[derive(Debug, Copy, Clone)]
359pub enum NavigationMode {
360 Normal,
361 GoingBack,
362 GoingForward,
363 ClosingItem,
364 ReopeningClosedItem,
365 Disabled,
366}
367
368impl Default for NavigationMode {
369 fn default() -> Self {
370 Self::Normal
371 }
372}
373
374pub struct NavigationEntry {
375 pub item: Arc<dyn WeakItemHandle>,
376 pub data: Option<Box<dyn Any + Send>>,
377 pub timestamp: usize,
378 pub is_preview: bool,
379}
380
381#[derive(Clone)]
382pub struct DraggedTab {
383 pub pane: Entity<Pane>,
384 pub item: Box<dyn ItemHandle>,
385 pub ix: usize,
386 pub detail: usize,
387 pub is_active: bool,
388}
389
390impl EventEmitter<Event> for Pane {}
391
392impl Pane {
393 pub fn new(
394 workspace: WeakEntity<Workspace>,
395 project: Entity<Project>,
396 next_timestamp: Arc<AtomicUsize>,
397 can_drop_predicate: Option<Arc<dyn Fn(&dyn Any, &mut Window, &mut App) -> bool + 'static>>,
398 double_click_dispatch_action: Box<dyn Action>,
399 window: &mut Window,
400 cx: &mut Context<Self>,
401 ) -> Self {
402 let focus_handle = cx.focus_handle();
403
404 let subscriptions = vec![
405 cx.on_focus(&focus_handle, window, Pane::focus_in),
406 cx.on_focus_in(&focus_handle, window, Pane::focus_in),
407 cx.on_focus_out(&focus_handle, window, Pane::focus_out),
408 cx.observe_global::<SettingsStore>(Self::settings_changed),
409 cx.subscribe(&project, Self::project_events),
410 ];
411
412 let handle = cx.entity().downgrade();
413 Self {
414 alternate_file_items: (None, None),
415 focus_handle,
416 items: Vec::new(),
417 activation_history: Vec::new(),
418 next_activation_timestamp: next_timestamp.clone(),
419 was_focused: false,
420 zoomed: false,
421 active_item_index: 0,
422 preview_item_id: None,
423 last_focus_handle_by_item: Default::default(),
424 nav_history: NavHistory(Arc::new(Mutex::new(NavHistoryState {
425 mode: NavigationMode::Normal,
426 backward_stack: Default::default(),
427 forward_stack: Default::default(),
428 closed_stack: Default::default(),
429 paths_by_item: Default::default(),
430 pane: handle.clone(),
431 next_timestamp,
432 }))),
433 toolbar: cx.new(|_| Toolbar::new()),
434 tab_bar_scroll_handle: ScrollHandle::new(),
435 drag_split_direction: None,
436 workspace,
437 project: project.downgrade(),
438 can_drop_predicate,
439 custom_drop_handle: None,
440 can_split_predicate: None,
441 should_display_tab_bar: Rc::new(|_, cx| TabBarSettings::get_global(cx).show),
442 render_tab_bar_buttons: Rc::new(default_render_tab_bar_buttons),
443 render_tab_bar: Rc::new(Self::render_tab_bar),
444 show_tab_bar_buttons: TabBarSettings::get_global(cx).show_tab_bar_buttons,
445 display_nav_history_buttons: Some(
446 TabBarSettings::get_global(cx).show_nav_history_buttons,
447 ),
448 _subscriptions: subscriptions,
449 double_click_dispatch_action,
450 save_modals_spawned: HashSet::default(),
451 close_pane_if_empty: true,
452 split_item_context_menu_handle: Default::default(),
453 new_item_context_menu_handle: Default::default(),
454 pinned_tab_count: 0,
455 diagnostics: Default::default(),
456 zoom_out_on_close: true,
457 project_item_restoration_data: HashMap::default(),
458 }
459 }
460
461 fn alternate_file(&mut self, window: &mut Window, cx: &mut Context<Pane>) {
462 let (_, alternative) = &self.alternate_file_items;
463 if let Some(alternative) = alternative {
464 let existing = self
465 .items()
466 .find_position(|item| item.item_id() == alternative.id());
467 if let Some((ix, _)) = existing {
468 self.activate_item(ix, true, true, window, cx);
469 } else if let Some(upgraded) = alternative.upgrade() {
470 self.add_item(upgraded, true, true, None, window, cx);
471 }
472 }
473 }
474
475 pub fn track_alternate_file_items(&mut self) {
476 if let Some(item) = self.active_item().map(|item| item.downgrade_item()) {
477 let (current, _) = &self.alternate_file_items;
478 match current {
479 Some(current) => {
480 if current.id() != item.id() {
481 self.alternate_file_items =
482 (Some(item), self.alternate_file_items.0.take());
483 }
484 }
485 None => {
486 self.alternate_file_items = (Some(item), None);
487 }
488 }
489 }
490 }
491
492 pub fn has_focus(&self, window: &Window, cx: &App) -> bool {
493 // We not only check whether our focus handle contains focus, but also
494 // whether the active item might have focus, because we might have just activated an item
495 // that hasn't rendered yet.
496 // Before the next render, we might transfer focus
497 // to the item, and `focus_handle.contains_focus` returns false because the `active_item`
498 // is not hooked up to us in the dispatch tree.
499 self.focus_handle.contains_focused(window, cx)
500 || self.active_item().map_or(false, |item| {
501 item.item_focus_handle(cx).contains_focused(window, cx)
502 })
503 }
504
505 fn focus_in(&mut self, window: &mut Window, cx: &mut Context<Self>) {
506 if !self.was_focused {
507 self.was_focused = true;
508 self.update_history(self.active_item_index);
509 cx.emit(Event::Focus);
510 cx.notify();
511 }
512
513 self.toolbar.update(cx, |toolbar, cx| {
514 toolbar.focus_changed(true, window, cx);
515 });
516
517 if let Some(active_item) = self.active_item() {
518 if self.focus_handle.is_focused(window) {
519 // Schedule a redraw next frame, so that the focus changes below take effect
520 cx.on_next_frame(window, |_, _, cx| {
521 cx.notify();
522 });
523
524 // Pane was focused directly. We need to either focus a view inside the active item,
525 // or focus the active item itself
526 if let Some(weak_last_focus_handle) =
527 self.last_focus_handle_by_item.get(&active_item.item_id())
528 {
529 if let Some(focus_handle) = weak_last_focus_handle.upgrade() {
530 focus_handle.focus(window);
531 return;
532 }
533 }
534
535 active_item.item_focus_handle(cx).focus(window);
536 } else if let Some(focused) = window.focused(cx) {
537 if !self.context_menu_focused(window, cx) {
538 self.last_focus_handle_by_item
539 .insert(active_item.item_id(), focused.downgrade());
540 }
541 }
542 }
543 }
544
545 pub fn context_menu_focused(&self, window: &mut Window, cx: &mut Context<Self>) -> bool {
546 self.new_item_context_menu_handle.is_focused(window, cx)
547 || self.split_item_context_menu_handle.is_focused(window, cx)
548 }
549
550 fn focus_out(&mut self, _event: FocusOutEvent, window: &mut Window, cx: &mut Context<Self>) {
551 self.was_focused = false;
552 self.toolbar.update(cx, |toolbar, cx| {
553 toolbar.focus_changed(false, window, cx);
554 });
555 cx.notify();
556 }
557
558 fn project_events(
559 &mut self,
560 _project: Entity<Project>,
561 event: &project::Event,
562 cx: &mut Context<Self>,
563 ) {
564 match event {
565 project::Event::DiskBasedDiagnosticsFinished { .. }
566 | project::Event::DiagnosticsUpdated { .. } => {
567 if ItemSettings::get_global(cx).show_diagnostics != ShowDiagnostics::Off {
568 self.update_diagnostics(cx);
569 cx.notify();
570 }
571 }
572 _ => {}
573 }
574 }
575
576 fn update_diagnostics(&mut self, cx: &mut Context<Self>) {
577 let Some(project) = self.project.upgrade() else {
578 return;
579 };
580 let show_diagnostics = ItemSettings::get_global(cx).show_diagnostics;
581 self.diagnostics = if show_diagnostics != ShowDiagnostics::Off {
582 project
583 .read(cx)
584 .diagnostic_summaries(false, cx)
585 .filter_map(|(project_path, _, diagnostic_summary)| {
586 if diagnostic_summary.error_count > 0 {
587 Some((project_path, DiagnosticSeverity::ERROR))
588 } else if diagnostic_summary.warning_count > 0
589 && show_diagnostics != ShowDiagnostics::Errors
590 {
591 Some((project_path, DiagnosticSeverity::WARNING))
592 } else {
593 None
594 }
595 })
596 .collect()
597 } else {
598 HashMap::default()
599 }
600 }
601
602 fn settings_changed(&mut self, cx: &mut Context<Self>) {
603 let tab_bar_settings = TabBarSettings::get_global(cx);
604
605 if let Some(display_nav_history_buttons) = self.display_nav_history_buttons.as_mut() {
606 *display_nav_history_buttons = tab_bar_settings.show_nav_history_buttons;
607 }
608 self.show_tab_bar_buttons = tab_bar_settings.show_tab_bar_buttons;
609
610 if !PreviewTabsSettings::get_global(cx).enabled {
611 self.preview_item_id = None;
612 }
613 self.update_diagnostics(cx);
614 cx.notify();
615 }
616
617 pub fn active_item_index(&self) -> usize {
618 self.active_item_index
619 }
620
621 pub fn activation_history(&self) -> &[ActivationHistoryEntry] {
622 &self.activation_history
623 }
624
625 pub fn set_should_display_tab_bar<F>(&mut self, should_display_tab_bar: F)
626 where
627 F: 'static + Fn(&Window, &mut Context<Pane>) -> bool,
628 {
629 self.should_display_tab_bar = Rc::new(should_display_tab_bar);
630 }
631
632 pub fn set_can_split(
633 &mut self,
634 can_split_predicate: Option<
635 Arc<dyn Fn(&mut Self, &dyn Any, &mut Window, &mut Context<Self>) -> bool + 'static>,
636 >,
637 ) {
638 self.can_split_predicate = can_split_predicate;
639 }
640
641 pub fn set_close_pane_if_empty(&mut self, close_pane_if_empty: bool, cx: &mut Context<Self>) {
642 self.close_pane_if_empty = close_pane_if_empty;
643 cx.notify();
644 }
645
646 pub fn set_can_navigate(&mut self, can_navigate: bool, cx: &mut Context<Self>) {
647 self.toolbar.update(cx, |toolbar, cx| {
648 toolbar.set_can_navigate(can_navigate, cx);
649 });
650 cx.notify();
651 }
652
653 pub fn set_render_tab_bar<F>(&mut self, cx: &mut Context<Self>, render: F)
654 where
655 F: 'static + Fn(&mut Pane, &mut Window, &mut Context<Pane>) -> AnyElement,
656 {
657 self.render_tab_bar = Rc::new(render);
658 cx.notify();
659 }
660
661 pub fn set_render_tab_bar_buttons<F>(&mut self, cx: &mut Context<Self>, render: F)
662 where
663 F: 'static
664 + Fn(
665 &mut Pane,
666 &mut Window,
667 &mut Context<Pane>,
668 ) -> (Option<AnyElement>, Option<AnyElement>),
669 {
670 self.render_tab_bar_buttons = Rc::new(render);
671 cx.notify();
672 }
673
674 pub fn set_custom_drop_handle<F>(&mut self, cx: &mut Context<Self>, handle: F)
675 where
676 F: 'static
677 + Fn(&mut Pane, &dyn Any, &mut Window, &mut Context<Pane>) -> ControlFlow<(), ()>,
678 {
679 self.custom_drop_handle = Some(Arc::new(handle));
680 cx.notify();
681 }
682
683 pub fn nav_history_for_item<T: Item>(&self, item: &Entity<T>) -> ItemNavHistory {
684 ItemNavHistory {
685 history: self.nav_history.clone(),
686 item: Arc::new(item.downgrade()),
687 is_preview: self.preview_item_id == Some(item.item_id()),
688 }
689 }
690
691 pub fn nav_history(&self) -> &NavHistory {
692 &self.nav_history
693 }
694
695 pub fn nav_history_mut(&mut self) -> &mut NavHistory {
696 &mut self.nav_history
697 }
698
699 pub fn disable_history(&mut self) {
700 self.nav_history.disable();
701 }
702
703 pub fn enable_history(&mut self) {
704 self.nav_history.enable();
705 }
706
707 pub fn can_navigate_backward(&self) -> bool {
708 !self.nav_history.0.lock().backward_stack.is_empty()
709 }
710
711 pub fn can_navigate_forward(&self) -> bool {
712 !self.nav_history.0.lock().forward_stack.is_empty()
713 }
714
715 pub fn navigate_backward(&mut self, window: &mut Window, cx: &mut Context<Self>) {
716 if let Some(workspace) = self.workspace.upgrade() {
717 let pane = cx.entity().downgrade();
718 window.defer(cx, move |window, cx| {
719 workspace.update(cx, |workspace, cx| {
720 workspace.go_back(pane, window, cx).detach_and_log_err(cx)
721 })
722 })
723 }
724 }
725
726 fn navigate_forward(&mut self, window: &mut Window, cx: &mut Context<Self>) {
727 if let Some(workspace) = self.workspace.upgrade() {
728 let pane = cx.entity().downgrade();
729 window.defer(cx, move |window, cx| {
730 workspace.update(cx, |workspace, cx| {
731 workspace
732 .go_forward(pane, window, cx)
733 .detach_and_log_err(cx)
734 })
735 })
736 }
737 }
738
739 fn history_updated(&mut self, cx: &mut Context<Self>) {
740 self.toolbar.update(cx, |_, cx| cx.notify());
741 }
742
743 pub fn preview_item_id(&self) -> Option<EntityId> {
744 self.preview_item_id
745 }
746
747 pub fn preview_item(&self) -> Option<Box<dyn ItemHandle>> {
748 self.preview_item_id
749 .and_then(|id| self.items.iter().find(|item| item.item_id() == id))
750 .cloned()
751 }
752
753 pub fn preview_item_idx(&self) -> Option<usize> {
754 if let Some(preview_item_id) = self.preview_item_id {
755 self.items
756 .iter()
757 .position(|item| item.item_id() == preview_item_id)
758 } else {
759 None
760 }
761 }
762
763 pub fn is_active_preview_item(&self, item_id: EntityId) -> bool {
764 self.preview_item_id == Some(item_id)
765 }
766
767 /// Marks the item with the given ID as the preview item.
768 /// This will be ignored if the global setting `preview_tabs` is disabled.
769 pub fn set_preview_item_id(&mut self, item_id: Option<EntityId>, cx: &App) {
770 if PreviewTabsSettings::get_global(cx).enabled {
771 self.preview_item_id = item_id;
772 }
773 }
774
775 pub(crate) fn set_pinned_count(&mut self, count: usize) {
776 self.pinned_tab_count = count;
777 }
778
779 pub(crate) fn pinned_count(&self) -> usize {
780 self.pinned_tab_count
781 }
782
783 pub fn handle_item_edit(&mut self, item_id: EntityId, cx: &App) {
784 if let Some(preview_item) = self.preview_item() {
785 if preview_item.item_id() == item_id && !preview_item.preserve_preview(cx) {
786 self.set_preview_item_id(None, cx);
787 }
788 }
789 }
790
791 pub(crate) fn open_item(
792 &mut self,
793 project_entry_id: Option<ProjectEntryId>,
794 focus_item: bool,
795 allow_preview: bool,
796 activate: bool,
797 suggested_position: Option<usize>,
798 window: &mut Window,
799 cx: &mut Context<Self>,
800 build_item: WorkspaceItemBuilder,
801 ) -> Box<dyn ItemHandle> {
802 let mut existing_item = None;
803 if let Some(project_entry_id) = project_entry_id {
804 for (index, item) in self.items.iter().enumerate() {
805 if item.is_singleton(cx)
806 && item.project_entry_ids(cx).as_slice() == [project_entry_id]
807 {
808 let item = item.boxed_clone();
809 existing_item = Some((index, item));
810 break;
811 }
812 }
813 }
814 if let Some((index, existing_item)) = existing_item {
815 // If the item is already open, and the item is a preview item
816 // and we are not allowing items to open as preview, mark the item as persistent.
817 if let Some(preview_item_id) = self.preview_item_id {
818 if let Some(tab) = self.items.get(index) {
819 if tab.item_id() == preview_item_id && !allow_preview {
820 self.set_preview_item_id(None, cx);
821 }
822 }
823 }
824 if activate {
825 self.activate_item(index, focus_item, focus_item, window, cx);
826 }
827 existing_item
828 } else {
829 // If the item is being opened as preview and we have an existing preview tab,
830 // open the new item in the position of the existing preview tab.
831 let destination_index = if allow_preview {
832 self.close_current_preview_item(window, cx)
833 } else {
834 suggested_position
835 };
836
837 let new_item = build_item(self, window, cx);
838
839 if allow_preview {
840 self.set_preview_item_id(Some(new_item.item_id()), cx);
841 }
842 self.add_item_inner(
843 new_item.clone(),
844 true,
845 focus_item,
846 activate,
847 destination_index,
848 window,
849 cx,
850 );
851
852 new_item
853 }
854 }
855
856 pub fn close_current_preview_item(
857 &mut self,
858 window: &mut Window,
859 cx: &mut Context<Self>,
860 ) -> Option<usize> {
861 let item_idx = self.preview_item_idx()?;
862 let id = self.preview_item_id()?;
863
864 let prev_active_item_index = self.active_item_index;
865 self.remove_item(id, false, false, window, cx);
866 self.active_item_index = prev_active_item_index;
867
868 if item_idx < self.items.len() {
869 Some(item_idx)
870 } else {
871 None
872 }
873 }
874
875 pub fn add_item_inner(
876 &mut self,
877 item: Box<dyn ItemHandle>,
878 activate_pane: bool,
879 focus_item: bool,
880 activate: bool,
881 destination_index: Option<usize>,
882 window: &mut Window,
883 cx: &mut Context<Self>,
884 ) {
885 self.close_items_over_max_tabs(window, cx);
886
887 if item.is_singleton(cx) {
888 if let Some(&entry_id) = item.project_entry_ids(cx).first() {
889 let Some(project) = self.project.upgrade() else {
890 return;
891 };
892 let project = project.read(cx);
893 if let Some(project_path) = project.path_for_entry(entry_id, cx) {
894 let abs_path = project.absolute_path(&project_path, cx);
895 self.nav_history
896 .0
897 .lock()
898 .paths_by_item
899 .insert(item.item_id(), (project_path, abs_path));
900 }
901 }
902 }
903 // If no destination index is specified, add or move the item after the
904 // active item (or at the start of tab bar, if the active item is pinned)
905 let mut insertion_index = {
906 cmp::min(
907 if let Some(destination_index) = destination_index {
908 destination_index
909 } else {
910 cmp::max(self.active_item_index + 1, self.pinned_count())
911 },
912 self.items.len(),
913 )
914 };
915
916 // Does the item already exist?
917 let project_entry_id = if item.is_singleton(cx) {
918 item.project_entry_ids(cx).first().copied()
919 } else {
920 None
921 };
922
923 let existing_item_index = self.items.iter().position(|existing_item| {
924 if existing_item.item_id() == item.item_id() {
925 true
926 } else if existing_item.is_singleton(cx) {
927 existing_item
928 .project_entry_ids(cx)
929 .first()
930 .map_or(false, |existing_entry_id| {
931 Some(existing_entry_id) == project_entry_id.as_ref()
932 })
933 } else {
934 false
935 }
936 });
937
938 if let Some(existing_item_index) = existing_item_index {
939 // If the item already exists, move it to the desired destination and activate it
940
941 if existing_item_index != insertion_index {
942 let existing_item_is_active = existing_item_index == self.active_item_index;
943
944 // If the caller didn't specify a destination and the added item is already
945 // the active one, don't move it
946 if existing_item_is_active && destination_index.is_none() {
947 insertion_index = existing_item_index;
948 } else {
949 self.items.remove(existing_item_index);
950 if existing_item_index < self.active_item_index {
951 self.active_item_index -= 1;
952 }
953 insertion_index = insertion_index.min(self.items.len());
954
955 self.items.insert(insertion_index, item.clone());
956
957 if existing_item_is_active {
958 self.active_item_index = insertion_index;
959 } else if insertion_index <= self.active_item_index {
960 self.active_item_index += 1;
961 }
962 }
963
964 cx.notify();
965 }
966
967 if activate {
968 self.activate_item(insertion_index, activate_pane, focus_item, window, cx);
969 }
970 } else {
971 self.items.insert(insertion_index, item.clone());
972
973 if activate {
974 if insertion_index <= self.active_item_index
975 && self.preview_item_idx() != Some(self.active_item_index)
976 {
977 self.active_item_index += 1;
978 }
979
980 self.activate_item(insertion_index, activate_pane, focus_item, window, cx);
981 }
982 cx.notify();
983 }
984
985 cx.emit(Event::AddItem { item });
986 }
987
988 pub fn add_item(
989 &mut self,
990 item: Box<dyn ItemHandle>,
991 activate_pane: bool,
992 focus_item: bool,
993 destination_index: Option<usize>,
994 window: &mut Window,
995 cx: &mut Context<Self>,
996 ) {
997 self.add_item_inner(
998 item,
999 activate_pane,
1000 focus_item,
1001 true,
1002 destination_index,
1003 window,
1004 cx,
1005 )
1006 }
1007
1008 pub fn items_len(&self) -> usize {
1009 self.items.len()
1010 }
1011
1012 pub fn items(&self) -> impl DoubleEndedIterator<Item = &Box<dyn ItemHandle>> {
1013 self.items.iter()
1014 }
1015
1016 pub fn items_of_type<T: Render>(&self) -> impl '_ + Iterator<Item = Entity<T>> {
1017 self.items
1018 .iter()
1019 .filter_map(|item| item.to_any().downcast().ok())
1020 }
1021
1022 pub fn active_item(&self) -> Option<Box<dyn ItemHandle>> {
1023 self.items.get(self.active_item_index).cloned()
1024 }
1025
1026 pub fn pixel_position_of_cursor(&self, cx: &App) -> Option<Point<Pixels>> {
1027 self.items
1028 .get(self.active_item_index)?
1029 .pixel_position_of_cursor(cx)
1030 }
1031
1032 pub fn item_for_entry(
1033 &self,
1034 entry_id: ProjectEntryId,
1035 cx: &App,
1036 ) -> Option<Box<dyn ItemHandle>> {
1037 self.items.iter().find_map(|item| {
1038 if item.is_singleton(cx) && (item.project_entry_ids(cx).as_slice() == [entry_id]) {
1039 Some(item.boxed_clone())
1040 } else {
1041 None
1042 }
1043 })
1044 }
1045
1046 pub fn item_for_path(
1047 &self,
1048 project_path: ProjectPath,
1049 cx: &App,
1050 ) -> Option<Box<dyn ItemHandle>> {
1051 self.items.iter().find_map(move |item| {
1052 if item.is_singleton(cx) && (item.project_path(cx).as_slice() == [project_path.clone()])
1053 {
1054 Some(item.boxed_clone())
1055 } else {
1056 None
1057 }
1058 })
1059 }
1060
1061 pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option<usize> {
1062 self.index_for_item_id(item.item_id())
1063 }
1064
1065 fn index_for_item_id(&self, item_id: EntityId) -> Option<usize> {
1066 self.items.iter().position(|i| i.item_id() == item_id)
1067 }
1068
1069 pub fn item_for_index(&self, ix: usize) -> Option<&dyn ItemHandle> {
1070 self.items.get(ix).map(|i| i.as_ref())
1071 }
1072
1073 pub fn toggle_zoom(&mut self, _: &ToggleZoom, window: &mut Window, cx: &mut Context<Self>) {
1074 if self.zoomed {
1075 cx.emit(Event::ZoomOut);
1076 } else if !self.items.is_empty() {
1077 if !self.focus_handle.contains_focused(window, cx) {
1078 cx.focus_self(window);
1079 }
1080 cx.emit(Event::ZoomIn);
1081 }
1082 }
1083
1084 pub fn activate_item(
1085 &mut self,
1086 index: usize,
1087 activate_pane: bool,
1088 focus_item: bool,
1089 window: &mut Window,
1090 cx: &mut Context<Self>,
1091 ) {
1092 use NavigationMode::{GoingBack, GoingForward};
1093 if index < self.items.len() {
1094 let prev_active_item_ix = mem::replace(&mut self.active_item_index, index);
1095 if prev_active_item_ix != self.active_item_index
1096 || matches!(self.nav_history.mode(), GoingBack | GoingForward)
1097 {
1098 if let Some(prev_item) = self.items.get(prev_active_item_ix) {
1099 prev_item.deactivated(window, cx);
1100 }
1101 }
1102 self.update_history(index);
1103 self.update_toolbar(window, cx);
1104 self.update_status_bar(window, cx);
1105
1106 if focus_item {
1107 self.focus_active_item(window, cx);
1108 }
1109
1110 cx.emit(Event::ActivateItem {
1111 local: activate_pane,
1112 focus_changed: focus_item,
1113 });
1114
1115 if !self.is_tab_pinned(index) {
1116 self.tab_bar_scroll_handle
1117 .scroll_to_item(index - self.pinned_tab_count);
1118 }
1119
1120 cx.notify();
1121 }
1122 }
1123
1124 fn update_history(&mut self, index: usize) {
1125 if let Some(newly_active_item) = self.items.get(index) {
1126 self.activation_history
1127 .retain(|entry| entry.entity_id != newly_active_item.item_id());
1128 self.activation_history.push(ActivationHistoryEntry {
1129 entity_id: newly_active_item.item_id(),
1130 timestamp: self
1131 .next_activation_timestamp
1132 .fetch_add(1, Ordering::SeqCst),
1133 });
1134 }
1135 }
1136
1137 pub fn activate_prev_item(
1138 &mut self,
1139 activate_pane: bool,
1140 window: &mut Window,
1141 cx: &mut Context<Self>,
1142 ) {
1143 let mut index = self.active_item_index;
1144 if index > 0 {
1145 index -= 1;
1146 } else if !self.items.is_empty() {
1147 index = self.items.len() - 1;
1148 }
1149 self.activate_item(index, activate_pane, activate_pane, window, cx);
1150 }
1151
1152 pub fn activate_next_item(
1153 &mut self,
1154 activate_pane: bool,
1155 window: &mut Window,
1156 cx: &mut Context<Self>,
1157 ) {
1158 let mut index = self.active_item_index;
1159 if index + 1 < self.items.len() {
1160 index += 1;
1161 } else {
1162 index = 0;
1163 }
1164 self.activate_item(index, activate_pane, activate_pane, window, cx);
1165 }
1166
1167 pub fn swap_item_left(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1168 let index = self.active_item_index;
1169 if index == 0 {
1170 return;
1171 }
1172
1173 self.items.swap(index, index - 1);
1174 self.activate_item(index - 1, true, true, window, cx);
1175 }
1176
1177 pub fn swap_item_right(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1178 let index = self.active_item_index;
1179 if index + 1 == self.items.len() {
1180 return;
1181 }
1182
1183 self.items.swap(index, index + 1);
1184 self.activate_item(index + 1, true, true, window, cx);
1185 }
1186
1187 pub fn close_active_item(
1188 &mut self,
1189 action: &CloseActiveItem,
1190 window: &mut Window,
1191 cx: &mut Context<Self>,
1192 ) -> Option<Task<Result<()>>> {
1193 if self.items.is_empty() {
1194 // Close the window when there's no active items to close, if configured
1195 if WorkspaceSettings::get_global(cx)
1196 .when_closing_with_no_tabs
1197 .should_close()
1198 {
1199 window.dispatch_action(Box::new(CloseWindow), cx);
1200 }
1201
1202 return None;
1203 }
1204 if self.is_tab_pinned(self.active_item_index) && !action.close_pinned {
1205 // Activate any non-pinned tab in same pane
1206 let non_pinned_tab_index = self
1207 .items()
1208 .enumerate()
1209 .find(|(index, _item)| !self.is_tab_pinned(*index))
1210 .map(|(index, _item)| index);
1211 if let Some(index) = non_pinned_tab_index {
1212 self.activate_item(index, false, false, window, cx);
1213 return None;
1214 }
1215
1216 // Activate any non-pinned tab in different pane
1217 let current_pane = cx.entity();
1218 self.workspace
1219 .update(cx, |workspace, cx| {
1220 let panes = workspace.center.panes();
1221 let pane_with_unpinned_tab = panes.iter().find(|pane| {
1222 if **pane == ¤t_pane {
1223 return false;
1224 }
1225 pane.read(cx).has_unpinned_tabs()
1226 });
1227 if let Some(pane) = pane_with_unpinned_tab {
1228 pane.update(cx, |pane, cx| pane.activate_unpinned_tab(window, cx));
1229 }
1230 })
1231 .ok();
1232
1233 return None;
1234 };
1235 let active_item_id = self.items[self.active_item_index].item_id();
1236 Some(self.close_item_by_id(
1237 active_item_id,
1238 action.save_intent.unwrap_or(SaveIntent::Close),
1239 window,
1240 cx,
1241 ))
1242 }
1243
1244 pub fn close_item_by_id(
1245 &mut self,
1246 item_id_to_close: EntityId,
1247 save_intent: SaveIntent,
1248 window: &mut Window,
1249 cx: &mut Context<Self>,
1250 ) -> Task<Result<()>> {
1251 self.close_items(window, cx, save_intent, move |view_id| {
1252 view_id == item_id_to_close
1253 })
1254 }
1255
1256 pub fn close_inactive_items(
1257 &mut self,
1258 action: &CloseInactiveItems,
1259 window: &mut Window,
1260 cx: &mut Context<Self>,
1261 ) -> Option<Task<Result<()>>> {
1262 if self.items.is_empty() {
1263 return None;
1264 }
1265
1266 let active_item_id = self.items[self.active_item_index].item_id();
1267 let non_closeable_items = self.get_non_closeable_item_ids(action.close_pinned);
1268 Some(self.close_items(
1269 window,
1270 cx,
1271 action.save_intent.unwrap_or(SaveIntent::Close),
1272 move |item_id| item_id != active_item_id && !non_closeable_items.contains(&item_id),
1273 ))
1274 }
1275
1276 pub fn close_clean_items(
1277 &mut self,
1278 action: &CloseCleanItems,
1279 window: &mut Window,
1280 cx: &mut Context<Self>,
1281 ) -> Option<Task<Result<()>>> {
1282 let item_ids: Vec<_> = self
1283 .items()
1284 .filter(|item| !item.is_dirty(cx))
1285 .map(|item| item.item_id())
1286 .collect();
1287 let non_closeable_items = self.get_non_closeable_item_ids(action.close_pinned);
1288 Some(
1289 self.close_items(window, cx, SaveIntent::Close, move |item_id| {
1290 item_ids.contains(&item_id) && !non_closeable_items.contains(&item_id)
1291 }),
1292 )
1293 }
1294
1295 pub fn close_items_to_the_left(
1296 &mut self,
1297 action: &CloseItemsToTheLeft,
1298 window: &mut Window,
1299 cx: &mut Context<Self>,
1300 ) -> Option<Task<Result<()>>> {
1301 if self.items.is_empty() {
1302 return None;
1303 }
1304 let active_item_id = self.items[self.active_item_index].item_id();
1305 let non_closeable_items = self.get_non_closeable_item_ids(action.close_pinned);
1306 Some(self.close_items_to_the_left_by_id(
1307 active_item_id,
1308 action,
1309 non_closeable_items,
1310 window,
1311 cx,
1312 ))
1313 }
1314
1315 pub fn close_items_to_the_left_by_id(
1316 &mut self,
1317 item_id: EntityId,
1318 action: &CloseItemsToTheLeft,
1319 non_closeable_items: Vec<EntityId>,
1320 window: &mut Window,
1321 cx: &mut Context<Self>,
1322 ) -> Task<Result<()>> {
1323 let item_ids: Vec<_> = self
1324 .items()
1325 .take_while(|item| item.item_id() != item_id)
1326 .map(|item| item.item_id())
1327 .collect();
1328 self.close_items(window, cx, SaveIntent::Close, move |item_id| {
1329 item_ids.contains(&item_id)
1330 && !action.close_pinned
1331 && !non_closeable_items.contains(&item_id)
1332 })
1333 }
1334
1335 pub fn close_items_to_the_right(
1336 &mut self,
1337 action: &CloseItemsToTheRight,
1338 window: &mut Window,
1339 cx: &mut Context<Self>,
1340 ) -> Option<Task<Result<()>>> {
1341 if self.items.is_empty() {
1342 return None;
1343 }
1344 let active_item_id = self.items[self.active_item_index].item_id();
1345 let non_closeable_items = self.get_non_closeable_item_ids(action.close_pinned);
1346 Some(self.close_items_to_the_right_by_id(
1347 active_item_id,
1348 action,
1349 non_closeable_items,
1350 window,
1351 cx,
1352 ))
1353 }
1354
1355 pub fn close_items_to_the_right_by_id(
1356 &mut self,
1357 item_id: EntityId,
1358 action: &CloseItemsToTheRight,
1359 non_closeable_items: Vec<EntityId>,
1360 window: &mut Window,
1361 cx: &mut Context<Self>,
1362 ) -> Task<Result<()>> {
1363 let item_ids: Vec<_> = self
1364 .items()
1365 .rev()
1366 .take_while(|item| item.item_id() != item_id)
1367 .map(|item| item.item_id())
1368 .collect();
1369 self.close_items(window, cx, SaveIntent::Close, move |item_id| {
1370 item_ids.contains(&item_id)
1371 && !action.close_pinned
1372 && !non_closeable_items.contains(&item_id)
1373 })
1374 }
1375
1376 pub fn close_all_items(
1377 &mut self,
1378 action: &CloseAllItems,
1379 window: &mut Window,
1380 cx: &mut Context<Self>,
1381 ) -> Option<Task<Result<()>>> {
1382 if self.items.is_empty() {
1383 return None;
1384 }
1385
1386 let non_closeable_items = self.get_non_closeable_item_ids(action.close_pinned);
1387 Some(self.close_items(
1388 window,
1389 cx,
1390 action.save_intent.unwrap_or(SaveIntent::Close),
1391 |item_id| !non_closeable_items.contains(&item_id),
1392 ))
1393 }
1394
1395 pub fn close_items_over_max_tabs(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1396 let Some(max_tabs) = WorkspaceSettings::get_global(cx).max_tabs.map(|i| i.get()) else {
1397 return;
1398 };
1399
1400 // Reduce over the activation history to get every dirty items up to max_tabs
1401 // count.
1402 let mut index_list = Vec::new();
1403 let mut items_len = self.items_len();
1404 let mut indexes: HashMap<EntityId, usize> = HashMap::default();
1405 for (index, item) in self.items.iter().enumerate() {
1406 indexes.insert(item.item_id(), index);
1407 }
1408 for entry in self.activation_history.iter() {
1409 if items_len < max_tabs {
1410 break;
1411 }
1412 let Some(&index) = indexes.get(&entry.entity_id) else {
1413 continue;
1414 };
1415 if let Some(true) = self.items.get(index).map(|item| item.is_dirty(cx)) {
1416 continue;
1417 }
1418
1419 index_list.push(index);
1420 items_len -= 1;
1421 }
1422 // The sort and reverse is necessary since we remove items
1423 // using their index position, hence removing from the end
1424 // of the list first to avoid changing indexes.
1425 index_list.sort_unstable();
1426 index_list
1427 .iter()
1428 .rev()
1429 .for_each(|&index| self._remove_item(index, false, false, None, window, cx));
1430 }
1431
1432 // Usually when you close an item that has unsaved changes, we prompt you to
1433 // save it. That said, if you still have the buffer open in a different pane
1434 // we can close this one without fear of losing data.
1435 pub fn skip_save_on_close(item: &dyn ItemHandle, workspace: &Workspace, cx: &App) -> bool {
1436 let mut dirty_project_item_ids = Vec::new();
1437 item.for_each_project_item(cx, &mut |project_item_id, project_item| {
1438 if project_item.is_dirty() {
1439 dirty_project_item_ids.push(project_item_id);
1440 }
1441 });
1442 if dirty_project_item_ids.is_empty() {
1443 if item.is_singleton(cx) && item.is_dirty(cx) {
1444 return false;
1445 }
1446 return true;
1447 }
1448
1449 for open_item in workspace.items(cx) {
1450 if open_item.item_id() == item.item_id() {
1451 continue;
1452 }
1453 if !open_item.is_singleton(cx) {
1454 continue;
1455 }
1456 let other_project_item_ids = open_item.project_item_model_ids(cx);
1457 dirty_project_item_ids.retain(|id| !other_project_item_ids.contains(id));
1458 }
1459 if dirty_project_item_ids.is_empty() {
1460 return true;
1461 }
1462
1463 false
1464 }
1465
1466 pub(super) fn file_names_for_prompt(
1467 items: &mut dyn Iterator<Item = &Box<dyn ItemHandle>>,
1468 cx: &App,
1469 ) -> String {
1470 let mut file_names = BTreeSet::default();
1471 for item in items {
1472 item.for_each_project_item(cx, &mut |_, project_item| {
1473 if !project_item.is_dirty() {
1474 return;
1475 }
1476 let filename = project_item.project_path(cx).and_then(|path| {
1477 path.path
1478 .file_name()
1479 .and_then(|name| name.to_str().map(ToOwned::to_owned))
1480 });
1481 file_names.insert(filename.unwrap_or("untitled".to_string()));
1482 });
1483 }
1484 if file_names.len() > 6 {
1485 format!(
1486 "{}\n.. and {} more",
1487 file_names.iter().take(5).join("\n"),
1488 file_names.len() - 5
1489 )
1490 } else {
1491 file_names.into_iter().join("\n")
1492 }
1493 }
1494
1495 pub fn close_items(
1496 &mut self,
1497 window: &mut Window,
1498 cx: &mut Context<Pane>,
1499 mut save_intent: SaveIntent,
1500 should_close: impl Fn(EntityId) -> bool,
1501 ) -> Task<Result<()>> {
1502 // Find the items to close.
1503 let mut items_to_close = Vec::new();
1504 for item in &self.items {
1505 if should_close(item.item_id()) {
1506 items_to_close.push(item.boxed_clone());
1507 }
1508 }
1509
1510 let active_item_id = self.active_item().map(|item| item.item_id());
1511
1512 items_to_close.sort_by_key(|item| {
1513 let path = item.project_path(cx);
1514 // Put the currently active item at the end, because if the currently active item is not closed last
1515 // closing the currently active item will cause the focus to switch to another item
1516 // This will cause Zed to expand the content of the currently active item
1517 //
1518 // Beyond that sort in order of project path, with untitled files and multibuffers coming last.
1519 (active_item_id == Some(item.item_id()), path.is_none(), path)
1520 });
1521
1522 let workspace = self.workspace.clone();
1523 let Some(project) = self.project.upgrade() else {
1524 return Task::ready(Ok(()));
1525 };
1526 cx.spawn_in(window, async move |pane, cx| {
1527 let dirty_items = workspace.update(cx, |workspace, cx| {
1528 items_to_close
1529 .iter()
1530 .filter(|item| {
1531 item.is_dirty(cx)
1532 && !Self::skip_save_on_close(item.as_ref(), &workspace, cx)
1533 })
1534 .map(|item| item.boxed_clone())
1535 .collect::<Vec<_>>()
1536 })?;
1537
1538 if save_intent == SaveIntent::Close && dirty_items.len() > 1 {
1539 let answer = pane.update_in(cx, |_, window, cx| {
1540 let detail = Self::file_names_for_prompt(&mut dirty_items.iter(), cx);
1541 window.prompt(
1542 PromptLevel::Warning,
1543 "Do you want to save changes to the following files?",
1544 Some(&detail),
1545 &["Save all", "Discard all", "Cancel"],
1546 cx,
1547 )
1548 })?;
1549 match answer.await {
1550 Ok(0) => save_intent = SaveIntent::SaveAll,
1551 Ok(1) => save_intent = SaveIntent::Skip,
1552 Ok(2) => return Ok(()),
1553 _ => {}
1554 }
1555 }
1556
1557 for item_to_close in items_to_close {
1558 let mut should_save = true;
1559 if save_intent == SaveIntent::Close {
1560 workspace.update(cx, |workspace, cx| {
1561 if Self::skip_save_on_close(item_to_close.as_ref(), &workspace, cx) {
1562 should_save = false;
1563 }
1564 })?;
1565 }
1566
1567 if should_save {
1568 if !Self::save_item(project.clone(), &pane, &*item_to_close, save_intent, cx)
1569 .await?
1570 {
1571 break;
1572 }
1573 }
1574
1575 // Remove the item from the pane.
1576 pane.update_in(cx, |pane, window, cx| {
1577 pane.remove_item(
1578 item_to_close.item_id(),
1579 false,
1580 pane.close_pane_if_empty,
1581 window,
1582 cx,
1583 );
1584 })
1585 .ok();
1586 }
1587
1588 pane.update(cx, |_, cx| cx.notify()).ok();
1589 Ok(())
1590 })
1591 }
1592
1593 pub fn remove_item(
1594 &mut self,
1595 item_id: EntityId,
1596 activate_pane: bool,
1597 close_pane_if_empty: bool,
1598 window: &mut Window,
1599 cx: &mut Context<Self>,
1600 ) {
1601 let Some(item_index) = self.index_for_item_id(item_id) else {
1602 return;
1603 };
1604 self._remove_item(
1605 item_index,
1606 activate_pane,
1607 close_pane_if_empty,
1608 None,
1609 window,
1610 cx,
1611 )
1612 }
1613
1614 pub fn remove_item_and_focus_on_pane(
1615 &mut self,
1616 item_index: usize,
1617 activate_pane: bool,
1618 focus_on_pane_if_closed: Entity<Pane>,
1619 window: &mut Window,
1620 cx: &mut Context<Self>,
1621 ) {
1622 self._remove_item(
1623 item_index,
1624 activate_pane,
1625 true,
1626 Some(focus_on_pane_if_closed),
1627 window,
1628 cx,
1629 )
1630 }
1631
1632 fn _remove_item(
1633 &mut self,
1634 item_index: usize,
1635 activate_pane: bool,
1636 close_pane_if_empty: bool,
1637 focus_on_pane_if_closed: Option<Entity<Pane>>,
1638 window: &mut Window,
1639 cx: &mut Context<Self>,
1640 ) {
1641 let activate_on_close = &ItemSettings::get_global(cx).activate_on_close;
1642 self.activation_history
1643 .retain(|entry| entry.entity_id != self.items[item_index].item_id());
1644
1645 if self.is_tab_pinned(item_index) {
1646 self.pinned_tab_count -= 1;
1647 }
1648 if item_index == self.active_item_index {
1649 let left_neighbour_index = || item_index.min(self.items.len()).saturating_sub(1);
1650 let index_to_activate = match activate_on_close {
1651 ActivateOnClose::History => self
1652 .activation_history
1653 .pop()
1654 .and_then(|last_activated_item| {
1655 self.items.iter().enumerate().find_map(|(index, item)| {
1656 (item.item_id() == last_activated_item.entity_id).then_some(index)
1657 })
1658 })
1659 // We didn't have a valid activation history entry, so fallback
1660 // to activating the item to the left
1661 .unwrap_or_else(left_neighbour_index),
1662 ActivateOnClose::Neighbour => {
1663 self.activation_history.pop();
1664 if item_index + 1 < self.items.len() {
1665 item_index + 1
1666 } else {
1667 item_index.saturating_sub(1)
1668 }
1669 }
1670 ActivateOnClose::LeftNeighbour => {
1671 self.activation_history.pop();
1672 left_neighbour_index()
1673 }
1674 };
1675
1676 let should_activate = activate_pane || self.has_focus(window, cx);
1677 if self.items.len() == 1 && should_activate {
1678 self.focus_handle.focus(window);
1679 } else {
1680 self.activate_item(
1681 index_to_activate,
1682 should_activate,
1683 should_activate,
1684 window,
1685 cx,
1686 );
1687 }
1688 }
1689
1690 let item = self.items.remove(item_index);
1691
1692 cx.emit(Event::RemovedItem { item: item.clone() });
1693 if self.items.is_empty() {
1694 item.deactivated(window, cx);
1695 if close_pane_if_empty {
1696 self.update_toolbar(window, cx);
1697 cx.emit(Event::Remove {
1698 focus_on_pane: focus_on_pane_if_closed,
1699 });
1700 }
1701 }
1702
1703 if item_index < self.active_item_index {
1704 self.active_item_index -= 1;
1705 }
1706
1707 let mode = self.nav_history.mode();
1708 self.nav_history.set_mode(NavigationMode::ClosingItem);
1709 item.deactivated(window, cx);
1710 self.nav_history.set_mode(mode);
1711
1712 if self.is_active_preview_item(item.item_id()) {
1713 self.set_preview_item_id(None, cx);
1714 }
1715
1716 if let Some(path) = item.project_path(cx) {
1717 let abs_path = self
1718 .nav_history
1719 .0
1720 .lock()
1721 .paths_by_item
1722 .get(&item.item_id())
1723 .and_then(|(_, abs_path)| abs_path.clone());
1724
1725 self.nav_history
1726 .0
1727 .lock()
1728 .paths_by_item
1729 .insert(item.item_id(), (path, abs_path));
1730 } else {
1731 self.nav_history
1732 .0
1733 .lock()
1734 .paths_by_item
1735 .remove(&item.item_id());
1736 }
1737
1738 if self.zoom_out_on_close && self.items.is_empty() && close_pane_if_empty && self.zoomed {
1739 cx.emit(Event::ZoomOut);
1740 }
1741
1742 cx.notify();
1743 }
1744
1745 pub async fn save_item(
1746 project: Entity<Project>,
1747 pane: &WeakEntity<Pane>,
1748 item: &dyn ItemHandle,
1749 save_intent: SaveIntent,
1750 cx: &mut AsyncWindowContext,
1751 ) -> Result<bool> {
1752 const CONFLICT_MESSAGE: &str = "This file has changed on disk since you started editing it. Do you want to overwrite it?";
1753
1754 const DELETED_MESSAGE: &str = "This file has been deleted on disk since you started editing it. Do you want to recreate it?";
1755
1756 if save_intent == SaveIntent::Skip {
1757 return Ok(true);
1758 }
1759 let Some(item_ix) = pane
1760 .update(cx, |pane, _| pane.index_for_item(item))
1761 .ok()
1762 .flatten()
1763 else {
1764 return Ok(true);
1765 };
1766
1767 let (
1768 mut has_conflict,
1769 mut is_dirty,
1770 mut can_save,
1771 can_save_as,
1772 is_singleton,
1773 has_deleted_file,
1774 ) = cx.update(|_window, cx| {
1775 (
1776 item.has_conflict(cx),
1777 item.is_dirty(cx),
1778 item.can_save(cx),
1779 item.can_save_as(cx),
1780 item.is_singleton(cx),
1781 item.has_deleted_file(cx),
1782 )
1783 })?;
1784
1785 // when saving a single buffer, we ignore whether or not it's dirty.
1786 if save_intent == SaveIntent::Save || save_intent == SaveIntent::SaveWithoutFormat {
1787 is_dirty = true;
1788 }
1789
1790 if save_intent == SaveIntent::SaveAs {
1791 is_dirty = true;
1792 has_conflict = false;
1793 can_save = false;
1794 }
1795
1796 if save_intent == SaveIntent::Overwrite {
1797 has_conflict = false;
1798 }
1799
1800 let should_format = save_intent != SaveIntent::SaveWithoutFormat;
1801
1802 if has_conflict && can_save {
1803 if has_deleted_file && is_singleton {
1804 let answer = pane.update_in(cx, |pane, window, cx| {
1805 pane.activate_item(item_ix, true, true, window, cx);
1806 window.prompt(
1807 PromptLevel::Warning,
1808 DELETED_MESSAGE,
1809 None,
1810 &["Save", "Close", "Cancel"],
1811 cx,
1812 )
1813 })?;
1814 match answer.await {
1815 Ok(0) => {
1816 pane.update_in(cx, |_, window, cx| {
1817 item.save(should_format, project, window, cx)
1818 })?
1819 .await?
1820 }
1821 Ok(1) => {
1822 pane.update_in(cx, |pane, window, cx| {
1823 pane.remove_item(item.item_id(), false, true, window, cx)
1824 })?;
1825 }
1826 _ => return Ok(false),
1827 }
1828 return Ok(true);
1829 } else {
1830 let answer = pane.update_in(cx, |pane, window, cx| {
1831 pane.activate_item(item_ix, true, true, window, cx);
1832 window.prompt(
1833 PromptLevel::Warning,
1834 CONFLICT_MESSAGE,
1835 None,
1836 &["Overwrite", "Discard", "Cancel"],
1837 cx,
1838 )
1839 })?;
1840 match answer.await {
1841 Ok(0) => {
1842 pane.update_in(cx, |_, window, cx| {
1843 item.save(should_format, project, window, cx)
1844 })?
1845 .await?
1846 }
1847 Ok(1) => {
1848 pane.update_in(cx, |_, window, cx| item.reload(project, window, cx))?
1849 .await?
1850 }
1851 _ => return Ok(false),
1852 }
1853 }
1854 } else if is_dirty && (can_save || can_save_as) {
1855 if save_intent == SaveIntent::Close {
1856 let will_autosave = cx.update(|_window, cx| {
1857 matches!(
1858 item.workspace_settings(cx).autosave,
1859 AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange
1860 ) && item.can_autosave(cx)
1861 })?;
1862 if !will_autosave {
1863 let item_id = item.item_id();
1864 let answer_task = pane.update_in(cx, |pane, window, cx| {
1865 if pane.save_modals_spawned.insert(item_id) {
1866 pane.activate_item(item_ix, true, true, window, cx);
1867 let prompt = dirty_message_for(item.project_path(cx));
1868 Some(window.prompt(
1869 PromptLevel::Warning,
1870 &prompt,
1871 None,
1872 &["Save", "Don't Save", "Cancel"],
1873 cx,
1874 ))
1875 } else {
1876 None
1877 }
1878 })?;
1879 if let Some(answer_task) = answer_task {
1880 let answer = answer_task.await;
1881 pane.update(cx, |pane, _| {
1882 if !pane.save_modals_spawned.remove(&item_id) {
1883 debug_panic!(
1884 "save modal was not present in spawned modals after awaiting for its answer"
1885 )
1886 }
1887 })?;
1888 match answer {
1889 Ok(0) => {}
1890 Ok(1) => {
1891 // Don't save this file
1892 pane.update_in(cx, |pane, window, cx| {
1893 if pane.is_tab_pinned(item_ix) && !item.can_save(cx) {
1894 pane.pinned_tab_count -= 1;
1895 }
1896 item.discarded(project, window, cx)
1897 })
1898 .log_err();
1899 return Ok(true);
1900 }
1901 _ => return Ok(false), // Cancel
1902 }
1903 } else {
1904 return Ok(false);
1905 }
1906 }
1907 }
1908
1909 if can_save {
1910 pane.update_in(cx, |pane, window, cx| {
1911 if pane.is_active_preview_item(item.item_id()) {
1912 pane.set_preview_item_id(None, cx);
1913 }
1914 item.save(should_format, project, window, cx)
1915 })?
1916 .await?;
1917 } else if can_save_as && is_singleton {
1918 let abs_path = pane.update_in(cx, |pane, window, cx| {
1919 pane.activate_item(item_ix, true, true, window, cx);
1920 pane.workspace.update(cx, |workspace, cx| {
1921 workspace.prompt_for_new_path(window, cx)
1922 })
1923 })??;
1924 if let Some(abs_path) = abs_path.await.ok().flatten() {
1925 pane.update_in(cx, |pane, window, cx| {
1926 if let Some(item) = pane.item_for_path(abs_path.clone(), cx) {
1927 pane.remove_item(item.item_id(), false, false, window, cx);
1928 }
1929
1930 item.save_as(project, abs_path, window, cx)
1931 })?
1932 .await?;
1933 } else {
1934 return Ok(false);
1935 }
1936 }
1937 }
1938
1939 pane.update(cx, |_, cx| {
1940 cx.emit(Event::UserSavedItem {
1941 item: item.downgrade_item(),
1942 save_intent,
1943 });
1944 true
1945 })
1946 }
1947
1948 pub fn autosave_item(
1949 item: &dyn ItemHandle,
1950 project: Entity<Project>,
1951 window: &mut Window,
1952 cx: &mut App,
1953 ) -> Task<Result<()>> {
1954 let format = !matches!(
1955 item.workspace_settings(cx).autosave,
1956 AutosaveSetting::AfterDelay { .. }
1957 );
1958 if item.can_autosave(cx) {
1959 item.save(format, project, window, cx)
1960 } else {
1961 Task::ready(Ok(()))
1962 }
1963 }
1964
1965 pub fn focus_active_item(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1966 if let Some(active_item) = self.active_item() {
1967 let focus_handle = active_item.item_focus_handle(cx);
1968 window.focus(&focus_handle);
1969 }
1970 }
1971
1972 pub fn split(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
1973 cx.emit(Event::Split(direction));
1974 }
1975
1976 pub fn toolbar(&self) -> &Entity<Toolbar> {
1977 &self.toolbar
1978 }
1979
1980 pub fn handle_deleted_project_item(
1981 &mut self,
1982 entry_id: ProjectEntryId,
1983 window: &mut Window,
1984 cx: &mut Context<Pane>,
1985 ) -> Option<()> {
1986 let item_id = self.items().find_map(|item| {
1987 if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
1988 Some(item.item_id())
1989 } else {
1990 None
1991 }
1992 })?;
1993
1994 self.remove_item(item_id, false, true, window, cx);
1995 self.nav_history.remove_item(item_id);
1996
1997 Some(())
1998 }
1999
2000 fn update_toolbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2001 let active_item = self
2002 .items
2003 .get(self.active_item_index)
2004 .map(|item| item.as_ref());
2005 self.toolbar.update(cx, |toolbar, cx| {
2006 toolbar.set_active_item(active_item, window, cx);
2007 });
2008 }
2009
2010 fn update_status_bar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2011 let workspace = self.workspace.clone();
2012 let pane = cx.entity().clone();
2013
2014 window.defer(cx, move |window, cx| {
2015 let Ok(status_bar) = workspace.update(cx, |workspace, _| workspace.status_bar.clone())
2016 else {
2017 return;
2018 };
2019
2020 status_bar.update(cx, move |status_bar, cx| {
2021 status_bar.set_active_pane(&pane, window, cx);
2022 });
2023 });
2024 }
2025
2026 fn entry_abs_path(&self, entry: ProjectEntryId, cx: &App) -> Option<PathBuf> {
2027 let worktree = self
2028 .workspace
2029 .upgrade()?
2030 .read(cx)
2031 .project()
2032 .read(cx)
2033 .worktree_for_entry(entry, cx)?
2034 .read(cx);
2035 let entry = worktree.entry_for_id(entry)?;
2036 match &entry.canonical_path {
2037 Some(canonical_path) => Some(canonical_path.to_path_buf()),
2038 None => worktree.absolutize(&entry.path).ok(),
2039 }
2040 }
2041
2042 pub fn icon_color(selected: bool) -> Color {
2043 if selected {
2044 Color::Default
2045 } else {
2046 Color::Muted
2047 }
2048 }
2049
2050 fn toggle_pin_tab(&mut self, _: &TogglePinTab, window: &mut Window, cx: &mut Context<Self>) {
2051 if self.items.is_empty() {
2052 return;
2053 }
2054 let active_tab_ix = self.active_item_index();
2055 if self.is_tab_pinned(active_tab_ix) {
2056 self.unpin_tab_at(active_tab_ix, window, cx);
2057 } else {
2058 self.pin_tab_at(active_tab_ix, window, cx);
2059 }
2060 }
2061
2062 fn pin_tab_at(&mut self, ix: usize, window: &mut Window, cx: &mut Context<Self>) {
2063 maybe!({
2064 let pane = cx.entity().clone();
2065 let destination_index = self.pinned_tab_count.min(ix);
2066 self.pinned_tab_count += 1;
2067 let id = self.item_for_index(ix)?.item_id();
2068
2069 if self.is_active_preview_item(id) {
2070 self.set_preview_item_id(None, cx);
2071 }
2072
2073 self.workspace
2074 .update(cx, |_, cx| {
2075 cx.defer_in(window, move |_, window, cx| {
2076 move_item(&pane, &pane, id, destination_index, window, cx)
2077 });
2078 })
2079 .ok()?;
2080
2081 Some(())
2082 });
2083 }
2084
2085 fn unpin_tab_at(&mut self, ix: usize, window: &mut Window, cx: &mut Context<Self>) {
2086 maybe!({
2087 let pane = cx.entity().clone();
2088 self.pinned_tab_count = self.pinned_tab_count.checked_sub(1)?;
2089 let destination_index = self.pinned_tab_count;
2090
2091 let id = self.item_for_index(ix)?.item_id();
2092
2093 self.workspace
2094 .update(cx, |_, cx| {
2095 cx.defer_in(window, move |_, window, cx| {
2096 move_item(&pane, &pane, id, destination_index, window, cx)
2097 });
2098 })
2099 .ok()?;
2100
2101 Some(())
2102 });
2103 }
2104
2105 fn is_tab_pinned(&self, ix: usize) -> bool {
2106 self.pinned_tab_count > ix
2107 }
2108
2109 fn has_pinned_tabs(&self) -> bool {
2110 self.pinned_tab_count != 0
2111 }
2112
2113 fn has_unpinned_tabs(&self) -> bool {
2114 self.pinned_tab_count < self.items.len()
2115 }
2116
2117 fn activate_unpinned_tab(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2118 if self.items.is_empty() {
2119 return;
2120 }
2121 let Some(index) = self
2122 .items()
2123 .enumerate()
2124 .find_map(|(index, _item)| (!self.is_tab_pinned(index)).then_some(index))
2125 else {
2126 return;
2127 };
2128 self.activate_item(index, true, true, window, cx);
2129 }
2130
2131 fn render_tab(
2132 &self,
2133 ix: usize,
2134 item: &dyn ItemHandle,
2135 detail: usize,
2136 focus_handle: &FocusHandle,
2137 window: &mut Window,
2138 cx: &mut Context<Pane>,
2139 ) -> impl IntoElement + use<> {
2140 let is_active = ix == self.active_item_index;
2141 let is_preview = self
2142 .preview_item_id
2143 .map(|id| id == item.item_id())
2144 .unwrap_or(false);
2145
2146 let label = item.tab_content(
2147 TabContentParams {
2148 detail: Some(detail),
2149 selected: is_active,
2150 preview: is_preview,
2151 deemphasized: !self.has_focus(window, cx),
2152 },
2153 window,
2154 cx,
2155 );
2156
2157 let item_diagnostic = item
2158 .project_path(cx)
2159 .map_or(None, |project_path| self.diagnostics.get(&project_path));
2160
2161 let decorated_icon = item_diagnostic.map_or(None, |diagnostic| {
2162 let icon = match item.tab_icon(window, cx) {
2163 Some(icon) => icon,
2164 None => return None,
2165 };
2166
2167 let knockout_item_color = if is_active {
2168 cx.theme().colors().tab_active_background
2169 } else {
2170 cx.theme().colors().tab_bar_background
2171 };
2172
2173 let (icon_decoration, icon_color) = if matches!(diagnostic, &DiagnosticSeverity::ERROR)
2174 {
2175 (IconDecorationKind::X, Color::Error)
2176 } else {
2177 (IconDecorationKind::Triangle, Color::Warning)
2178 };
2179
2180 Some(DecoratedIcon::new(
2181 icon.size(IconSize::Small).color(Color::Muted),
2182 Some(
2183 IconDecoration::new(icon_decoration, knockout_item_color, cx)
2184 .color(icon_color.color(cx))
2185 .position(Point {
2186 x: px(-2.),
2187 y: px(-2.),
2188 }),
2189 ),
2190 ))
2191 });
2192
2193 let icon = if decorated_icon.is_none() {
2194 match item_diagnostic {
2195 Some(&DiagnosticSeverity::ERROR) => None,
2196 Some(&DiagnosticSeverity::WARNING) => None,
2197 _ => item
2198 .tab_icon(window, cx)
2199 .map(|icon| icon.color(Color::Muted)),
2200 }
2201 .map(|icon| icon.size(IconSize::Small))
2202 } else {
2203 None
2204 };
2205
2206 let settings = ItemSettings::get_global(cx);
2207 let close_side = &settings.close_position;
2208 let show_close_button = &settings.show_close_button;
2209 let indicator = render_item_indicator(item.boxed_clone(), cx);
2210 let item_id = item.item_id();
2211 let is_first_item = ix == 0;
2212 let is_last_item = ix == self.items.len() - 1;
2213 let is_pinned = self.is_tab_pinned(ix);
2214 let position_relative_to_active_item = ix.cmp(&self.active_item_index);
2215
2216 let tab = Tab::new(ix)
2217 .position(if is_first_item {
2218 TabPosition::First
2219 } else if is_last_item {
2220 TabPosition::Last
2221 } else {
2222 TabPosition::Middle(position_relative_to_active_item)
2223 })
2224 .close_side(match close_side {
2225 ClosePosition::Left => ui::TabCloseSide::Start,
2226 ClosePosition::Right => ui::TabCloseSide::End,
2227 })
2228 .toggle_state(is_active)
2229 .on_click(cx.listener(move |pane: &mut Self, _, window, cx| {
2230 pane.activate_item(ix, true, true, window, cx)
2231 }))
2232 // TODO: This should be a click listener with the middle mouse button instead of a mouse down listener.
2233 .on_mouse_down(
2234 MouseButton::Middle,
2235 cx.listener(move |pane, _event, window, cx| {
2236 pane.close_item_by_id(item_id, SaveIntent::Close, window, cx)
2237 .detach_and_log_err(cx);
2238 }),
2239 )
2240 .on_mouse_down(
2241 MouseButton::Left,
2242 cx.listener(move |pane, event: &MouseDownEvent, _, cx| {
2243 if let Some(id) = pane.preview_item_id {
2244 if id == item_id && event.click_count > 1 {
2245 pane.set_preview_item_id(None, cx);
2246 }
2247 }
2248 }),
2249 )
2250 .on_drag(
2251 DraggedTab {
2252 item: item.boxed_clone(),
2253 pane: cx.entity().clone(),
2254 detail,
2255 is_active,
2256 ix,
2257 },
2258 |tab, _, _, cx| cx.new(|_| tab.clone()),
2259 )
2260 .drag_over::<DraggedTab>(|tab, _, _, cx| {
2261 tab.bg(cx.theme().colors().drop_target_background)
2262 })
2263 .drag_over::<DraggedSelection>(|tab, _, _, cx| {
2264 tab.bg(cx.theme().colors().drop_target_background)
2265 })
2266 .when_some(self.can_drop_predicate.clone(), |this, p| {
2267 this.can_drop(move |a, window, cx| p(a, window, cx))
2268 })
2269 .on_drop(
2270 cx.listener(move |this, dragged_tab: &DraggedTab, window, cx| {
2271 this.drag_split_direction = None;
2272 this.handle_tab_drop(dragged_tab, ix, window, cx)
2273 }),
2274 )
2275 .on_drop(
2276 cx.listener(move |this, selection: &DraggedSelection, window, cx| {
2277 this.drag_split_direction = None;
2278 this.handle_dragged_selection_drop(selection, Some(ix), window, cx)
2279 }),
2280 )
2281 .on_drop(cx.listener(move |this, paths, window, cx| {
2282 this.drag_split_direction = None;
2283 this.handle_external_paths_drop(paths, window, cx)
2284 }))
2285 .when_some(item.tab_tooltip_content(cx), |tab, content| match content {
2286 TabTooltipContent::Text(text) => tab.tooltip(Tooltip::text(text.clone())),
2287 TabTooltipContent::Custom(element_fn) => {
2288 tab.tooltip(move |window, cx| element_fn(window, cx))
2289 }
2290 })
2291 .start_slot::<Indicator>(indicator)
2292 .map(|this| {
2293 let end_slot_action: &'static dyn Action;
2294 let end_slot_tooltip_text: &'static str;
2295 let end_slot = if is_pinned {
2296 end_slot_action = &TogglePinTab;
2297 end_slot_tooltip_text = "Unpin Tab";
2298 IconButton::new("unpin tab", IconName::Pin)
2299 .shape(IconButtonShape::Square)
2300 .icon_color(Color::Muted)
2301 .size(ButtonSize::None)
2302 .icon_size(IconSize::XSmall)
2303 .on_click(cx.listener(move |pane, _, window, cx| {
2304 pane.unpin_tab_at(ix, window, cx);
2305 }))
2306 } else {
2307 end_slot_action = &CloseActiveItem {
2308 save_intent: None,
2309 close_pinned: false,
2310 };
2311 end_slot_tooltip_text = "Close Tab";
2312 match show_close_button {
2313 ShowCloseButton::Always => IconButton::new("close tab", IconName::Close),
2314 ShowCloseButton::Hover => {
2315 IconButton::new("close tab", IconName::Close).visible_on_hover("")
2316 }
2317 ShowCloseButton::Hidden => return this,
2318 }
2319 .shape(IconButtonShape::Square)
2320 .icon_color(Color::Muted)
2321 .size(ButtonSize::None)
2322 .icon_size(IconSize::XSmall)
2323 .on_click(cx.listener(move |pane, _, window, cx| {
2324 pane.close_item_by_id(item_id, SaveIntent::Close, window, cx)
2325 .detach_and_log_err(cx);
2326 }))
2327 }
2328 .map(|this| {
2329 if is_active {
2330 let focus_handle = focus_handle.clone();
2331 this.tooltip(move |window, cx| {
2332 Tooltip::for_action_in(
2333 end_slot_tooltip_text,
2334 end_slot_action,
2335 &focus_handle,
2336 window,
2337 cx,
2338 )
2339 })
2340 } else {
2341 this.tooltip(Tooltip::text(end_slot_tooltip_text))
2342 }
2343 });
2344 this.end_slot(end_slot)
2345 })
2346 .child(
2347 h_flex()
2348 .gap_1()
2349 .items_center()
2350 .children(
2351 std::iter::once(if let Some(decorated_icon) = decorated_icon {
2352 Some(div().child(decorated_icon.into_any_element()))
2353 } else if let Some(icon) = icon {
2354 Some(div().child(icon.into_any_element()))
2355 } else {
2356 None
2357 })
2358 .flatten(),
2359 )
2360 .child(label),
2361 );
2362
2363 let single_entry_to_resolve = self.items[ix]
2364 .is_singleton(cx)
2365 .then(|| self.items[ix].project_entry_ids(cx).get(0).copied())
2366 .flatten();
2367
2368 let total_items = self.items.len();
2369 let has_items_to_left = ix > 0;
2370 let has_items_to_right = ix < total_items - 1;
2371 let is_pinned = self.is_tab_pinned(ix);
2372 let pane = cx.entity().downgrade();
2373 let menu_context = item.item_focus_handle(cx);
2374 right_click_menu(ix)
2375 .trigger(|_| tab)
2376 .menu(move |window, cx| {
2377 let pane = pane.clone();
2378 let menu_context = menu_context.clone();
2379 ContextMenu::build(window, cx, move |mut menu, window, cx| {
2380 if let Some(pane) = pane.upgrade() {
2381 menu = menu
2382 .entry(
2383 "Close",
2384 Some(Box::new(CloseActiveItem {
2385 save_intent: None,
2386 close_pinned: true,
2387 })),
2388 window.handler_for(&pane, move |pane, window, cx| {
2389 pane.close_item_by_id(item_id, SaveIntent::Close, window, cx)
2390 .detach_and_log_err(cx);
2391 }),
2392 )
2393 .item(ContextMenuItem::Entry(
2394 ContextMenuEntry::new("Close Others")
2395 .action(Box::new(CloseInactiveItems {
2396 save_intent: None,
2397 close_pinned: false,
2398 }))
2399 .disabled(total_items == 1)
2400 .handler(window.handler_for(&pane, move |pane, window, cx| {
2401 pane.close_items(window, cx, SaveIntent::Close, |id| {
2402 id != item_id
2403 })
2404 .detach_and_log_err(cx);
2405 })),
2406 ))
2407 .separator()
2408 .item(ContextMenuItem::Entry(
2409 ContextMenuEntry::new("Close Left")
2410 .action(Box::new(CloseItemsToTheLeft {
2411 close_pinned: false,
2412 }))
2413 .disabled(!has_items_to_left)
2414 .handler(window.handler_for(&pane, move |pane, window, cx| {
2415 pane.close_items_to_the_left_by_id(
2416 item_id,
2417 &CloseItemsToTheLeft {
2418 close_pinned: false,
2419 },
2420 pane.get_non_closeable_item_ids(false),
2421 window,
2422 cx,
2423 )
2424 .detach_and_log_err(cx);
2425 })),
2426 ))
2427 .item(ContextMenuItem::Entry(
2428 ContextMenuEntry::new("Close Right")
2429 .action(Box::new(CloseItemsToTheRight {
2430 close_pinned: false,
2431 }))
2432 .disabled(!has_items_to_right)
2433 .handler(window.handler_for(&pane, move |pane, window, cx| {
2434 pane.close_items_to_the_right_by_id(
2435 item_id,
2436 &CloseItemsToTheRight {
2437 close_pinned: false,
2438 },
2439 pane.get_non_closeable_item_ids(false),
2440 window,
2441 cx,
2442 )
2443 .detach_and_log_err(cx);
2444 })),
2445 ))
2446 .separator()
2447 .entry(
2448 "Close Clean",
2449 Some(Box::new(CloseCleanItems {
2450 close_pinned: false,
2451 })),
2452 window.handler_for(&pane, move |pane, window, cx| {
2453 if let Some(task) = pane.close_clean_items(
2454 &CloseCleanItems {
2455 close_pinned: false,
2456 },
2457 window,
2458 cx,
2459 ) {
2460 task.detach_and_log_err(cx)
2461 }
2462 }),
2463 )
2464 .entry(
2465 "Close All",
2466 Some(Box::new(CloseAllItems {
2467 save_intent: None,
2468 close_pinned: false,
2469 })),
2470 window.handler_for(&pane, |pane, window, cx| {
2471 if let Some(task) = pane.close_all_items(
2472 &CloseAllItems {
2473 save_intent: None,
2474 close_pinned: false,
2475 },
2476 window,
2477 cx,
2478 ) {
2479 task.detach_and_log_err(cx)
2480 }
2481 }),
2482 );
2483
2484 let pin_tab_entries = |menu: ContextMenu| {
2485 menu.separator().map(|this| {
2486 if is_pinned {
2487 this.entry(
2488 "Unpin Tab",
2489 Some(TogglePinTab.boxed_clone()),
2490 window.handler_for(&pane, move |pane, window, cx| {
2491 pane.unpin_tab_at(ix, window, cx);
2492 }),
2493 )
2494 } else {
2495 this.entry(
2496 "Pin Tab",
2497 Some(TogglePinTab.boxed_clone()),
2498 window.handler_for(&pane, move |pane, window, cx| {
2499 pane.pin_tab_at(ix, window, cx);
2500 }),
2501 )
2502 }
2503 })
2504 };
2505 if let Some(entry) = single_entry_to_resolve {
2506 let project_path = pane
2507 .read(cx)
2508 .item_for_entry(entry, cx)
2509 .and_then(|item| item.project_path(cx));
2510 let worktree = project_path.as_ref().and_then(|project_path| {
2511 pane.read(cx)
2512 .project
2513 .upgrade()?
2514 .read(cx)
2515 .worktree_for_id(project_path.worktree_id, cx)
2516 });
2517 let has_relative_path = worktree.as_ref().is_some_and(|worktree| {
2518 worktree
2519 .read(cx)
2520 .root_entry()
2521 .map_or(false, |entry| entry.is_dir())
2522 });
2523
2524 let entry_abs_path = pane.read(cx).entry_abs_path(entry, cx);
2525 let parent_abs_path = entry_abs_path
2526 .as_deref()
2527 .and_then(|abs_path| Some(abs_path.parent()?.to_path_buf()));
2528 let relative_path = project_path
2529 .map(|project_path| project_path.path)
2530 .filter(|_| has_relative_path);
2531
2532 let visible_in_project_panel = relative_path.is_some()
2533 && worktree.is_some_and(|worktree| worktree.read(cx).is_visible());
2534
2535 let entry_id = entry.to_proto();
2536 menu = menu
2537 .separator()
2538 .when_some(entry_abs_path, |menu, abs_path| {
2539 menu.entry(
2540 "Copy Path",
2541 Some(Box::new(zed_actions::workspace::CopyPath)),
2542 window.handler_for(&pane, move |_, _, cx| {
2543 cx.write_to_clipboard(ClipboardItem::new_string(
2544 abs_path.to_string_lossy().to_string(),
2545 ));
2546 }),
2547 )
2548 })
2549 .when_some(relative_path, |menu, relative_path| {
2550 menu.entry(
2551 "Copy Relative Path",
2552 Some(Box::new(zed_actions::workspace::CopyRelativePath)),
2553 window.handler_for(&pane, move |_, _, cx| {
2554 cx.write_to_clipboard(ClipboardItem::new_string(
2555 relative_path.to_string_lossy().to_string(),
2556 ));
2557 }),
2558 )
2559 })
2560 .map(pin_tab_entries)
2561 .separator()
2562 .when(visible_in_project_panel, |menu| {
2563 menu.entry(
2564 "Reveal In Project Panel",
2565 Some(Box::new(RevealInProjectPanel {
2566 entry_id: Some(entry_id),
2567 })),
2568 window.handler_for(&pane, move |pane, _, cx| {
2569 pane.project
2570 .update(cx, |_, cx| {
2571 cx.emit(project::Event::RevealInProjectPanel(
2572 ProjectEntryId::from_proto(entry_id),
2573 ))
2574 })
2575 .ok();
2576 }),
2577 )
2578 })
2579 .when_some(parent_abs_path, |menu, parent_abs_path| {
2580 menu.entry(
2581 "Open in Terminal",
2582 Some(Box::new(OpenInTerminal)),
2583 window.handler_for(&pane, move |_, window, cx| {
2584 window.dispatch_action(
2585 OpenTerminal {
2586 working_directory: parent_abs_path.clone(),
2587 }
2588 .boxed_clone(),
2589 cx,
2590 );
2591 }),
2592 )
2593 });
2594 } else {
2595 menu = menu.map(pin_tab_entries);
2596 }
2597 }
2598
2599 menu.context(menu_context)
2600 })
2601 })
2602 }
2603
2604 fn render_tab_bar(&mut self, window: &mut Window, cx: &mut Context<Pane>) -> AnyElement {
2605 let focus_handle = self.focus_handle.clone();
2606 let navigate_backward = IconButton::new("navigate_backward", IconName::ArrowLeft)
2607 .icon_size(IconSize::Small)
2608 .on_click({
2609 let entity = cx.entity().clone();
2610 move |_, window, cx| {
2611 entity.update(cx, |pane, cx| pane.navigate_backward(window, cx))
2612 }
2613 })
2614 .disabled(!self.can_navigate_backward())
2615 .tooltip({
2616 let focus_handle = focus_handle.clone();
2617 move |window, cx| {
2618 Tooltip::for_action_in("Go Back", &GoBack, &focus_handle, window, cx)
2619 }
2620 });
2621
2622 let navigate_forward = IconButton::new("navigate_forward", IconName::ArrowRight)
2623 .icon_size(IconSize::Small)
2624 .on_click({
2625 let entity = cx.entity().clone();
2626 move |_, window, cx| entity.update(cx, |pane, cx| pane.navigate_forward(window, cx))
2627 })
2628 .disabled(!self.can_navigate_forward())
2629 .tooltip({
2630 let focus_handle = focus_handle.clone();
2631 move |window, cx| {
2632 Tooltip::for_action_in("Go Forward", &GoForward, &focus_handle, window, cx)
2633 }
2634 });
2635
2636 let mut tab_items = self
2637 .items
2638 .iter()
2639 .enumerate()
2640 .zip(tab_details(&self.items, window, cx))
2641 .map(|((ix, item), detail)| {
2642 self.render_tab(ix, &**item, detail, &focus_handle, window, cx)
2643 })
2644 .collect::<Vec<_>>();
2645 let tab_count = tab_items.len();
2646 let unpinned_tabs = tab_items.split_off(self.pinned_tab_count);
2647 let pinned_tabs = tab_items;
2648 TabBar::new("tab_bar")
2649 .when(
2650 self.display_nav_history_buttons.unwrap_or_default(),
2651 |tab_bar| {
2652 tab_bar
2653 .start_child(navigate_backward)
2654 .start_child(navigate_forward)
2655 },
2656 )
2657 .map(|tab_bar| {
2658 if self.show_tab_bar_buttons {
2659 let render_tab_buttons = self.render_tab_bar_buttons.clone();
2660 let (left_children, right_children) = render_tab_buttons(self, window, cx);
2661 tab_bar
2662 .start_children(left_children)
2663 .end_children(right_children)
2664 } else {
2665 tab_bar
2666 }
2667 })
2668 .children(pinned_tabs.len().ne(&0).then(|| {
2669 let content_width = self.tab_bar_scroll_handle.content_size().width;
2670 let viewport_width = self.tab_bar_scroll_handle.viewport().size.width;
2671 // We need to check both because offset returns delta values even when the scroll handle is not scrollable
2672 let is_scrollable = content_width > viewport_width;
2673 let is_scrolled = self.tab_bar_scroll_handle.offset().x < px(0.);
2674 let has_active_unpinned_tab = self.active_item_index >= self.pinned_tab_count;
2675 h_flex()
2676 .children(pinned_tabs)
2677 .when(is_scrollable && is_scrolled, |this| {
2678 this.when(has_active_unpinned_tab, |this| this.border_r_2())
2679 .when(!has_active_unpinned_tab, |this| this.border_r_1())
2680 .border_color(cx.theme().colors().border)
2681 })
2682 }))
2683 .child(
2684 h_flex()
2685 .id("unpinned tabs")
2686 .overflow_x_scroll()
2687 .w_full()
2688 .track_scroll(&self.tab_bar_scroll_handle)
2689 .children(unpinned_tabs)
2690 .child(
2691 div()
2692 .id("tab_bar_drop_target")
2693 .min_w_6()
2694 // HACK: This empty child is currently necessary to force the drop target to appear
2695 // despite us setting a min width above.
2696 .child("")
2697 .h_full()
2698 .flex_grow()
2699 .drag_over::<DraggedTab>(|bar, _, _, cx| {
2700 bar.bg(cx.theme().colors().drop_target_background)
2701 })
2702 .drag_over::<DraggedSelection>(|bar, _, _, cx| {
2703 bar.bg(cx.theme().colors().drop_target_background)
2704 })
2705 .on_drop(cx.listener(
2706 move |this, dragged_tab: &DraggedTab, window, cx| {
2707 this.drag_split_direction = None;
2708 this.handle_tab_drop(dragged_tab, this.items.len(), window, cx)
2709 },
2710 ))
2711 .on_drop(cx.listener(
2712 move |this, selection: &DraggedSelection, window, cx| {
2713 this.drag_split_direction = None;
2714 this.handle_project_entry_drop(
2715 &selection.active_selection.entry_id,
2716 Some(tab_count),
2717 window,
2718 cx,
2719 )
2720 },
2721 ))
2722 .on_drop(cx.listener(move |this, paths, window, cx| {
2723 this.drag_split_direction = None;
2724 this.handle_external_paths_drop(paths, window, cx)
2725 }))
2726 .on_click(cx.listener(move |this, event: &ClickEvent, window, cx| {
2727 if event.up.click_count == 2 {
2728 window.dispatch_action(
2729 this.double_click_dispatch_action.boxed_clone(),
2730 cx,
2731 );
2732 }
2733 })),
2734 ),
2735 )
2736 .into_any_element()
2737 }
2738
2739 pub fn render_menu_overlay(menu: &Entity<ContextMenu>) -> Div {
2740 div().absolute().bottom_0().right_0().size_0().child(
2741 deferred(anchored().anchor(Corner::TopRight).child(menu.clone())).with_priority(1),
2742 )
2743 }
2744
2745 pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut Context<Self>) {
2746 self.zoomed = zoomed;
2747 cx.notify();
2748 }
2749
2750 pub fn is_zoomed(&self) -> bool {
2751 self.zoomed
2752 }
2753
2754 fn handle_drag_move<T: 'static>(
2755 &mut self,
2756 event: &DragMoveEvent<T>,
2757 window: &mut Window,
2758 cx: &mut Context<Self>,
2759 ) {
2760 let can_split_predicate = self.can_split_predicate.take();
2761 let can_split = match &can_split_predicate {
2762 Some(can_split_predicate) => {
2763 can_split_predicate(self, event.dragged_item(), window, cx)
2764 }
2765 None => false,
2766 };
2767 self.can_split_predicate = can_split_predicate;
2768 if !can_split {
2769 return;
2770 }
2771
2772 let rect = event.bounds.size;
2773
2774 let size = event.bounds.size.width.min(event.bounds.size.height)
2775 * WorkspaceSettings::get_global(cx).drop_target_size;
2776
2777 let relative_cursor = Point::new(
2778 event.event.position.x - event.bounds.left(),
2779 event.event.position.y - event.bounds.top(),
2780 );
2781
2782 let direction = if relative_cursor.x < size
2783 || relative_cursor.x > rect.width - size
2784 || relative_cursor.y < size
2785 || relative_cursor.y > rect.height - size
2786 {
2787 [
2788 SplitDirection::Up,
2789 SplitDirection::Right,
2790 SplitDirection::Down,
2791 SplitDirection::Left,
2792 ]
2793 .iter()
2794 .min_by_key(|side| match side {
2795 SplitDirection::Up => relative_cursor.y,
2796 SplitDirection::Right => rect.width - relative_cursor.x,
2797 SplitDirection::Down => rect.height - relative_cursor.y,
2798 SplitDirection::Left => relative_cursor.x,
2799 })
2800 .cloned()
2801 } else {
2802 None
2803 };
2804
2805 if direction != self.drag_split_direction {
2806 self.drag_split_direction = direction;
2807 }
2808 }
2809
2810 pub fn handle_tab_drop(
2811 &mut self,
2812 dragged_tab: &DraggedTab,
2813 ix: usize,
2814 window: &mut Window,
2815 cx: &mut Context<Self>,
2816 ) {
2817 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2818 if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_tab, window, cx) {
2819 return;
2820 }
2821 }
2822 let mut to_pane = cx.entity().clone();
2823 let split_direction = self.drag_split_direction;
2824 let item_id = dragged_tab.item.item_id();
2825 if let Some(preview_item_id) = self.preview_item_id {
2826 if item_id == preview_item_id {
2827 self.set_preview_item_id(None, cx);
2828 }
2829 }
2830
2831 let from_pane = dragged_tab.pane.clone();
2832 self.workspace
2833 .update(cx, |_, cx| {
2834 cx.defer_in(window, move |workspace, window, cx| {
2835 if let Some(split_direction) = split_direction {
2836 to_pane = workspace.split_pane(to_pane, split_direction, window, cx);
2837 }
2838 let old_ix = from_pane.read(cx).index_for_item_id(item_id);
2839 let old_len = to_pane.read(cx).items.len();
2840 move_item(&from_pane, &to_pane, item_id, ix, window, cx);
2841 if to_pane == from_pane {
2842 if let Some(old_index) = old_ix {
2843 to_pane.update(cx, |this, _| {
2844 if old_index < this.pinned_tab_count
2845 && (ix == this.items.len() || ix > this.pinned_tab_count)
2846 {
2847 this.pinned_tab_count -= 1;
2848 } else if this.has_pinned_tabs()
2849 && old_index >= this.pinned_tab_count
2850 && ix < this.pinned_tab_count
2851 {
2852 this.pinned_tab_count += 1;
2853 }
2854 });
2855 }
2856 } else {
2857 to_pane.update(cx, |this, _| {
2858 if this.items.len() > old_len // Did we not deduplicate on drag?
2859 && this.has_pinned_tabs()
2860 && ix < this.pinned_tab_count
2861 {
2862 this.pinned_tab_count += 1;
2863 }
2864 });
2865 from_pane.update(cx, |this, _| {
2866 if let Some(index) = old_ix {
2867 if this.pinned_tab_count > index {
2868 this.pinned_tab_count -= 1;
2869 }
2870 }
2871 })
2872 }
2873 });
2874 })
2875 .log_err();
2876 }
2877
2878 fn handle_dragged_selection_drop(
2879 &mut self,
2880 dragged_selection: &DraggedSelection,
2881 dragged_onto: Option<usize>,
2882 window: &mut Window,
2883 cx: &mut Context<Self>,
2884 ) {
2885 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2886 if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_selection, window, cx)
2887 {
2888 return;
2889 }
2890 }
2891 self.handle_project_entry_drop(
2892 &dragged_selection.active_selection.entry_id,
2893 dragged_onto,
2894 window,
2895 cx,
2896 );
2897 }
2898
2899 fn handle_project_entry_drop(
2900 &mut self,
2901 project_entry_id: &ProjectEntryId,
2902 target: Option<usize>,
2903 window: &mut Window,
2904 cx: &mut Context<Self>,
2905 ) {
2906 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2907 if let ControlFlow::Break(()) = custom_drop_handle(self, project_entry_id, window, cx) {
2908 return;
2909 }
2910 }
2911 let mut to_pane = cx.entity().clone();
2912 let split_direction = self.drag_split_direction;
2913 let project_entry_id = *project_entry_id;
2914 self.workspace
2915 .update(cx, |_, cx| {
2916 cx.defer_in(window, move |workspace, window, cx| {
2917 if let Some(path) = workspace
2918 .project()
2919 .read(cx)
2920 .path_for_entry(project_entry_id, cx)
2921 {
2922 let load_path_task = workspace.load_path(path, window, cx);
2923 cx.spawn_in(window, async move |workspace, cx| {
2924 if let Some((project_entry_id, build_item)) =
2925 load_path_task.await.notify_async_err(cx)
2926 {
2927 let (to_pane, new_item_handle) = workspace
2928 .update_in(cx, |workspace, window, cx| {
2929 if let Some(split_direction) = split_direction {
2930 to_pane = workspace.split_pane(
2931 to_pane,
2932 split_direction,
2933 window,
2934 cx,
2935 );
2936 }
2937 let new_item_handle = to_pane.update(cx, |pane, cx| {
2938 pane.open_item(
2939 project_entry_id,
2940 true,
2941 false,
2942 true,
2943 target,
2944 window,
2945 cx,
2946 build_item,
2947 )
2948 });
2949 (to_pane, new_item_handle)
2950 })
2951 .log_err()?;
2952 to_pane
2953 .update_in(cx, |this, window, cx| {
2954 let Some(index) = this.index_for_item(&*new_item_handle)
2955 else {
2956 return;
2957 };
2958
2959 if target.map_or(false, |target| this.is_tab_pinned(target))
2960 {
2961 this.pin_tab_at(index, window, cx);
2962 }
2963 })
2964 .ok()?
2965 }
2966 Some(())
2967 })
2968 .detach();
2969 };
2970 });
2971 })
2972 .log_err();
2973 }
2974
2975 fn handle_external_paths_drop(
2976 &mut self,
2977 paths: &ExternalPaths,
2978 window: &mut Window,
2979 cx: &mut Context<Self>,
2980 ) {
2981 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2982 if let ControlFlow::Break(()) = custom_drop_handle(self, paths, window, cx) {
2983 return;
2984 }
2985 }
2986 let mut to_pane = cx.entity().clone();
2987 let mut split_direction = self.drag_split_direction;
2988 let paths = paths.paths().to_vec();
2989 let is_remote = self
2990 .workspace
2991 .update(cx, |workspace, cx| {
2992 if workspace.project().read(cx).is_via_collab() {
2993 workspace.show_error(
2994 &anyhow::anyhow!("Cannot drop files on a remote project"),
2995 cx,
2996 );
2997 true
2998 } else {
2999 false
3000 }
3001 })
3002 .unwrap_or(true);
3003 if is_remote {
3004 return;
3005 }
3006
3007 self.workspace
3008 .update(cx, |workspace, cx| {
3009 let fs = Arc::clone(workspace.project().read(cx).fs());
3010 cx.spawn_in(window, async move |workspace, cx| {
3011 let mut is_file_checks = FuturesUnordered::new();
3012 for path in &paths {
3013 is_file_checks.push(fs.is_file(path))
3014 }
3015 let mut has_files_to_open = false;
3016 while let Some(is_file) = is_file_checks.next().await {
3017 if is_file {
3018 has_files_to_open = true;
3019 break;
3020 }
3021 }
3022 drop(is_file_checks);
3023 if !has_files_to_open {
3024 split_direction = None;
3025 }
3026
3027 if let Ok(open_task) = workspace.update_in(cx, |workspace, window, cx| {
3028 if let Some(split_direction) = split_direction {
3029 to_pane = workspace.split_pane(to_pane, split_direction, window, cx);
3030 }
3031 workspace.open_paths(
3032 paths,
3033 OpenOptions {
3034 visible: Some(OpenVisible::OnlyDirectories),
3035 ..Default::default()
3036 },
3037 Some(to_pane.downgrade()),
3038 window,
3039 cx,
3040 )
3041 }) {
3042 let opened_items: Vec<_> = open_task.await;
3043 _ = workspace.update(cx, |workspace, cx| {
3044 for item in opened_items.into_iter().flatten() {
3045 if let Err(e) = item {
3046 workspace.show_error(&e, cx);
3047 }
3048 }
3049 });
3050 }
3051 })
3052 .detach();
3053 })
3054 .log_err();
3055 }
3056
3057 pub fn display_nav_history_buttons(&mut self, display: Option<bool>) {
3058 self.display_nav_history_buttons = display;
3059 }
3060
3061 fn get_non_closeable_item_ids(&self, close_pinned: bool) -> Vec<EntityId> {
3062 if close_pinned {
3063 return vec![];
3064 }
3065
3066 self.items
3067 .iter()
3068 .enumerate()
3069 .filter(|(index, _item)| self.is_tab_pinned(*index))
3070 .map(|(_, item)| item.item_id())
3071 .collect()
3072 }
3073
3074 pub fn drag_split_direction(&self) -> Option<SplitDirection> {
3075 self.drag_split_direction
3076 }
3077
3078 pub fn set_zoom_out_on_close(&mut self, zoom_out_on_close: bool) {
3079 self.zoom_out_on_close = zoom_out_on_close;
3080 }
3081}
3082
3083fn default_render_tab_bar_buttons(
3084 pane: &mut Pane,
3085 window: &mut Window,
3086 cx: &mut Context<Pane>,
3087) -> (Option<AnyElement>, Option<AnyElement>) {
3088 if !pane.has_focus(window, cx) && !pane.context_menu_focused(window, cx) {
3089 return (None, None);
3090 }
3091 // Ideally we would return a vec of elements here to pass directly to the [TabBar]'s
3092 // `end_slot`, but due to needing a view here that isn't possible.
3093 let right_children = h_flex()
3094 // Instead we need to replicate the spacing from the [TabBar]'s `end_slot` here.
3095 .gap(DynamicSpacing::Base04.rems(cx))
3096 .child(
3097 PopoverMenu::new("pane-tab-bar-popover-menu")
3098 .trigger_with_tooltip(
3099 IconButton::new("plus", IconName::Plus).icon_size(IconSize::Small),
3100 Tooltip::text("New..."),
3101 )
3102 .anchor(Corner::TopRight)
3103 .with_handle(pane.new_item_context_menu_handle.clone())
3104 .menu(move |window, cx| {
3105 Some(ContextMenu::build(window, cx, |menu, _, _| {
3106 menu.action("New File", NewFile.boxed_clone())
3107 .action("Open File", ToggleFileFinder::default().boxed_clone())
3108 .separator()
3109 .action(
3110 "Search Project",
3111 DeploySearch {
3112 replace_enabled: false,
3113 included_files: None,
3114 }
3115 .boxed_clone(),
3116 )
3117 .action("Search Symbols", ToggleProjectSymbols.boxed_clone())
3118 .separator()
3119 .action("New Terminal", NewTerminal.boxed_clone())
3120 }))
3121 }),
3122 )
3123 .child(
3124 PopoverMenu::new("pane-tab-bar-split")
3125 .trigger_with_tooltip(
3126 IconButton::new("split", IconName::Split).icon_size(IconSize::Small),
3127 Tooltip::text("Split Pane"),
3128 )
3129 .anchor(Corner::TopRight)
3130 .with_handle(pane.split_item_context_menu_handle.clone())
3131 .menu(move |window, cx| {
3132 ContextMenu::build(window, cx, |menu, _, _| {
3133 menu.action("Split Right", SplitRight.boxed_clone())
3134 .action("Split Left", SplitLeft.boxed_clone())
3135 .action("Split Up", SplitUp.boxed_clone())
3136 .action("Split Down", SplitDown.boxed_clone())
3137 })
3138 .into()
3139 }),
3140 )
3141 .child({
3142 let zoomed = pane.is_zoomed();
3143 IconButton::new("toggle_zoom", IconName::Maximize)
3144 .icon_size(IconSize::Small)
3145 .toggle_state(zoomed)
3146 .selected_icon(IconName::Minimize)
3147 .on_click(cx.listener(|pane, _, window, cx| {
3148 pane.toggle_zoom(&crate::ToggleZoom, window, cx);
3149 }))
3150 .tooltip(move |window, cx| {
3151 Tooltip::for_action(
3152 if zoomed { "Zoom Out" } else { "Zoom In" },
3153 &ToggleZoom,
3154 window,
3155 cx,
3156 )
3157 })
3158 })
3159 .into_any_element()
3160 .into();
3161 (None, right_children)
3162}
3163
3164impl Focusable for Pane {
3165 fn focus_handle(&self, _cx: &App) -> FocusHandle {
3166 self.focus_handle.clone()
3167 }
3168}
3169
3170impl Render for Pane {
3171 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3172 let mut key_context = KeyContext::new_with_defaults();
3173 key_context.add("Pane");
3174 if self.active_item().is_none() {
3175 key_context.add("EmptyPane");
3176 }
3177
3178 let should_display_tab_bar = self.should_display_tab_bar.clone();
3179 let display_tab_bar = should_display_tab_bar(window, cx);
3180 let Some(project) = self.project.upgrade() else {
3181 return div().track_focus(&self.focus_handle(cx));
3182 };
3183 let is_local = project.read(cx).is_local();
3184
3185 v_flex()
3186 .key_context(key_context)
3187 .track_focus(&self.focus_handle(cx))
3188 .size_full()
3189 .flex_none()
3190 .overflow_hidden()
3191 .on_action(cx.listener(|pane, _: &AlternateFile, window, cx| {
3192 pane.alternate_file(window, cx);
3193 }))
3194 .on_action(
3195 cx.listener(|pane, _: &SplitLeft, _, cx| pane.split(SplitDirection::Left, cx)),
3196 )
3197 .on_action(cx.listener(|pane, _: &SplitUp, _, cx| pane.split(SplitDirection::Up, cx)))
3198 .on_action(cx.listener(|pane, _: &SplitHorizontal, _, cx| {
3199 pane.split(SplitDirection::horizontal(cx), cx)
3200 }))
3201 .on_action(cx.listener(|pane, _: &SplitVertical, _, cx| {
3202 pane.split(SplitDirection::vertical(cx), cx)
3203 }))
3204 .on_action(
3205 cx.listener(|pane, _: &SplitRight, _, cx| pane.split(SplitDirection::Right, cx)),
3206 )
3207 .on_action(
3208 cx.listener(|pane, _: &SplitDown, _, cx| pane.split(SplitDirection::Down, cx)),
3209 )
3210 .on_action(
3211 cx.listener(|pane, _: &GoBack, window, cx| pane.navigate_backward(window, cx)),
3212 )
3213 .on_action(
3214 cx.listener(|pane, _: &GoForward, window, cx| pane.navigate_forward(window, cx)),
3215 )
3216 .on_action(cx.listener(|_, _: &JoinIntoNext, _, cx| {
3217 cx.emit(Event::JoinIntoNext);
3218 }))
3219 .on_action(cx.listener(|_, _: &JoinAll, _, cx| {
3220 cx.emit(Event::JoinAll);
3221 }))
3222 .on_action(cx.listener(Pane::toggle_zoom))
3223 .on_action(
3224 cx.listener(|pane: &mut Pane, action: &ActivateItem, window, cx| {
3225 pane.activate_item(action.0, true, true, window, cx);
3226 }),
3227 )
3228 .on_action(
3229 cx.listener(|pane: &mut Pane, _: &ActivateLastItem, window, cx| {
3230 pane.activate_item(pane.items.len() - 1, true, true, window, cx);
3231 }),
3232 )
3233 .on_action(
3234 cx.listener(|pane: &mut Pane, _: &ActivatePreviousItem, window, cx| {
3235 pane.activate_prev_item(true, window, cx);
3236 }),
3237 )
3238 .on_action(
3239 cx.listener(|pane: &mut Pane, _: &ActivateNextItem, window, cx| {
3240 pane.activate_next_item(true, window, cx);
3241 }),
3242 )
3243 .on_action(
3244 cx.listener(|pane, _: &SwapItemLeft, window, cx| pane.swap_item_left(window, cx)),
3245 )
3246 .on_action(
3247 cx.listener(|pane, _: &SwapItemRight, window, cx| pane.swap_item_right(window, cx)),
3248 )
3249 .on_action(cx.listener(|pane, action, window, cx| {
3250 pane.toggle_pin_tab(action, window, cx);
3251 }))
3252 .when(PreviewTabsSettings::get_global(cx).enabled, |this| {
3253 this.on_action(cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, _, cx| {
3254 if let Some(active_item_id) = pane.active_item().map(|i| i.item_id()) {
3255 if pane.is_active_preview_item(active_item_id) {
3256 pane.set_preview_item_id(None, cx);
3257 } else {
3258 pane.set_preview_item_id(Some(active_item_id), cx);
3259 }
3260 }
3261 }))
3262 })
3263 .on_action(
3264 cx.listener(|pane: &mut Self, action: &CloseActiveItem, window, cx| {
3265 if let Some(task) = pane.close_active_item(action, window, cx) {
3266 task.detach_and_log_err(cx)
3267 }
3268 }),
3269 )
3270 .on_action(
3271 cx.listener(|pane: &mut Self, action: &CloseInactiveItems, window, cx| {
3272 if let Some(task) = pane.close_inactive_items(action, window, cx) {
3273 task.detach_and_log_err(cx)
3274 }
3275 }),
3276 )
3277 .on_action(
3278 cx.listener(|pane: &mut Self, action: &CloseCleanItems, window, cx| {
3279 if let Some(task) = pane.close_clean_items(action, window, cx) {
3280 task.detach_and_log_err(cx)
3281 }
3282 }),
3283 )
3284 .on_action(cx.listener(
3285 |pane: &mut Self, action: &CloseItemsToTheLeft, window, cx| {
3286 if let Some(task) = pane.close_items_to_the_left(action, window, cx) {
3287 task.detach_and_log_err(cx)
3288 }
3289 },
3290 ))
3291 .on_action(cx.listener(
3292 |pane: &mut Self, action: &CloseItemsToTheRight, window, cx| {
3293 if let Some(task) = pane.close_items_to_the_right(action, window, cx) {
3294 task.detach_and_log_err(cx)
3295 }
3296 },
3297 ))
3298 .on_action(
3299 cx.listener(|pane: &mut Self, action: &CloseAllItems, window, cx| {
3300 if let Some(task) = pane.close_all_items(action, window, cx) {
3301 task.detach_and_log_err(cx)
3302 }
3303 }),
3304 )
3305 .on_action(
3306 cx.listener(|pane: &mut Self, action: &CloseActiveItem, window, cx| {
3307 if let Some(task) = pane.close_active_item(action, window, cx) {
3308 task.detach_and_log_err(cx)
3309 }
3310 }),
3311 )
3312 .on_action(
3313 cx.listener(|pane: &mut Self, action: &RevealInProjectPanel, _, cx| {
3314 let entry_id = action
3315 .entry_id
3316 .map(ProjectEntryId::from_proto)
3317 .or_else(|| pane.active_item()?.project_entry_ids(cx).first().copied());
3318 if let Some(entry_id) = entry_id {
3319 pane.project
3320 .update(cx, |_, cx| {
3321 cx.emit(project::Event::RevealInProjectPanel(entry_id))
3322 })
3323 .ok();
3324 }
3325 }),
3326 )
3327 .on_action(cx.listener(|_, _: &menu::Cancel, window, cx| {
3328 if cx.stop_active_drag(window) {
3329 return;
3330 } else {
3331 cx.propagate();
3332 }
3333 }))
3334 .when(self.active_item().is_some() && display_tab_bar, |pane| {
3335 pane.child((self.render_tab_bar.clone())(self, window, cx))
3336 })
3337 .child({
3338 let has_worktrees = project.read(cx).visible_worktrees(cx).next().is_some();
3339 // main content
3340 div()
3341 .flex_1()
3342 .relative()
3343 .group("")
3344 .overflow_hidden()
3345 .on_drag_move::<DraggedTab>(cx.listener(Self::handle_drag_move))
3346 .on_drag_move::<DraggedSelection>(cx.listener(Self::handle_drag_move))
3347 .when(is_local, |div| {
3348 div.on_drag_move::<ExternalPaths>(cx.listener(Self::handle_drag_move))
3349 })
3350 .map(|div| {
3351 if let Some(item) = self.active_item() {
3352 div.id("pane_placeholder")
3353 .v_flex()
3354 .size_full()
3355 .overflow_hidden()
3356 .child(self.toolbar.clone())
3357 .child(item.to_any())
3358 } else {
3359 let placeholder = div
3360 .id("pane_placeholder")
3361 .h_flex()
3362 .size_full()
3363 .justify_center()
3364 .on_click(cx.listener(
3365 move |this, event: &ClickEvent, window, cx| {
3366 if event.up.click_count == 2 {
3367 window.dispatch_action(
3368 this.double_click_dispatch_action.boxed_clone(),
3369 cx,
3370 );
3371 }
3372 },
3373 ));
3374 if has_worktrees {
3375 placeholder
3376 } else {
3377 placeholder.child(
3378 Label::new("Open a file or project to get started.")
3379 .color(Color::Muted),
3380 )
3381 }
3382 }
3383 })
3384 .child(
3385 // drag target
3386 div()
3387 .invisible()
3388 .absolute()
3389 .bg(cx.theme().colors().drop_target_background)
3390 .group_drag_over::<DraggedTab>("", |style| style.visible())
3391 .group_drag_over::<DraggedSelection>("", |style| style.visible())
3392 .when(is_local, |div| {
3393 div.group_drag_over::<ExternalPaths>("", |style| style.visible())
3394 })
3395 .when_some(self.can_drop_predicate.clone(), |this, p| {
3396 this.can_drop(move |a, window, cx| p(a, window, cx))
3397 })
3398 .on_drop(cx.listener(move |this, dragged_tab, window, cx| {
3399 this.handle_tab_drop(
3400 dragged_tab,
3401 this.active_item_index(),
3402 window,
3403 cx,
3404 )
3405 }))
3406 .on_drop(cx.listener(
3407 move |this, selection: &DraggedSelection, window, cx| {
3408 this.handle_dragged_selection_drop(selection, None, window, cx)
3409 },
3410 ))
3411 .on_drop(cx.listener(move |this, paths, window, cx| {
3412 this.handle_external_paths_drop(paths, window, cx)
3413 }))
3414 .map(|div| {
3415 let size = DefiniteLength::Fraction(0.5);
3416 match self.drag_split_direction {
3417 None => div.top_0().right_0().bottom_0().left_0(),
3418 Some(SplitDirection::Up) => {
3419 div.top_0().left_0().right_0().h(size)
3420 }
3421 Some(SplitDirection::Down) => {
3422 div.left_0().bottom_0().right_0().h(size)
3423 }
3424 Some(SplitDirection::Left) => {
3425 div.top_0().left_0().bottom_0().w(size)
3426 }
3427 Some(SplitDirection::Right) => {
3428 div.top_0().bottom_0().right_0().w(size)
3429 }
3430 }
3431 }),
3432 )
3433 })
3434 .on_mouse_down(
3435 MouseButton::Navigate(NavigationDirection::Back),
3436 cx.listener(|pane, _, window, cx| {
3437 if let Some(workspace) = pane.workspace.upgrade() {
3438 let pane = cx.entity().downgrade();
3439 window.defer(cx, move |window, cx| {
3440 workspace.update(cx, |workspace, cx| {
3441 workspace.go_back(pane, window, cx).detach_and_log_err(cx)
3442 })
3443 })
3444 }
3445 }),
3446 )
3447 .on_mouse_down(
3448 MouseButton::Navigate(NavigationDirection::Forward),
3449 cx.listener(|pane, _, window, cx| {
3450 if let Some(workspace) = pane.workspace.upgrade() {
3451 let pane = cx.entity().downgrade();
3452 window.defer(cx, move |window, cx| {
3453 workspace.update(cx, |workspace, cx| {
3454 workspace
3455 .go_forward(pane, window, cx)
3456 .detach_and_log_err(cx)
3457 })
3458 })
3459 }
3460 }),
3461 )
3462 }
3463}
3464
3465impl ItemNavHistory {
3466 pub fn push<D: 'static + Send + Any>(&mut self, data: Option<D>, cx: &mut App) {
3467 if self
3468 .item
3469 .upgrade()
3470 .is_some_and(|item| item.include_in_nav_history())
3471 {
3472 self.history
3473 .push(data, self.item.clone(), self.is_preview, cx);
3474 }
3475 }
3476
3477 pub fn pop_backward(&mut self, cx: &mut App) -> Option<NavigationEntry> {
3478 self.history.pop(NavigationMode::GoingBack, cx)
3479 }
3480
3481 pub fn pop_forward(&mut self, cx: &mut App) -> Option<NavigationEntry> {
3482 self.history.pop(NavigationMode::GoingForward, cx)
3483 }
3484}
3485
3486impl NavHistory {
3487 pub fn for_each_entry(
3488 &self,
3489 cx: &App,
3490 mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
3491 ) {
3492 let borrowed_history = self.0.lock();
3493 borrowed_history
3494 .forward_stack
3495 .iter()
3496 .chain(borrowed_history.backward_stack.iter())
3497 .chain(borrowed_history.closed_stack.iter())
3498 .for_each(|entry| {
3499 if let Some(project_and_abs_path) =
3500 borrowed_history.paths_by_item.get(&entry.item.id())
3501 {
3502 f(entry, project_and_abs_path.clone());
3503 } else if let Some(item) = entry.item.upgrade() {
3504 if let Some(path) = item.project_path(cx) {
3505 f(entry, (path, None));
3506 }
3507 }
3508 })
3509 }
3510
3511 pub fn set_mode(&mut self, mode: NavigationMode) {
3512 self.0.lock().mode = mode;
3513 }
3514
3515 pub fn mode(&self) -> NavigationMode {
3516 self.0.lock().mode
3517 }
3518
3519 pub fn disable(&mut self) {
3520 self.0.lock().mode = NavigationMode::Disabled;
3521 }
3522
3523 pub fn enable(&mut self) {
3524 self.0.lock().mode = NavigationMode::Normal;
3525 }
3526
3527 pub fn pop(&mut self, mode: NavigationMode, cx: &mut App) -> Option<NavigationEntry> {
3528 let mut state = self.0.lock();
3529 let entry = match mode {
3530 NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
3531 return None;
3532 }
3533 NavigationMode::GoingBack => &mut state.backward_stack,
3534 NavigationMode::GoingForward => &mut state.forward_stack,
3535 NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
3536 }
3537 .pop_back();
3538 if entry.is_some() {
3539 state.did_update(cx);
3540 }
3541 entry
3542 }
3543
3544 pub fn push<D: 'static + Send + Any>(
3545 &mut self,
3546 data: Option<D>,
3547 item: Arc<dyn WeakItemHandle>,
3548 is_preview: bool,
3549 cx: &mut App,
3550 ) {
3551 let state = &mut *self.0.lock();
3552 match state.mode {
3553 NavigationMode::Disabled => {}
3554 NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
3555 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3556 state.backward_stack.pop_front();
3557 }
3558 state.backward_stack.push_back(NavigationEntry {
3559 item,
3560 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3561 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3562 is_preview,
3563 });
3564 state.forward_stack.clear();
3565 }
3566 NavigationMode::GoingBack => {
3567 if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3568 state.forward_stack.pop_front();
3569 }
3570 state.forward_stack.push_back(NavigationEntry {
3571 item,
3572 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3573 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3574 is_preview,
3575 });
3576 }
3577 NavigationMode::GoingForward => {
3578 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3579 state.backward_stack.pop_front();
3580 }
3581 state.backward_stack.push_back(NavigationEntry {
3582 item,
3583 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3584 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3585 is_preview,
3586 });
3587 }
3588 NavigationMode::ClosingItem => {
3589 if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3590 state.closed_stack.pop_front();
3591 }
3592 state.closed_stack.push_back(NavigationEntry {
3593 item,
3594 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3595 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3596 is_preview,
3597 });
3598 }
3599 }
3600 state.did_update(cx);
3601 }
3602
3603 pub fn remove_item(&mut self, item_id: EntityId) {
3604 let mut state = self.0.lock();
3605 state.paths_by_item.remove(&item_id);
3606 state
3607 .backward_stack
3608 .retain(|entry| entry.item.id() != item_id);
3609 state
3610 .forward_stack
3611 .retain(|entry| entry.item.id() != item_id);
3612 state
3613 .closed_stack
3614 .retain(|entry| entry.item.id() != item_id);
3615 }
3616
3617 pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option<PathBuf>)> {
3618 self.0.lock().paths_by_item.get(&item_id).cloned()
3619 }
3620}
3621
3622impl NavHistoryState {
3623 pub fn did_update(&self, cx: &mut App) {
3624 if let Some(pane) = self.pane.upgrade() {
3625 cx.defer(move |cx| {
3626 pane.update(cx, |pane, cx| pane.history_updated(cx));
3627 });
3628 }
3629 }
3630}
3631
3632fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
3633 let path = buffer_path
3634 .as_ref()
3635 .and_then(|p| {
3636 p.path
3637 .to_str()
3638 .and_then(|s| if s.is_empty() { None } else { Some(s) })
3639 })
3640 .unwrap_or("This buffer");
3641 let path = truncate_and_remove_front(path, 80);
3642 format!("{path} contains unsaved edits. Do you want to save it?")
3643}
3644
3645pub fn tab_details(items: &[Box<dyn ItemHandle>], _window: &Window, cx: &App) -> Vec<usize> {
3646 let mut tab_details = items.iter().map(|_| 0).collect::<Vec<_>>();
3647 let mut tab_descriptions = HashMap::default();
3648 let mut done = false;
3649 while !done {
3650 done = true;
3651
3652 // Store item indices by their tab description.
3653 for (ix, (item, detail)) in items.iter().zip(&tab_details).enumerate() {
3654 let description = item.tab_content_text(*detail, cx);
3655 if *detail == 0 || description != item.tab_content_text(detail - 1, cx) {
3656 tab_descriptions
3657 .entry(description)
3658 .or_insert(Vec::new())
3659 .push(ix);
3660 }
3661 }
3662
3663 // If two or more items have the same tab description, increase their level
3664 // of detail and try again.
3665 for (_, item_ixs) in tab_descriptions.drain() {
3666 if item_ixs.len() > 1 {
3667 done = false;
3668 for ix in item_ixs {
3669 tab_details[ix] += 1;
3670 }
3671 }
3672 }
3673 }
3674
3675 tab_details
3676}
3677
3678pub fn render_item_indicator(item: Box<dyn ItemHandle>, cx: &App) -> Option<Indicator> {
3679 maybe!({
3680 let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
3681 (true, _) => Color::Warning,
3682 (_, true) => Color::Accent,
3683 (false, false) => return None,
3684 };
3685
3686 Some(Indicator::dot().color(indicator_color))
3687 })
3688}
3689
3690impl Render for DraggedTab {
3691 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3692 let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
3693 let label = self.item.tab_content(
3694 TabContentParams {
3695 detail: Some(self.detail),
3696 selected: false,
3697 preview: false,
3698 deemphasized: false,
3699 },
3700 window,
3701 cx,
3702 );
3703 Tab::new("")
3704 .toggle_state(self.is_active)
3705 .child(label)
3706 .render(window, cx)
3707 .font(ui_font)
3708 }
3709}
3710
3711#[cfg(test)]
3712mod tests {
3713 use std::num::NonZero;
3714
3715 use super::*;
3716 use crate::item::test::{TestItem, TestProjectItem};
3717 use gpui::{TestAppContext, VisualTestContext};
3718 use project::FakeFs;
3719 use settings::SettingsStore;
3720 use theme::LoadThemes;
3721
3722 #[gpui::test]
3723 async fn test_remove_active_empty(cx: &mut TestAppContext) {
3724 init_test(cx);
3725 let fs = FakeFs::new(cx.executor());
3726
3727 let project = Project::test(fs, None, cx).await;
3728 let (workspace, cx) =
3729 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3730 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3731
3732 pane.update_in(cx, |pane, window, cx| {
3733 assert!(
3734 pane.close_active_item(
3735 &CloseActiveItem {
3736 save_intent: None,
3737 close_pinned: false
3738 },
3739 window,
3740 cx
3741 )
3742 .is_none()
3743 )
3744 });
3745 }
3746
3747 #[gpui::test]
3748 async fn test_add_item_capped_to_max_tabs(cx: &mut TestAppContext) {
3749 init_test(cx);
3750 let fs = FakeFs::new(cx.executor());
3751
3752 let project = Project::test(fs, None, cx).await;
3753 let (workspace, cx) =
3754 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3755 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3756
3757 for i in 0..7 {
3758 add_labeled_item(&pane, format!("{}", i).as_str(), false, cx);
3759 }
3760 set_max_tabs(cx, Some(5));
3761 add_labeled_item(&pane, "7", false, cx);
3762 // Remove items to respect the max tab cap.
3763 assert_item_labels(&pane, ["3", "4", "5", "6", "7*"], cx);
3764 pane.update_in(cx, |pane, window, cx| {
3765 pane.activate_item(0, false, false, window, cx);
3766 });
3767 add_labeled_item(&pane, "X", false, cx);
3768 // Respect activation order.
3769 assert_item_labels(&pane, ["3", "X*", "5", "6", "7"], cx);
3770
3771 for i in 0..7 {
3772 add_labeled_item(&pane, format!("D{}", i).as_str(), true, cx);
3773 }
3774 // Keeps dirty items, even over max tab cap.
3775 assert_item_labels(
3776 &pane,
3777 ["D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6*^"],
3778 cx,
3779 );
3780
3781 set_max_tabs(cx, None);
3782 for i in 0..7 {
3783 add_labeled_item(&pane, format!("N{}", i).as_str(), false, cx);
3784 }
3785 // No cap when max tabs is None.
3786 assert_item_labels(
3787 &pane,
3788 [
3789 "D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6^", "N0", "N1", "N2", "N3", "N4",
3790 "N5", "N6*",
3791 ],
3792 cx,
3793 );
3794 }
3795
3796 #[gpui::test]
3797 async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
3798 init_test(cx);
3799 let fs = FakeFs::new(cx.executor());
3800
3801 let project = Project::test(fs, None, cx).await;
3802 let (workspace, cx) =
3803 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3804 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3805
3806 // 1. Add with a destination index
3807 // a. Add before the active item
3808 set_labeled_items(&pane, ["A", "B*", "C"], cx);
3809 pane.update_in(cx, |pane, window, cx| {
3810 pane.add_item(
3811 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3812 false,
3813 false,
3814 Some(0),
3815 window,
3816 cx,
3817 );
3818 });
3819 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
3820
3821 // b. Add after the active item
3822 set_labeled_items(&pane, ["A", "B*", "C"], cx);
3823 pane.update_in(cx, |pane, window, cx| {
3824 pane.add_item(
3825 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3826 false,
3827 false,
3828 Some(2),
3829 window,
3830 cx,
3831 );
3832 });
3833 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3834
3835 // c. Add at the end of the item list (including off the length)
3836 set_labeled_items(&pane, ["A", "B*", "C"], cx);
3837 pane.update_in(cx, |pane, window, cx| {
3838 pane.add_item(
3839 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3840 false,
3841 false,
3842 Some(5),
3843 window,
3844 cx,
3845 );
3846 });
3847 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3848
3849 // 2. Add without a destination index
3850 // a. Add with active item at the start of the item list
3851 set_labeled_items(&pane, ["A*", "B", "C"], cx);
3852 pane.update_in(cx, |pane, window, cx| {
3853 pane.add_item(
3854 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3855 false,
3856 false,
3857 None,
3858 window,
3859 cx,
3860 );
3861 });
3862 set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
3863
3864 // b. Add with active item at the end of the item list
3865 set_labeled_items(&pane, ["A", "B", "C*"], cx);
3866 pane.update_in(cx, |pane, window, cx| {
3867 pane.add_item(
3868 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3869 false,
3870 false,
3871 None,
3872 window,
3873 cx,
3874 );
3875 });
3876 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3877 }
3878
3879 #[gpui::test]
3880 async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
3881 init_test(cx);
3882 let fs = FakeFs::new(cx.executor());
3883
3884 let project = Project::test(fs, None, cx).await;
3885 let (workspace, cx) =
3886 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3887 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3888
3889 // 1. Add with a destination index
3890 // 1a. Add before the active item
3891 let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3892 pane.update_in(cx, |pane, window, cx| {
3893 pane.add_item(d, false, false, Some(0), window, cx);
3894 });
3895 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
3896
3897 // 1b. Add after the active item
3898 let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3899 pane.update_in(cx, |pane, window, cx| {
3900 pane.add_item(d, false, false, Some(2), window, cx);
3901 });
3902 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3903
3904 // 1c. Add at the end of the item list (including off the length)
3905 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3906 pane.update_in(cx, |pane, window, cx| {
3907 pane.add_item(a, false, false, Some(5), window, cx);
3908 });
3909 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3910
3911 // 1d. Add same item to active index
3912 let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3913 pane.update_in(cx, |pane, window, cx| {
3914 pane.add_item(b, false, false, Some(1), window, cx);
3915 });
3916 assert_item_labels(&pane, ["A", "B*", "C"], cx);
3917
3918 // 1e. Add item to index after same item in last position
3919 let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3920 pane.update_in(cx, |pane, window, cx| {
3921 pane.add_item(c, false, false, Some(2), window, cx);
3922 });
3923 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3924
3925 // 2. Add without a destination index
3926 // 2a. Add with active item at the start of the item list
3927 let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
3928 pane.update_in(cx, |pane, window, cx| {
3929 pane.add_item(d, false, false, None, window, cx);
3930 });
3931 assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
3932
3933 // 2b. Add with active item at the end of the item list
3934 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
3935 pane.update_in(cx, |pane, window, cx| {
3936 pane.add_item(a, false, false, None, window, cx);
3937 });
3938 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3939
3940 // 2c. Add active item to active item at end of list
3941 let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
3942 pane.update_in(cx, |pane, window, cx| {
3943 pane.add_item(c, false, false, None, window, cx);
3944 });
3945 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3946
3947 // 2d. Add active item to active item at start of list
3948 let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
3949 pane.update_in(cx, |pane, window, cx| {
3950 pane.add_item(a, false, false, None, window, cx);
3951 });
3952 assert_item_labels(&pane, ["A*", "B", "C"], cx);
3953 }
3954
3955 #[gpui::test]
3956 async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
3957 init_test(cx);
3958 let fs = FakeFs::new(cx.executor());
3959
3960 let project = Project::test(fs, None, cx).await;
3961 let (workspace, cx) =
3962 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3963 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3964
3965 // singleton view
3966 pane.update_in(cx, |pane, window, cx| {
3967 pane.add_item(
3968 Box::new(cx.new(|cx| {
3969 TestItem::new(cx)
3970 .with_singleton(true)
3971 .with_label("buffer 1")
3972 .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
3973 })),
3974 false,
3975 false,
3976 None,
3977 window,
3978 cx,
3979 );
3980 });
3981 assert_item_labels(&pane, ["buffer 1*"], cx);
3982
3983 // new singleton view with the same project entry
3984 pane.update_in(cx, |pane, window, cx| {
3985 pane.add_item(
3986 Box::new(cx.new(|cx| {
3987 TestItem::new(cx)
3988 .with_singleton(true)
3989 .with_label("buffer 1")
3990 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3991 })),
3992 false,
3993 false,
3994 None,
3995 window,
3996 cx,
3997 );
3998 });
3999 assert_item_labels(&pane, ["buffer 1*"], cx);
4000
4001 // new singleton view with different project entry
4002 pane.update_in(cx, |pane, window, cx| {
4003 pane.add_item(
4004 Box::new(cx.new(|cx| {
4005 TestItem::new(cx)
4006 .with_singleton(true)
4007 .with_label("buffer 2")
4008 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
4009 })),
4010 false,
4011 false,
4012 None,
4013 window,
4014 cx,
4015 );
4016 });
4017 assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
4018
4019 // new multibuffer view with the same project entry
4020 pane.update_in(cx, |pane, window, cx| {
4021 pane.add_item(
4022 Box::new(cx.new(|cx| {
4023 TestItem::new(cx)
4024 .with_singleton(false)
4025 .with_label("multibuffer 1")
4026 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4027 })),
4028 false,
4029 false,
4030 None,
4031 window,
4032 cx,
4033 );
4034 });
4035 assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
4036
4037 // another multibuffer view with the same project entry
4038 pane.update_in(cx, |pane, window, cx| {
4039 pane.add_item(
4040 Box::new(cx.new(|cx| {
4041 TestItem::new(cx)
4042 .with_singleton(false)
4043 .with_label("multibuffer 1b")
4044 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4045 })),
4046 false,
4047 false,
4048 None,
4049 window,
4050 cx,
4051 );
4052 });
4053 assert_item_labels(
4054 &pane,
4055 ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
4056 cx,
4057 );
4058 }
4059
4060 #[gpui::test]
4061 async fn test_remove_item_ordering_history(cx: &mut TestAppContext) {
4062 init_test(cx);
4063 let fs = FakeFs::new(cx.executor());
4064
4065 let project = Project::test(fs, None, cx).await;
4066 let (workspace, cx) =
4067 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4068 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4069
4070 add_labeled_item(&pane, "A", false, cx);
4071 add_labeled_item(&pane, "B", false, cx);
4072 add_labeled_item(&pane, "C", false, cx);
4073 add_labeled_item(&pane, "D", false, cx);
4074 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4075
4076 pane.update_in(cx, |pane, window, cx| {
4077 pane.activate_item(1, false, false, window, cx)
4078 });
4079 add_labeled_item(&pane, "1", false, cx);
4080 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
4081
4082 pane.update_in(cx, |pane, window, cx| {
4083 pane.close_active_item(
4084 &CloseActiveItem {
4085 save_intent: None,
4086 close_pinned: false,
4087 },
4088 window,
4089 cx,
4090 )
4091 })
4092 .unwrap()
4093 .await
4094 .unwrap();
4095 assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
4096
4097 pane.update_in(cx, |pane, window, cx| {
4098 pane.activate_item(3, false, false, window, cx)
4099 });
4100 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4101
4102 pane.update_in(cx, |pane, window, cx| {
4103 pane.close_active_item(
4104 &CloseActiveItem {
4105 save_intent: None,
4106 close_pinned: false,
4107 },
4108 window,
4109 cx,
4110 )
4111 })
4112 .unwrap()
4113 .await
4114 .unwrap();
4115 assert_item_labels(&pane, ["A", "B*", "C"], cx);
4116
4117 pane.update_in(cx, |pane, window, cx| {
4118 pane.close_active_item(
4119 &CloseActiveItem {
4120 save_intent: None,
4121 close_pinned: false,
4122 },
4123 window,
4124 cx,
4125 )
4126 })
4127 .unwrap()
4128 .await
4129 .unwrap();
4130 assert_item_labels(&pane, ["A", "C*"], cx);
4131
4132 pane.update_in(cx, |pane, window, cx| {
4133 pane.close_active_item(
4134 &CloseActiveItem {
4135 save_intent: None,
4136 close_pinned: false,
4137 },
4138 window,
4139 cx,
4140 )
4141 })
4142 .unwrap()
4143 .await
4144 .unwrap();
4145 assert_item_labels(&pane, ["A*"], cx);
4146 }
4147
4148 #[gpui::test]
4149 async fn test_remove_item_ordering_neighbour(cx: &mut TestAppContext) {
4150 init_test(cx);
4151 cx.update_global::<SettingsStore, ()>(|s, cx| {
4152 s.update_user_settings::<ItemSettings>(cx, |s| {
4153 s.activate_on_close = Some(ActivateOnClose::Neighbour);
4154 });
4155 });
4156 let fs = FakeFs::new(cx.executor());
4157
4158 let project = Project::test(fs, None, cx).await;
4159 let (workspace, cx) =
4160 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4161 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4162
4163 add_labeled_item(&pane, "A", false, cx);
4164 add_labeled_item(&pane, "B", false, cx);
4165 add_labeled_item(&pane, "C", false, cx);
4166 add_labeled_item(&pane, "D", false, cx);
4167 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4168
4169 pane.update_in(cx, |pane, window, cx| {
4170 pane.activate_item(1, false, false, window, cx)
4171 });
4172 add_labeled_item(&pane, "1", false, cx);
4173 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
4174
4175 pane.update_in(cx, |pane, window, cx| {
4176 pane.close_active_item(
4177 &CloseActiveItem {
4178 save_intent: None,
4179 close_pinned: false,
4180 },
4181 window,
4182 cx,
4183 )
4184 })
4185 .unwrap()
4186 .await
4187 .unwrap();
4188 assert_item_labels(&pane, ["A", "B", "C*", "D"], cx);
4189
4190 pane.update_in(cx, |pane, window, cx| {
4191 pane.activate_item(3, false, false, window, cx)
4192 });
4193 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4194
4195 pane.update_in(cx, |pane, window, cx| {
4196 pane.close_active_item(
4197 &CloseActiveItem {
4198 save_intent: None,
4199 close_pinned: false,
4200 },
4201 window,
4202 cx,
4203 )
4204 })
4205 .unwrap()
4206 .await
4207 .unwrap();
4208 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4209
4210 pane.update_in(cx, |pane, window, cx| {
4211 pane.close_active_item(
4212 &CloseActiveItem {
4213 save_intent: None,
4214 close_pinned: false,
4215 },
4216 window,
4217 cx,
4218 )
4219 })
4220 .unwrap()
4221 .await
4222 .unwrap();
4223 assert_item_labels(&pane, ["A", "B*"], cx);
4224
4225 pane.update_in(cx, |pane, window, cx| {
4226 pane.close_active_item(
4227 &CloseActiveItem {
4228 save_intent: None,
4229 close_pinned: false,
4230 },
4231 window,
4232 cx,
4233 )
4234 })
4235 .unwrap()
4236 .await
4237 .unwrap();
4238 assert_item_labels(&pane, ["A*"], cx);
4239 }
4240
4241 #[gpui::test]
4242 async fn test_remove_item_ordering_left_neighbour(cx: &mut TestAppContext) {
4243 init_test(cx);
4244 cx.update_global::<SettingsStore, ()>(|s, cx| {
4245 s.update_user_settings::<ItemSettings>(cx, |s| {
4246 s.activate_on_close = Some(ActivateOnClose::LeftNeighbour);
4247 });
4248 });
4249 let fs = FakeFs::new(cx.executor());
4250
4251 let project = Project::test(fs, None, cx).await;
4252 let (workspace, cx) =
4253 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4254 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4255
4256 add_labeled_item(&pane, "A", false, cx);
4257 add_labeled_item(&pane, "B", false, cx);
4258 add_labeled_item(&pane, "C", false, cx);
4259 add_labeled_item(&pane, "D", false, cx);
4260 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4261
4262 pane.update_in(cx, |pane, window, cx| {
4263 pane.activate_item(1, false, false, window, cx)
4264 });
4265 add_labeled_item(&pane, "1", false, cx);
4266 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
4267
4268 pane.update_in(cx, |pane, window, cx| {
4269 pane.close_active_item(
4270 &CloseActiveItem {
4271 save_intent: None,
4272 close_pinned: false,
4273 },
4274 window,
4275 cx,
4276 )
4277 })
4278 .unwrap()
4279 .await
4280 .unwrap();
4281 assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
4282
4283 pane.update_in(cx, |pane, window, cx| {
4284 pane.activate_item(3, false, false, window, cx)
4285 });
4286 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4287
4288 pane.update_in(cx, |pane, window, cx| {
4289 pane.close_active_item(
4290 &CloseActiveItem {
4291 save_intent: None,
4292 close_pinned: false,
4293 },
4294 window,
4295 cx,
4296 )
4297 })
4298 .unwrap()
4299 .await
4300 .unwrap();
4301 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4302
4303 pane.update_in(cx, |pane, window, cx| {
4304 pane.activate_item(0, false, false, window, cx)
4305 });
4306 assert_item_labels(&pane, ["A*", "B", "C"], cx);
4307
4308 pane.update_in(cx, |pane, window, cx| {
4309 pane.close_active_item(
4310 &CloseActiveItem {
4311 save_intent: None,
4312 close_pinned: false,
4313 },
4314 window,
4315 cx,
4316 )
4317 })
4318 .unwrap()
4319 .await
4320 .unwrap();
4321 assert_item_labels(&pane, ["B*", "C"], cx);
4322
4323 pane.update_in(cx, |pane, window, cx| {
4324 pane.close_active_item(
4325 &CloseActiveItem {
4326 save_intent: None,
4327 close_pinned: false,
4328 },
4329 window,
4330 cx,
4331 )
4332 })
4333 .unwrap()
4334 .await
4335 .unwrap();
4336 assert_item_labels(&pane, ["C*"], cx);
4337 }
4338
4339 #[gpui::test]
4340 async fn test_close_inactive_items(cx: &mut TestAppContext) {
4341 init_test(cx);
4342 let fs = FakeFs::new(cx.executor());
4343
4344 let project = Project::test(fs, None, cx).await;
4345 let (workspace, cx) =
4346 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4347 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4348
4349 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
4350
4351 pane.update_in(cx, |pane, window, cx| {
4352 pane.close_inactive_items(
4353 &CloseInactiveItems {
4354 save_intent: None,
4355 close_pinned: false,
4356 },
4357 window,
4358 cx,
4359 )
4360 })
4361 .unwrap()
4362 .await
4363 .unwrap();
4364 assert_item_labels(&pane, ["C*"], cx);
4365 }
4366
4367 #[gpui::test]
4368 async fn test_close_clean_items(cx: &mut TestAppContext) {
4369 init_test(cx);
4370 let fs = FakeFs::new(cx.executor());
4371
4372 let project = Project::test(fs, None, cx).await;
4373 let (workspace, cx) =
4374 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4375 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4376
4377 add_labeled_item(&pane, "A", true, cx);
4378 add_labeled_item(&pane, "B", false, cx);
4379 add_labeled_item(&pane, "C", true, cx);
4380 add_labeled_item(&pane, "D", false, cx);
4381 add_labeled_item(&pane, "E", false, cx);
4382 assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
4383
4384 pane.update_in(cx, |pane, window, cx| {
4385 pane.close_clean_items(
4386 &CloseCleanItems {
4387 close_pinned: false,
4388 },
4389 window,
4390 cx,
4391 )
4392 })
4393 .unwrap()
4394 .await
4395 .unwrap();
4396 assert_item_labels(&pane, ["A^", "C*^"], cx);
4397 }
4398
4399 #[gpui::test]
4400 async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
4401 init_test(cx);
4402 let fs = FakeFs::new(cx.executor());
4403
4404 let project = Project::test(fs, None, cx).await;
4405 let (workspace, cx) =
4406 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4407 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4408
4409 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
4410
4411 pane.update_in(cx, |pane, window, cx| {
4412 pane.close_items_to_the_left(
4413 &CloseItemsToTheLeft {
4414 close_pinned: false,
4415 },
4416 window,
4417 cx,
4418 )
4419 })
4420 .unwrap()
4421 .await
4422 .unwrap();
4423 assert_item_labels(&pane, ["C*", "D", "E"], cx);
4424 }
4425
4426 #[gpui::test]
4427 async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
4428 init_test(cx);
4429 let fs = FakeFs::new(cx.executor());
4430
4431 let project = Project::test(fs, None, cx).await;
4432 let (workspace, cx) =
4433 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4434 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4435
4436 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
4437
4438 pane.update_in(cx, |pane, window, cx| {
4439 pane.close_items_to_the_right(
4440 &CloseItemsToTheRight {
4441 close_pinned: false,
4442 },
4443 window,
4444 cx,
4445 )
4446 })
4447 .unwrap()
4448 .await
4449 .unwrap();
4450 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4451 }
4452
4453 #[gpui::test]
4454 async fn test_close_all_items(cx: &mut TestAppContext) {
4455 init_test(cx);
4456 let fs = FakeFs::new(cx.executor());
4457
4458 let project = Project::test(fs, None, cx).await;
4459 let (workspace, cx) =
4460 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4461 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4462
4463 let item_a = add_labeled_item(&pane, "A", false, cx);
4464 add_labeled_item(&pane, "B", false, cx);
4465 add_labeled_item(&pane, "C", false, cx);
4466 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4467
4468 pane.update_in(cx, |pane, window, cx| {
4469 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4470 pane.pin_tab_at(ix, window, cx);
4471 pane.close_all_items(
4472 &CloseAllItems {
4473 save_intent: None,
4474 close_pinned: false,
4475 },
4476 window,
4477 cx,
4478 )
4479 })
4480 .unwrap()
4481 .await
4482 .unwrap();
4483 assert_item_labels(&pane, ["A*"], cx);
4484
4485 pane.update_in(cx, |pane, window, cx| {
4486 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4487 pane.unpin_tab_at(ix, window, cx);
4488 pane.close_all_items(
4489 &CloseAllItems {
4490 save_intent: None,
4491 close_pinned: false,
4492 },
4493 window,
4494 cx,
4495 )
4496 })
4497 .unwrap()
4498 .await
4499 .unwrap();
4500
4501 assert_item_labels(&pane, [], cx);
4502
4503 add_labeled_item(&pane, "A", true, cx).update(cx, |item, cx| {
4504 item.project_items
4505 .push(TestProjectItem::new_dirty(1, "A.txt", cx))
4506 });
4507 add_labeled_item(&pane, "B", true, cx).update(cx, |item, cx| {
4508 item.project_items
4509 .push(TestProjectItem::new_dirty(2, "B.txt", cx))
4510 });
4511 add_labeled_item(&pane, "C", true, cx).update(cx, |item, cx| {
4512 item.project_items
4513 .push(TestProjectItem::new_dirty(3, "C.txt", cx))
4514 });
4515 assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
4516
4517 let save = pane
4518 .update_in(cx, |pane, window, cx| {
4519 pane.close_all_items(
4520 &CloseAllItems {
4521 save_intent: None,
4522 close_pinned: false,
4523 },
4524 window,
4525 cx,
4526 )
4527 })
4528 .unwrap();
4529
4530 cx.executor().run_until_parked();
4531 cx.simulate_prompt_answer("Save all");
4532 save.await.unwrap();
4533 assert_item_labels(&pane, [], cx);
4534
4535 add_labeled_item(&pane, "A", true, cx);
4536 add_labeled_item(&pane, "B", true, cx);
4537 add_labeled_item(&pane, "C", true, cx);
4538 assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
4539 let save = pane
4540 .update_in(cx, |pane, window, cx| {
4541 pane.close_all_items(
4542 &CloseAllItems {
4543 save_intent: None,
4544 close_pinned: false,
4545 },
4546 window,
4547 cx,
4548 )
4549 })
4550 .unwrap();
4551
4552 cx.executor().run_until_parked();
4553 cx.simulate_prompt_answer("Discard all");
4554 save.await.unwrap();
4555 assert_item_labels(&pane, [], cx);
4556 }
4557
4558 #[gpui::test]
4559 async fn test_close_with_save_intent(cx: &mut TestAppContext) {
4560 init_test(cx);
4561 let fs = FakeFs::new(cx.executor());
4562
4563 let project = Project::test(fs, None, cx).await;
4564 let (workspace, cx) =
4565 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
4566 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4567
4568 let a = cx.update(|_, cx| TestProjectItem::new_dirty(1, "A.txt", cx));
4569 let b = cx.update(|_, cx| TestProjectItem::new_dirty(1, "B.txt", cx));
4570 let c = cx.update(|_, cx| TestProjectItem::new_dirty(1, "C.txt", cx));
4571
4572 add_labeled_item(&pane, "AB", true, cx).update(cx, |item, _| {
4573 item.project_items.push(a.clone());
4574 item.project_items.push(b.clone());
4575 });
4576 add_labeled_item(&pane, "C", true, cx)
4577 .update(cx, |item, _| item.project_items.push(c.clone()));
4578 assert_item_labels(&pane, ["AB^", "C*^"], cx);
4579
4580 pane.update_in(cx, |pane, window, cx| {
4581 pane.close_all_items(
4582 &CloseAllItems {
4583 save_intent: Some(SaveIntent::Save),
4584 close_pinned: false,
4585 },
4586 window,
4587 cx,
4588 )
4589 })
4590 .unwrap()
4591 .await
4592 .unwrap();
4593
4594 assert_item_labels(&pane, [], cx);
4595 cx.update(|_, cx| {
4596 assert!(!a.read(cx).is_dirty);
4597 assert!(!b.read(cx).is_dirty);
4598 assert!(!c.read(cx).is_dirty);
4599 });
4600 }
4601
4602 #[gpui::test]
4603 async fn test_close_all_items_including_pinned(cx: &mut TestAppContext) {
4604 init_test(cx);
4605 let fs = FakeFs::new(cx.executor());
4606
4607 let project = Project::test(fs, None, cx).await;
4608 let (workspace, cx) =
4609 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
4610 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4611
4612 let item_a = add_labeled_item(&pane, "A", false, cx);
4613 add_labeled_item(&pane, "B", false, cx);
4614 add_labeled_item(&pane, "C", false, cx);
4615 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4616
4617 pane.update_in(cx, |pane, window, cx| {
4618 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4619 pane.pin_tab_at(ix, window, cx);
4620 pane.close_all_items(
4621 &CloseAllItems {
4622 save_intent: None,
4623 close_pinned: true,
4624 },
4625 window,
4626 cx,
4627 )
4628 })
4629 .unwrap()
4630 .await
4631 .unwrap();
4632 assert_item_labels(&pane, [], cx);
4633 }
4634
4635 #[gpui::test]
4636 async fn test_close_pinned_tab_with_non_pinned_in_same_pane(cx: &mut TestAppContext) {
4637 init_test(cx);
4638 let fs = FakeFs::new(cx.executor());
4639 let project = Project::test(fs, None, cx).await;
4640 let (workspace, cx) =
4641 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
4642
4643 // Non-pinned tabs in same pane
4644 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4645 add_labeled_item(&pane, "A", false, cx);
4646 add_labeled_item(&pane, "B", false, cx);
4647 add_labeled_item(&pane, "C", false, cx);
4648 pane.update_in(cx, |pane, window, cx| {
4649 pane.pin_tab_at(0, window, cx);
4650 });
4651 set_labeled_items(&pane, ["A*", "B", "C"], cx);
4652 pane.update_in(cx, |pane, window, cx| {
4653 pane.close_active_item(
4654 &CloseActiveItem {
4655 save_intent: None,
4656 close_pinned: false,
4657 },
4658 window,
4659 cx,
4660 );
4661 });
4662 // Non-pinned tab should be active
4663 assert_item_labels(&pane, ["A", "B*", "C"], cx);
4664 }
4665
4666 #[gpui::test]
4667 async fn test_close_pinned_tab_with_non_pinned_in_different_pane(cx: &mut TestAppContext) {
4668 init_test(cx);
4669 let fs = FakeFs::new(cx.executor());
4670 let project = Project::test(fs, None, cx).await;
4671 let (workspace, cx) =
4672 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
4673
4674 // No non-pinned tabs in same pane, non-pinned tabs in another pane
4675 let pane1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4676 let pane2 = workspace.update_in(cx, |workspace, window, cx| {
4677 workspace.split_pane(pane1.clone(), SplitDirection::Right, window, cx)
4678 });
4679 add_labeled_item(&pane1, "A", false, cx);
4680 pane1.update_in(cx, |pane, window, cx| {
4681 pane.pin_tab_at(0, window, cx);
4682 });
4683 set_labeled_items(&pane1, ["A*"], cx);
4684 add_labeled_item(&pane2, "B", false, cx);
4685 set_labeled_items(&pane2, ["B"], cx);
4686 pane1.update_in(cx, |pane, window, cx| {
4687 pane.close_active_item(
4688 &CloseActiveItem {
4689 save_intent: None,
4690 close_pinned: false,
4691 },
4692 window,
4693 cx,
4694 );
4695 });
4696 // Non-pinned tab of other pane should be active
4697 assert_item_labels(&pane2, ["B*"], cx);
4698 }
4699
4700 fn init_test(cx: &mut TestAppContext) {
4701 cx.update(|cx| {
4702 let settings_store = SettingsStore::test(cx);
4703 cx.set_global(settings_store);
4704 theme::init(LoadThemes::JustBase, cx);
4705 crate::init_settings(cx);
4706 Project::init_settings(cx);
4707 });
4708 }
4709
4710 fn set_max_tabs(cx: &mut TestAppContext, value: Option<usize>) {
4711 cx.update_global(|store: &mut SettingsStore, cx| {
4712 store.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4713 settings.max_tabs = value.map(|v| NonZero::new(v).unwrap())
4714 });
4715 });
4716 }
4717
4718 fn add_labeled_item(
4719 pane: &Entity<Pane>,
4720 label: &str,
4721 is_dirty: bool,
4722 cx: &mut VisualTestContext,
4723 ) -> Box<Entity<TestItem>> {
4724 pane.update_in(cx, |pane, window, cx| {
4725 let labeled_item =
4726 Box::new(cx.new(|cx| TestItem::new(cx).with_label(label).with_dirty(is_dirty)));
4727 pane.add_item(labeled_item.clone(), false, false, None, window, cx);
4728 labeled_item
4729 })
4730 }
4731
4732 fn set_labeled_items<const COUNT: usize>(
4733 pane: &Entity<Pane>,
4734 labels: [&str; COUNT],
4735 cx: &mut VisualTestContext,
4736 ) -> [Box<Entity<TestItem>>; COUNT] {
4737 pane.update_in(cx, |pane, window, cx| {
4738 pane.items.clear();
4739 let mut active_item_index = 0;
4740
4741 let mut index = 0;
4742 let items = labels.map(|mut label| {
4743 if label.ends_with('*') {
4744 label = label.trim_end_matches('*');
4745 active_item_index = index;
4746 }
4747
4748 let labeled_item = Box::new(cx.new(|cx| TestItem::new(cx).with_label(label)));
4749 pane.add_item(labeled_item.clone(), false, false, None, window, cx);
4750 index += 1;
4751 labeled_item
4752 });
4753
4754 pane.activate_item(active_item_index, false, false, window, cx);
4755
4756 items
4757 })
4758 }
4759
4760 // Assert the item label, with the active item label suffixed with a '*'
4761 #[track_caller]
4762 fn assert_item_labels<const COUNT: usize>(
4763 pane: &Entity<Pane>,
4764 expected_states: [&str; COUNT],
4765 cx: &mut VisualTestContext,
4766 ) {
4767 let actual_states = pane.update(cx, |pane, cx| {
4768 pane.items
4769 .iter()
4770 .enumerate()
4771 .map(|(ix, item)| {
4772 let mut state = item
4773 .to_any()
4774 .downcast::<TestItem>()
4775 .unwrap()
4776 .read(cx)
4777 .label
4778 .clone();
4779 if ix == pane.active_item_index {
4780 state.push('*');
4781 }
4782 if item.is_dirty(cx) {
4783 state.push('^');
4784 }
4785 state
4786 })
4787 .collect::<Vec<_>>()
4788 });
4789 assert_eq!(
4790 actual_states, expected_states,
4791 "pane items do not match expectation"
4792 );
4793 }
4794}