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