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