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