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