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, _, cx| {
1338 tab.bg(cx.theme().colors().drop_target_background)
1339 })
1340 .drag_over::<ProjectEntryId>(|tab, _, cx| {
1341 tab.bg(cx.theme().colors().drop_target_background)
1342 })
1343 .when_some(self.can_drop_predicate.clone(), |this, p| {
1344 this.can_drop(move |a, cx| p(a, cx))
1345 })
1346 .on_drop(cx.listener(move |this, dragged_tab: &DraggedTab, cx| {
1347 this.drag_split_direction = None;
1348 this.handle_tab_drop(dragged_tab, ix, cx)
1349 }))
1350 .on_drop(cx.listener(move |this, entry_id: &ProjectEntryId, cx| {
1351 this.drag_split_direction = None;
1352 this.handle_project_entry_drop(entry_id, cx)
1353 }))
1354 .on_drop(cx.listener(move |this, paths, cx| {
1355 this.drag_split_direction = None;
1356 this.handle_external_paths_drop(paths, cx)
1357 }))
1358 .when_some(item.tab_tooltip_text(cx), |tab, text| {
1359 tab.tooltip(move |cx| Tooltip::text(text.clone(), cx))
1360 })
1361 .start_slot::<Indicator>(indicator)
1362 .end_slot(
1363 IconButton::new("close tab", IconName::Close)
1364 .shape(IconButtonShape::Square)
1365 .icon_color(Color::Muted)
1366 .size(ButtonSize::None)
1367 .icon_size(IconSize::XSmall)
1368 .on_click(cx.listener(move |pane, _, cx| {
1369 pane.close_item_by_id(item_id, SaveIntent::Close, cx)
1370 .detach_and_log_err(cx);
1371 })),
1372 )
1373 .child(label);
1374
1375 let single_entry_to_resolve = {
1376 let item_entries = self.items[ix].project_entry_ids(cx);
1377 if item_entries.len() == 1 {
1378 Some(item_entries[0])
1379 } else {
1380 None
1381 }
1382 };
1383
1384 let pane = cx.view().downgrade();
1385 right_click_menu(ix).trigger(tab).menu(move |cx| {
1386 let pane = pane.clone();
1387 ContextMenu::build(cx, move |mut menu, cx| {
1388 if let Some(pane) = pane.upgrade() {
1389 menu = menu
1390 .entry(
1391 "Close",
1392 Some(Box::new(CloseActiveItem { save_intent: None })),
1393 cx.handler_for(&pane, move |pane, cx| {
1394 pane.close_item_by_id(item_id, SaveIntent::Close, cx)
1395 .detach_and_log_err(cx);
1396 }),
1397 )
1398 .entry(
1399 "Close Others",
1400 Some(Box::new(CloseInactiveItems)),
1401 cx.handler_for(&pane, move |pane, cx| {
1402 pane.close_items(cx, SaveIntent::Close, |id| id != item_id)
1403 .detach_and_log_err(cx);
1404 }),
1405 )
1406 .separator()
1407 .entry(
1408 "Close Left",
1409 Some(Box::new(CloseItemsToTheLeft)),
1410 cx.handler_for(&pane, move |pane, cx| {
1411 pane.close_items_to_the_left_by_id(item_id, cx)
1412 .detach_and_log_err(cx);
1413 }),
1414 )
1415 .entry(
1416 "Close Right",
1417 Some(Box::new(CloseItemsToTheRight)),
1418 cx.handler_for(&pane, move |pane, cx| {
1419 pane.close_items_to_the_right_by_id(item_id, cx)
1420 .detach_and_log_err(cx);
1421 }),
1422 )
1423 .separator()
1424 .entry(
1425 "Close Clean",
1426 Some(Box::new(CloseCleanItems)),
1427 cx.handler_for(&pane, move |pane, cx| {
1428 pane.close_clean_items(&CloseCleanItems, cx)
1429 .map(|task| task.detach_and_log_err(cx));
1430 }),
1431 )
1432 .entry(
1433 "Close All",
1434 Some(Box::new(CloseAllItems { save_intent: None })),
1435 cx.handler_for(&pane, |pane, cx| {
1436 pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
1437 .map(|task| task.detach_and_log_err(cx));
1438 }),
1439 );
1440
1441 if let Some(entry) = single_entry_to_resolve {
1442 let entry_id = entry.to_proto();
1443 menu = menu.separator().entry(
1444 "Reveal In Project Panel",
1445 Some(Box::new(RevealInProjectPanel { entry_id })),
1446 cx.handler_for(&pane, move |pane, cx| {
1447 pane.project.update(cx, |_, cx| {
1448 cx.emit(project::Event::RevealInProjectPanel(
1449 ProjectEntryId::from_proto(entry_id),
1450 ))
1451 });
1452 }),
1453 );
1454 }
1455 }
1456
1457 menu
1458 })
1459 })
1460 }
1461
1462 fn render_tab_bar(&mut self, cx: &mut ViewContext<'_, Pane>) -> impl IntoElement {
1463 TabBar::new("tab_bar")
1464 .track_scroll(self.tab_bar_scroll_handle.clone())
1465 .when(self.display_nav_history_buttons, |tab_bar| {
1466 tab_bar.start_child(
1467 h_flex()
1468 .gap_2()
1469 .child(
1470 IconButton::new("navigate_backward", IconName::ArrowLeft)
1471 .icon_size(IconSize::Small)
1472 .on_click({
1473 let view = cx.view().clone();
1474 move |_, cx| view.update(cx, Self::navigate_backward)
1475 })
1476 .disabled(!self.can_navigate_backward())
1477 .tooltip(|cx| Tooltip::for_action("Go Back", &GoBack, cx)),
1478 )
1479 .child(
1480 IconButton::new("navigate_forward", IconName::ArrowRight)
1481 .icon_size(IconSize::Small)
1482 .on_click({
1483 let view = cx.view().clone();
1484 move |_, cx| view.update(cx, Self::navigate_forward)
1485 })
1486 .disabled(!self.can_navigate_forward())
1487 .tooltip(|cx| Tooltip::for_action("Go Forward", &GoForward, cx)),
1488 ),
1489 )
1490 })
1491 .when(self.has_focus(cx), |tab_bar| {
1492 tab_bar.end_child({
1493 let render_tab_buttons = self.render_tab_bar_buttons.clone();
1494 render_tab_buttons(self, cx)
1495 })
1496 })
1497 .children(
1498 self.items
1499 .iter()
1500 .enumerate()
1501 .zip(self.tab_details(cx))
1502 .map(|((ix, item), detail)| self.render_tab(ix, item, detail, cx)),
1503 )
1504 .child(
1505 div()
1506 .min_w_6()
1507 // HACK: This empty child is currently necessary to force the drop target to appear
1508 // despite us setting a min width above.
1509 .child("")
1510 .h_full()
1511 .flex_grow()
1512 .drag_over::<DraggedTab>(|bar, _, cx| {
1513 bar.bg(cx.theme().colors().drop_target_background)
1514 })
1515 .drag_over::<ProjectEntryId>(|bar, _, cx| {
1516 bar.bg(cx.theme().colors().drop_target_background)
1517 })
1518 .on_drop(cx.listener(move |this, dragged_tab: &DraggedTab, cx| {
1519 this.drag_split_direction = None;
1520 this.handle_tab_drop(dragged_tab, this.items.len(), cx)
1521 }))
1522 .on_drop(cx.listener(move |this, entry_id: &ProjectEntryId, cx| {
1523 this.drag_split_direction = None;
1524 this.handle_project_entry_drop(entry_id, cx)
1525 }))
1526 .on_drop(cx.listener(move |this, paths, cx| {
1527 this.drag_split_direction = None;
1528 this.handle_external_paths_drop(paths, cx)
1529 })),
1530 )
1531 }
1532
1533 fn render_menu_overlay(menu: &View<ContextMenu>) -> Div {
1534 div()
1535 .absolute()
1536 .z_index(1)
1537 .bottom_0()
1538 .right_0()
1539 .size_0()
1540 .child(overlay().anchor(AnchorCorner::TopRight).child(menu.clone()))
1541 }
1542
1543 fn tab_details(&self, cx: &AppContext) -> Vec<usize> {
1544 let mut tab_details = self.items.iter().map(|_| 0).collect::<Vec<_>>();
1545
1546 let mut tab_descriptions = HashMap::default();
1547 let mut done = false;
1548 while !done {
1549 done = true;
1550
1551 // Store item indices by their tab description.
1552 for (ix, (item, detail)) in self.items.iter().zip(&tab_details).enumerate() {
1553 if let Some(description) = item.tab_description(*detail, cx) {
1554 if *detail == 0
1555 || Some(&description) != item.tab_description(detail - 1, cx).as_ref()
1556 {
1557 tab_descriptions
1558 .entry(description)
1559 .or_insert(Vec::new())
1560 .push(ix);
1561 }
1562 }
1563 }
1564
1565 // If two or more items have the same tab description, increase eir level
1566 // of detail and try again.
1567 for (_, item_ixs) in tab_descriptions.drain() {
1568 if item_ixs.len() > 1 {
1569 done = false;
1570 for ix in item_ixs {
1571 tab_details[ix] += 1;
1572 }
1573 }
1574 }
1575 }
1576
1577 tab_details
1578 }
1579
1580 pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
1581 self.zoomed = zoomed;
1582 cx.notify();
1583 }
1584
1585 pub fn is_zoomed(&self) -> bool {
1586 self.zoomed
1587 }
1588
1589 fn handle_drag_move<T>(&mut self, event: &DragMoveEvent<T>, cx: &mut ViewContext<Self>) {
1590 if !self.can_split {
1591 return;
1592 }
1593
1594 let edge_width = cx.rem_size() * 8;
1595 let cursor = event.event.position;
1596 let direction = if cursor.x < event.bounds.left() + edge_width {
1597 Some(SplitDirection::Left)
1598 } else if cursor.x > event.bounds.right() - edge_width {
1599 Some(SplitDirection::Right)
1600 } else if cursor.y < event.bounds.top() + edge_width {
1601 Some(SplitDirection::Up)
1602 } else if cursor.y > event.bounds.bottom() - edge_width {
1603 Some(SplitDirection::Down)
1604 } else {
1605 None
1606 };
1607
1608 if direction != self.drag_split_direction {
1609 self.drag_split_direction = direction;
1610 }
1611 }
1612
1613 fn handle_tab_drop(
1614 &mut self,
1615 dragged_tab: &DraggedTab,
1616 ix: usize,
1617 cx: &mut ViewContext<'_, Self>,
1618 ) {
1619 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
1620 if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_tab, cx) {
1621 return;
1622 }
1623 }
1624 let mut to_pane = cx.view().clone();
1625 let split_direction = self.drag_split_direction;
1626 let item_id = dragged_tab.item.item_id();
1627 let from_pane = dragged_tab.pane.clone();
1628 self.workspace
1629 .update(cx, |_, cx| {
1630 cx.defer(move |workspace, cx| {
1631 if let Some(split_direction) = split_direction {
1632 to_pane = workspace.split_pane(to_pane, split_direction, cx);
1633 }
1634 workspace.move_item(from_pane, to_pane, item_id, ix, cx);
1635 });
1636 })
1637 .log_err();
1638 }
1639
1640 fn handle_project_entry_drop(
1641 &mut self,
1642 project_entry_id: &ProjectEntryId,
1643 cx: &mut ViewContext<'_, Self>,
1644 ) {
1645 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
1646 if let ControlFlow::Break(()) = custom_drop_handle(self, project_entry_id, cx) {
1647 return;
1648 }
1649 }
1650 let mut to_pane = cx.view().clone();
1651 let split_direction = self.drag_split_direction;
1652 let project_entry_id = *project_entry_id;
1653 self.workspace
1654 .update(cx, |_, cx| {
1655 cx.defer(move |workspace, cx| {
1656 if let Some(path) = workspace
1657 .project()
1658 .read(cx)
1659 .path_for_entry(project_entry_id, cx)
1660 {
1661 if let Some(split_direction) = split_direction {
1662 to_pane = workspace.split_pane(to_pane, split_direction, cx);
1663 }
1664 workspace
1665 .open_path(path, Some(to_pane.downgrade()), true, cx)
1666 .detach_and_log_err(cx);
1667 }
1668 });
1669 })
1670 .log_err();
1671 }
1672
1673 fn handle_external_paths_drop(
1674 &mut self,
1675 paths: &ExternalPaths,
1676 cx: &mut ViewContext<'_, Self>,
1677 ) {
1678 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
1679 if let ControlFlow::Break(()) = custom_drop_handle(self, paths, cx) {
1680 return;
1681 }
1682 }
1683 let mut to_pane = cx.view().clone();
1684 let mut split_direction = self.drag_split_direction;
1685 let paths = paths.paths().to_vec();
1686 self.workspace
1687 .update(cx, |workspace, cx| {
1688 let fs = Arc::clone(workspace.project().read(cx).fs());
1689 cx.spawn(|workspace, mut cx| async move {
1690 let mut is_file_checks = FuturesUnordered::new();
1691 for path in &paths {
1692 is_file_checks.push(fs.is_file(path))
1693 }
1694 let mut has_files_to_open = false;
1695 while let Some(is_file) = is_file_checks.next().await {
1696 if is_file {
1697 has_files_to_open = true;
1698 break;
1699 }
1700 }
1701 drop(is_file_checks);
1702 if !has_files_to_open {
1703 split_direction = None;
1704 }
1705
1706 if let Some(open_task) = workspace
1707 .update(&mut cx, |workspace, cx| {
1708 if let Some(split_direction) = split_direction {
1709 to_pane = workspace.split_pane(to_pane, split_direction, cx);
1710 }
1711 workspace.open_paths(
1712 paths,
1713 OpenVisible::OnlyDirectories,
1714 Some(to_pane.downgrade()),
1715 cx,
1716 )
1717 })
1718 .ok()
1719 {
1720 let _opened_items: Vec<_> = open_task.await;
1721 }
1722 })
1723 .detach();
1724 })
1725 .log_err();
1726 }
1727
1728 pub fn display_nav_history_buttons(&mut self, display: bool) {
1729 self.display_nav_history_buttons = display;
1730 }
1731}
1732
1733impl FocusableView for Pane {
1734 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
1735 self.focus_handle.clone()
1736 }
1737}
1738
1739impl Render for Pane {
1740 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
1741 v_flex()
1742 .key_context("Pane")
1743 .track_focus(&self.focus_handle)
1744 .size_full()
1745 .flex_none()
1746 .overflow_hidden()
1747 .on_action(cx.listener(|pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx)))
1748 .on_action(cx.listener(|pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx)))
1749 .on_action(
1750 cx.listener(|pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx)),
1751 )
1752 .on_action(cx.listener(|pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx)))
1753 .on_action(cx.listener(|pane, _: &GoBack, cx| pane.navigate_backward(cx)))
1754 .on_action(cx.listener(|pane, _: &GoForward, cx| pane.navigate_forward(cx)))
1755 .on_action(cx.listener(Pane::toggle_zoom))
1756 .on_action(cx.listener(|pane: &mut Pane, action: &ActivateItem, cx| {
1757 pane.activate_item(action.0, true, true, cx);
1758 }))
1759 .on_action(cx.listener(|pane: &mut Pane, _: &ActivateLastItem, cx| {
1760 pane.activate_item(pane.items.len() - 1, true, true, cx);
1761 }))
1762 .on_action(cx.listener(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
1763 pane.activate_prev_item(true, cx);
1764 }))
1765 .on_action(cx.listener(|pane: &mut Pane, _: &ActivateNextItem, cx| {
1766 pane.activate_next_item(true, cx);
1767 }))
1768 .on_action(
1769 cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
1770 pane.close_active_item(action, cx)
1771 .map(|task| task.detach_and_log_err(cx));
1772 }),
1773 )
1774 .on_action(
1775 cx.listener(|pane: &mut Self, action: &CloseInactiveItems, cx| {
1776 pane.close_inactive_items(action, cx)
1777 .map(|task| task.detach_and_log_err(cx));
1778 }),
1779 )
1780 .on_action(
1781 cx.listener(|pane: &mut Self, action: &CloseCleanItems, cx| {
1782 pane.close_clean_items(action, cx)
1783 .map(|task| task.detach_and_log_err(cx));
1784 }),
1785 )
1786 .on_action(
1787 cx.listener(|pane: &mut Self, action: &CloseItemsToTheLeft, cx| {
1788 pane.close_items_to_the_left(action, cx)
1789 .map(|task| task.detach_and_log_err(cx));
1790 }),
1791 )
1792 .on_action(
1793 cx.listener(|pane: &mut Self, action: &CloseItemsToTheRight, cx| {
1794 pane.close_items_to_the_right(action, cx)
1795 .map(|task| task.detach_and_log_err(cx));
1796 }),
1797 )
1798 .on_action(cx.listener(|pane: &mut Self, action: &CloseAllItems, cx| {
1799 pane.close_all_items(action, cx)
1800 .map(|task| task.detach_and_log_err(cx));
1801 }))
1802 .on_action(
1803 cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
1804 pane.close_active_item(action, cx)
1805 .map(|task| task.detach_and_log_err(cx));
1806 }),
1807 )
1808 .on_action(
1809 cx.listener(|pane: &mut Self, action: &RevealInProjectPanel, cx| {
1810 pane.project.update(cx, |_, cx| {
1811 cx.emit(project::Event::RevealInProjectPanel(
1812 ProjectEntryId::from_proto(action.entry_id),
1813 ))
1814 })
1815 }),
1816 )
1817 .when(self.active_item().is_some(), |pane| {
1818 pane.child(self.render_tab_bar(cx))
1819 })
1820 .child({
1821 let has_worktrees = self.project.read(cx).worktrees().next().is_some();
1822 // main content
1823 div()
1824 .flex_1()
1825 .relative()
1826 .group("")
1827 .on_drag_move::<DraggedTab>(cx.listener(Self::handle_drag_move))
1828 .on_drag_move::<ProjectEntryId>(cx.listener(Self::handle_drag_move))
1829 .on_drag_move::<ExternalPaths>(cx.listener(Self::handle_drag_move))
1830 .map(|div| {
1831 if let Some(item) = self.active_item() {
1832 div.v_flex()
1833 .child(self.toolbar.clone())
1834 .child(item.to_any())
1835 } else {
1836 let placeholder = div.h_flex().size_full().justify_center();
1837 if has_worktrees {
1838 placeholder
1839 } else {
1840 placeholder.child(
1841 Label::new("Open a file or project to get started.")
1842 .color(Color::Muted),
1843 )
1844 }
1845 }
1846 })
1847 .child(
1848 // drag target
1849 div()
1850 .z_index(1)
1851 .invisible()
1852 .absolute()
1853 .bg(theme::color_alpha(
1854 cx.theme().colors().drop_target_background,
1855 0.75,
1856 ))
1857 .group_drag_over::<DraggedTab>("", |style| style.visible())
1858 .group_drag_over::<ProjectEntryId>("", |style| style.visible())
1859 .group_drag_over::<ExternalPaths>("", |style| style.visible())
1860 .when_some(self.can_drop_predicate.clone(), |this, p| {
1861 this.can_drop(move |a, cx| p(a, cx))
1862 })
1863 .on_drop(cx.listener(move |this, dragged_tab, cx| {
1864 this.handle_tab_drop(dragged_tab, this.active_item_index(), cx)
1865 }))
1866 .on_drop(cx.listener(move |this, entry_id, cx| {
1867 this.handle_project_entry_drop(entry_id, cx)
1868 }))
1869 .on_drop(cx.listener(move |this, paths, cx| {
1870 this.handle_external_paths_drop(paths, cx)
1871 }))
1872 .map(|div| match self.drag_split_direction {
1873 None => div.top_0().left_0().right_0().bottom_0(),
1874 Some(SplitDirection::Up) => div.top_0().left_0().right_0().h_32(),
1875 Some(SplitDirection::Down) => {
1876 div.left_0().bottom_0().right_0().h_32()
1877 }
1878 Some(SplitDirection::Left) => {
1879 div.top_0().left_0().bottom_0().w_32()
1880 }
1881 Some(SplitDirection::Right) => {
1882 div.top_0().bottom_0().right_0().w_32()
1883 }
1884 }),
1885 )
1886 })
1887 .on_mouse_down(
1888 MouseButton::Navigate(NavigationDirection::Back),
1889 cx.listener(|pane, _, cx| {
1890 if let Some(workspace) = pane.workspace.upgrade() {
1891 let pane = cx.view().downgrade();
1892 cx.window_context().defer(move |cx| {
1893 workspace.update(cx, |workspace, cx| {
1894 workspace.go_back(pane, cx).detach_and_log_err(cx)
1895 })
1896 })
1897 }
1898 }),
1899 )
1900 .on_mouse_down(
1901 MouseButton::Navigate(NavigationDirection::Forward),
1902 cx.listener(|pane, _, cx| {
1903 if let Some(workspace) = pane.workspace.upgrade() {
1904 let pane = cx.view().downgrade();
1905 cx.window_context().defer(move |cx| {
1906 workspace.update(cx, |workspace, cx| {
1907 workspace.go_forward(pane, cx).detach_and_log_err(cx)
1908 })
1909 })
1910 }
1911 }),
1912 )
1913 }
1914}
1915
1916impl ItemNavHistory {
1917 pub fn push<D: 'static + Send + Any>(&mut self, data: Option<D>, cx: &mut WindowContext) {
1918 self.history.push(data, self.item.clone(), cx);
1919 }
1920
1921 pub fn pop_backward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
1922 self.history.pop(NavigationMode::GoingBack, cx)
1923 }
1924
1925 pub fn pop_forward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
1926 self.history.pop(NavigationMode::GoingForward, cx)
1927 }
1928}
1929
1930impl NavHistory {
1931 pub fn for_each_entry(
1932 &self,
1933 cx: &AppContext,
1934 mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
1935 ) {
1936 let borrowed_history = self.0.lock();
1937 borrowed_history
1938 .forward_stack
1939 .iter()
1940 .chain(borrowed_history.backward_stack.iter())
1941 .chain(borrowed_history.closed_stack.iter())
1942 .for_each(|entry| {
1943 if let Some(project_and_abs_path) =
1944 borrowed_history.paths_by_item.get(&entry.item.id())
1945 {
1946 f(entry, project_and_abs_path.clone());
1947 } else if let Some(item) = entry.item.upgrade() {
1948 if let Some(path) = item.project_path(cx) {
1949 f(entry, (path, None));
1950 }
1951 }
1952 })
1953 }
1954
1955 pub fn set_mode(&mut self, mode: NavigationMode) {
1956 self.0.lock().mode = mode;
1957 }
1958
1959 pub fn mode(&self) -> NavigationMode {
1960 self.0.lock().mode
1961 }
1962
1963 pub fn disable(&mut self) {
1964 self.0.lock().mode = NavigationMode::Disabled;
1965 }
1966
1967 pub fn enable(&mut self) {
1968 self.0.lock().mode = NavigationMode::Normal;
1969 }
1970
1971 pub fn pop(&mut self, mode: NavigationMode, cx: &mut WindowContext) -> Option<NavigationEntry> {
1972 let mut state = self.0.lock();
1973 let entry = match mode {
1974 NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
1975 return None
1976 }
1977 NavigationMode::GoingBack => &mut state.backward_stack,
1978 NavigationMode::GoingForward => &mut state.forward_stack,
1979 NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
1980 }
1981 .pop_back();
1982 if entry.is_some() {
1983 state.did_update(cx);
1984 }
1985 entry
1986 }
1987
1988 pub fn push<D: 'static + Send + Any>(
1989 &mut self,
1990 data: Option<D>,
1991 item: Arc<dyn WeakItemHandle>,
1992 cx: &mut WindowContext,
1993 ) {
1994 let state = &mut *self.0.lock();
1995 match state.mode {
1996 NavigationMode::Disabled => {}
1997 NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
1998 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
1999 state.backward_stack.pop_front();
2000 }
2001 state.backward_stack.push_back(NavigationEntry {
2002 item,
2003 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2004 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2005 });
2006 state.forward_stack.clear();
2007 }
2008 NavigationMode::GoingBack => {
2009 if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2010 state.forward_stack.pop_front();
2011 }
2012 state.forward_stack.push_back(NavigationEntry {
2013 item,
2014 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2015 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2016 });
2017 }
2018 NavigationMode::GoingForward => {
2019 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2020 state.backward_stack.pop_front();
2021 }
2022 state.backward_stack.push_back(NavigationEntry {
2023 item,
2024 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2025 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2026 });
2027 }
2028 NavigationMode::ClosingItem => {
2029 if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2030 state.closed_stack.pop_front();
2031 }
2032 state.closed_stack.push_back(NavigationEntry {
2033 item,
2034 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2035 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2036 });
2037 }
2038 }
2039 state.did_update(cx);
2040 }
2041
2042 pub fn remove_item(&mut self, item_id: EntityId) {
2043 let mut state = self.0.lock();
2044 state.paths_by_item.remove(&item_id);
2045 state
2046 .backward_stack
2047 .retain(|entry| entry.item.id() != item_id);
2048 state
2049 .forward_stack
2050 .retain(|entry| entry.item.id() != item_id);
2051 state
2052 .closed_stack
2053 .retain(|entry| entry.item.id() != item_id);
2054 }
2055
2056 pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option<PathBuf>)> {
2057 self.0.lock().paths_by_item.get(&item_id).cloned()
2058 }
2059}
2060
2061impl NavHistoryState {
2062 pub fn did_update(&self, cx: &mut WindowContext) {
2063 if let Some(pane) = self.pane.upgrade() {
2064 cx.defer(move |cx| {
2065 pane.update(cx, |pane, cx| pane.history_updated(cx));
2066 });
2067 }
2068 }
2069}
2070
2071fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
2072 let path = buffer_path
2073 .as_ref()
2074 .and_then(|p| p.path.to_str())
2075 .unwrap_or("This buffer");
2076 let path = truncate_and_remove_front(path, 80);
2077 format!("{path} contains unsaved edits. Do you want to save it?")
2078}
2079
2080#[cfg(test)]
2081mod tests {
2082 use super::*;
2083 use crate::item::test::{TestItem, TestProjectItem};
2084 use gpui::{TestAppContext, VisualTestContext};
2085 use project::FakeFs;
2086 use settings::SettingsStore;
2087 use theme::LoadThemes;
2088
2089 #[gpui::test]
2090 async fn test_remove_active_empty(cx: &mut TestAppContext) {
2091 init_test(cx);
2092 let fs = FakeFs::new(cx.executor());
2093
2094 let project = Project::test(fs, None, cx).await;
2095 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2096 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2097
2098 pane.update(cx, |pane, cx| {
2099 assert!(pane
2100 .close_active_item(&CloseActiveItem { save_intent: None }, cx)
2101 .is_none())
2102 });
2103 }
2104
2105 #[gpui::test]
2106 async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
2107 init_test(cx);
2108 let fs = FakeFs::new(cx.executor());
2109
2110 let project = Project::test(fs, None, cx).await;
2111 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2112 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2113
2114 // 1. Add with a destination index
2115 // a. Add before the active item
2116 set_labeled_items(&pane, ["A", "B*", "C"], cx);
2117 pane.update(cx, |pane, cx| {
2118 pane.add_item(
2119 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2120 false,
2121 false,
2122 Some(0),
2123 cx,
2124 );
2125 });
2126 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
2127
2128 // b. Add after the active item
2129 set_labeled_items(&pane, ["A", "B*", "C"], cx);
2130 pane.update(cx, |pane, cx| {
2131 pane.add_item(
2132 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2133 false,
2134 false,
2135 Some(2),
2136 cx,
2137 );
2138 });
2139 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
2140
2141 // c. Add at the end of the item list (including off the length)
2142 set_labeled_items(&pane, ["A", "B*", "C"], cx);
2143 pane.update(cx, |pane, cx| {
2144 pane.add_item(
2145 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2146 false,
2147 false,
2148 Some(5),
2149 cx,
2150 );
2151 });
2152 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2153
2154 // 2. Add without a destination index
2155 // a. Add with active item at the start of the item list
2156 set_labeled_items(&pane, ["A*", "B", "C"], cx);
2157 pane.update(cx, |pane, cx| {
2158 pane.add_item(
2159 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2160 false,
2161 false,
2162 None,
2163 cx,
2164 );
2165 });
2166 set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
2167
2168 // b. Add with active item at the end of the item list
2169 set_labeled_items(&pane, ["A", "B", "C*"], cx);
2170 pane.update(cx, |pane, cx| {
2171 pane.add_item(
2172 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2173 false,
2174 false,
2175 None,
2176 cx,
2177 );
2178 });
2179 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2180 }
2181
2182 #[gpui::test]
2183 async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
2184 init_test(cx);
2185 let fs = FakeFs::new(cx.executor());
2186
2187 let project = Project::test(fs, None, cx).await;
2188 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2189 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2190
2191 // 1. Add with a destination index
2192 // 1a. Add before the active item
2193 let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
2194 pane.update(cx, |pane, cx| {
2195 pane.add_item(d, false, false, Some(0), cx);
2196 });
2197 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
2198
2199 // 1b. Add after the active item
2200 let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
2201 pane.update(cx, |pane, cx| {
2202 pane.add_item(d, false, false, Some(2), cx);
2203 });
2204 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
2205
2206 // 1c. Add at the end of the item list (including off the length)
2207 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
2208 pane.update(cx, |pane, cx| {
2209 pane.add_item(a, false, false, Some(5), cx);
2210 });
2211 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
2212
2213 // 1d. Add same item to active index
2214 let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
2215 pane.update(cx, |pane, cx| {
2216 pane.add_item(b, false, false, Some(1), cx);
2217 });
2218 assert_item_labels(&pane, ["A", "B*", "C"], cx);
2219
2220 // 1e. Add item to index after same item in last position
2221 let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
2222 pane.update(cx, |pane, cx| {
2223 pane.add_item(c, false, false, Some(2), cx);
2224 });
2225 assert_item_labels(&pane, ["A", "B", "C*"], cx);
2226
2227 // 2. Add without a destination index
2228 // 2a. Add with active item at the start of the item list
2229 let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
2230 pane.update(cx, |pane, cx| {
2231 pane.add_item(d, false, false, None, cx);
2232 });
2233 assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
2234
2235 // 2b. Add with active item at the end of the item list
2236 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
2237 pane.update(cx, |pane, cx| {
2238 pane.add_item(a, false, false, None, cx);
2239 });
2240 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
2241
2242 // 2c. Add active item to active item at end of list
2243 let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
2244 pane.update(cx, |pane, cx| {
2245 pane.add_item(c, false, false, None, cx);
2246 });
2247 assert_item_labels(&pane, ["A", "B", "C*"], cx);
2248
2249 // 2d. Add active item to active item at start of list
2250 let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
2251 pane.update(cx, |pane, cx| {
2252 pane.add_item(a, false, false, None, cx);
2253 });
2254 assert_item_labels(&pane, ["A*", "B", "C"], cx);
2255 }
2256
2257 #[gpui::test]
2258 async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
2259 init_test(cx);
2260 let fs = FakeFs::new(cx.executor());
2261
2262 let project = Project::test(fs, None, cx).await;
2263 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2264 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2265
2266 // singleton view
2267 pane.update(cx, |pane, cx| {
2268 pane.add_item(
2269 Box::new(cx.new_view(|cx| {
2270 TestItem::new(cx)
2271 .with_singleton(true)
2272 .with_label("buffer 1")
2273 .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
2274 })),
2275 false,
2276 false,
2277 None,
2278 cx,
2279 );
2280 });
2281 assert_item_labels(&pane, ["buffer 1*"], cx);
2282
2283 // new singleton view with the same project entry
2284 pane.update(cx, |pane, cx| {
2285 pane.add_item(
2286 Box::new(cx.new_view(|cx| {
2287 TestItem::new(cx)
2288 .with_singleton(true)
2289 .with_label("buffer 1")
2290 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
2291 })),
2292 false,
2293 false,
2294 None,
2295 cx,
2296 );
2297 });
2298 assert_item_labels(&pane, ["buffer 1*"], cx);
2299
2300 // new singleton view with different project entry
2301 pane.update(cx, |pane, cx| {
2302 pane.add_item(
2303 Box::new(cx.new_view(|cx| {
2304 TestItem::new(cx)
2305 .with_singleton(true)
2306 .with_label("buffer 2")
2307 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
2308 })),
2309 false,
2310 false,
2311 None,
2312 cx,
2313 );
2314 });
2315 assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
2316
2317 // new multibuffer view with the same project entry
2318 pane.update(cx, |pane, cx| {
2319 pane.add_item(
2320 Box::new(cx.new_view(|cx| {
2321 TestItem::new(cx)
2322 .with_singleton(false)
2323 .with_label("multibuffer 1")
2324 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
2325 })),
2326 false,
2327 false,
2328 None,
2329 cx,
2330 );
2331 });
2332 assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
2333
2334 // another multibuffer view with the same project entry
2335 pane.update(cx, |pane, cx| {
2336 pane.add_item(
2337 Box::new(cx.new_view(|cx| {
2338 TestItem::new(cx)
2339 .with_singleton(false)
2340 .with_label("multibuffer 1b")
2341 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
2342 })),
2343 false,
2344 false,
2345 None,
2346 cx,
2347 );
2348 });
2349 assert_item_labels(
2350 &pane,
2351 ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
2352 cx,
2353 );
2354 }
2355
2356 #[gpui::test]
2357 async fn test_remove_item_ordering(cx: &mut TestAppContext) {
2358 init_test(cx);
2359 let fs = FakeFs::new(cx.executor());
2360
2361 let project = Project::test(fs, None, cx).await;
2362 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2363 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2364
2365 add_labeled_item(&pane, "A", false, cx);
2366 add_labeled_item(&pane, "B", false, cx);
2367 add_labeled_item(&pane, "C", false, cx);
2368 add_labeled_item(&pane, "D", false, cx);
2369 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2370
2371 pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
2372 add_labeled_item(&pane, "1", false, cx);
2373 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
2374
2375 pane.update(cx, |pane, cx| {
2376 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
2377 })
2378 .unwrap()
2379 .await
2380 .unwrap();
2381 assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
2382
2383 pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
2384 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2385
2386 pane.update(cx, |pane, cx| {
2387 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
2388 })
2389 .unwrap()
2390 .await
2391 .unwrap();
2392 assert_item_labels(&pane, ["A", "B*", "C"], cx);
2393
2394 pane.update(cx, |pane, cx| {
2395 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
2396 })
2397 .unwrap()
2398 .await
2399 .unwrap();
2400 assert_item_labels(&pane, ["A", "C*"], cx);
2401
2402 pane.update(cx, |pane, cx| {
2403 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
2404 })
2405 .unwrap()
2406 .await
2407 .unwrap();
2408 assert_item_labels(&pane, ["A*"], cx);
2409 }
2410
2411 #[gpui::test]
2412 async fn test_close_inactive_items(cx: &mut TestAppContext) {
2413 init_test(cx);
2414 let fs = FakeFs::new(cx.executor());
2415
2416 let project = Project::test(fs, None, cx).await;
2417 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2418 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2419
2420 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
2421
2422 pane.update(cx, |pane, cx| {
2423 pane.close_inactive_items(&CloseInactiveItems, cx)
2424 })
2425 .unwrap()
2426 .await
2427 .unwrap();
2428 assert_item_labels(&pane, ["C*"], cx);
2429 }
2430
2431 #[gpui::test]
2432 async fn test_close_clean_items(cx: &mut TestAppContext) {
2433 init_test(cx);
2434 let fs = FakeFs::new(cx.executor());
2435
2436 let project = Project::test(fs, None, cx).await;
2437 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2438 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2439
2440 add_labeled_item(&pane, "A", true, cx);
2441 add_labeled_item(&pane, "B", false, cx);
2442 add_labeled_item(&pane, "C", true, cx);
2443 add_labeled_item(&pane, "D", false, cx);
2444 add_labeled_item(&pane, "E", false, cx);
2445 assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
2446
2447 pane.update(cx, |pane, cx| pane.close_clean_items(&CloseCleanItems, cx))
2448 .unwrap()
2449 .await
2450 .unwrap();
2451 assert_item_labels(&pane, ["A^", "C*^"], cx);
2452 }
2453
2454 #[gpui::test]
2455 async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
2456 init_test(cx);
2457 let fs = FakeFs::new(cx.executor());
2458
2459 let project = Project::test(fs, None, cx).await;
2460 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2461 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2462
2463 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
2464
2465 pane.update(cx, |pane, cx| {
2466 pane.close_items_to_the_left(&CloseItemsToTheLeft, cx)
2467 })
2468 .unwrap()
2469 .await
2470 .unwrap();
2471 assert_item_labels(&pane, ["C*", "D", "E"], cx);
2472 }
2473
2474 #[gpui::test]
2475 async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
2476 init_test(cx);
2477 let fs = FakeFs::new(cx.executor());
2478
2479 let project = Project::test(fs, None, cx).await;
2480 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2481 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2482
2483 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
2484
2485 pane.update(cx, |pane, cx| {
2486 pane.close_items_to_the_right(&CloseItemsToTheRight, cx)
2487 })
2488 .unwrap()
2489 .await
2490 .unwrap();
2491 assert_item_labels(&pane, ["A", "B", "C*"], cx);
2492 }
2493
2494 #[gpui::test]
2495 async fn test_close_all_items(cx: &mut TestAppContext) {
2496 init_test(cx);
2497 let fs = FakeFs::new(cx.executor());
2498
2499 let project = Project::test(fs, None, cx).await;
2500 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2501 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2502
2503 add_labeled_item(&pane, "A", false, cx);
2504 add_labeled_item(&pane, "B", false, cx);
2505 add_labeled_item(&pane, "C", false, cx);
2506 assert_item_labels(&pane, ["A", "B", "C*"], cx);
2507
2508 pane.update(cx, |pane, cx| {
2509 pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
2510 })
2511 .unwrap()
2512 .await
2513 .unwrap();
2514 assert_item_labels(&pane, [], cx);
2515
2516 add_labeled_item(&pane, "A", true, cx);
2517 add_labeled_item(&pane, "B", true, cx);
2518 add_labeled_item(&pane, "C", true, cx);
2519 assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
2520
2521 let save = pane
2522 .update(cx, |pane, cx| {
2523 pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
2524 })
2525 .unwrap();
2526
2527 cx.executor().run_until_parked();
2528 cx.simulate_prompt_answer(2);
2529 save.await.unwrap();
2530 assert_item_labels(&pane, [], cx);
2531 }
2532
2533 fn init_test(cx: &mut TestAppContext) {
2534 cx.update(|cx| {
2535 let settings_store = SettingsStore::test(cx);
2536 cx.set_global(settings_store);
2537 theme::init(LoadThemes::JustBase, cx);
2538 crate::init_settings(cx);
2539 Project::init_settings(cx);
2540 });
2541 }
2542
2543 fn add_labeled_item(
2544 pane: &View<Pane>,
2545 label: &str,
2546 is_dirty: bool,
2547 cx: &mut VisualTestContext,
2548 ) -> Box<View<TestItem>> {
2549 pane.update(cx, |pane, cx| {
2550 let labeled_item = Box::new(
2551 cx.new_view(|cx| TestItem::new(cx).with_label(label).with_dirty(is_dirty)),
2552 );
2553 pane.add_item(labeled_item.clone(), false, false, None, cx);
2554 labeled_item
2555 })
2556 }
2557
2558 fn set_labeled_items<const COUNT: usize>(
2559 pane: &View<Pane>,
2560 labels: [&str; COUNT],
2561 cx: &mut VisualTestContext,
2562 ) -> [Box<View<TestItem>>; COUNT] {
2563 pane.update(cx, |pane, cx| {
2564 pane.items.clear();
2565 let mut active_item_index = 0;
2566
2567 let mut index = 0;
2568 let items = labels.map(|mut label| {
2569 if label.ends_with("*") {
2570 label = label.trim_end_matches("*");
2571 active_item_index = index;
2572 }
2573
2574 let labeled_item = Box::new(cx.new_view(|cx| TestItem::new(cx).with_label(label)));
2575 pane.add_item(labeled_item.clone(), false, false, None, cx);
2576 index += 1;
2577 labeled_item
2578 });
2579
2580 pane.activate_item(active_item_index, false, false, cx);
2581
2582 items
2583 })
2584 }
2585
2586 // Assert the item label, with the active item label suffixed with a '*'
2587 fn assert_item_labels<const COUNT: usize>(
2588 pane: &View<Pane>,
2589 expected_states: [&str; COUNT],
2590 cx: &mut VisualTestContext,
2591 ) {
2592 pane.update(cx, |pane, cx| {
2593 let actual_states = pane
2594 .items
2595 .iter()
2596 .enumerate()
2597 .map(|(ix, item)| {
2598 let mut state = item
2599 .to_any()
2600 .downcast::<TestItem>()
2601 .unwrap()
2602 .read(cx)
2603 .label
2604 .clone();
2605 if ix == pane.active_item_index {
2606 state.push('*');
2607 }
2608 if item.is_dirty(cx) {
2609 state.push('^');
2610 }
2611 state
2612 })
2613 .collect::<Vec<_>>();
2614
2615 assert_eq!(
2616 actual_states, expected_states,
2617 "pane items do not match expectation"
2618 );
2619 })
2620 }
2621}
2622
2623impl Render for DraggedTab {
2624 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2625 let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
2626 let label = self.item.tab_content(Some(self.detail), false, cx);
2627 Tab::new("")
2628 .selected(self.is_active)
2629 .child(label)
2630 .render(cx)
2631 .font(ui_font)
2632 }
2633}