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