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, Default)]
76#[serde(rename_all = "camelCase")]
77pub struct RevealInProjectPanel {
78 pub entry_id: Option<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 println!("{}", std::backtrace::Backtrace::force_capture());
766 self.close_items(cx, save_intent, move |view_id| view_id == item_id_to_close)
767 }
768
769 pub fn close_inactive_items(
770 &mut self,
771 _: &CloseInactiveItems,
772 cx: &mut ViewContext<Self>,
773 ) -> Option<Task<Result<()>>> {
774 if self.items.is_empty() {
775 return None;
776 }
777
778 let active_item_id = self.items[self.active_item_index].item_id();
779 Some(self.close_items(cx, SaveIntent::Close, move |item_id| {
780 item_id != active_item_id
781 }))
782 }
783
784 pub fn close_clean_items(
785 &mut self,
786 _: &CloseCleanItems,
787 cx: &mut ViewContext<Self>,
788 ) -> Option<Task<Result<()>>> {
789 let item_ids: Vec<_> = self
790 .items()
791 .filter(|item| !item.is_dirty(cx))
792 .map(|item| item.item_id())
793 .collect();
794 Some(self.close_items(cx, SaveIntent::Close, move |item_id| {
795 item_ids.contains(&item_id)
796 }))
797 }
798
799 pub fn close_items_to_the_left(
800 &mut self,
801 _: &CloseItemsToTheLeft,
802 cx: &mut ViewContext<Self>,
803 ) -> Option<Task<Result<()>>> {
804 if self.items.is_empty() {
805 return None;
806 }
807 let active_item_id = self.items[self.active_item_index].item_id();
808 Some(self.close_items_to_the_left_by_id(active_item_id, cx))
809 }
810
811 pub fn close_items_to_the_left_by_id(
812 &mut self,
813 item_id: EntityId,
814 cx: &mut ViewContext<Self>,
815 ) -> Task<Result<()>> {
816 let item_ids: Vec<_> = self
817 .items()
818 .take_while(|item| item.item_id() != item_id)
819 .map(|item| item.item_id())
820 .collect();
821 self.close_items(cx, SaveIntent::Close, move |item_id| {
822 item_ids.contains(&item_id)
823 })
824 }
825
826 pub fn close_items_to_the_right(
827 &mut self,
828 _: &CloseItemsToTheRight,
829 cx: &mut ViewContext<Self>,
830 ) -> Option<Task<Result<()>>> {
831 if self.items.is_empty() {
832 return None;
833 }
834 let active_item_id = self.items[self.active_item_index].item_id();
835 Some(self.close_items_to_the_right_by_id(active_item_id, cx))
836 }
837
838 pub fn close_items_to_the_right_by_id(
839 &mut self,
840 item_id: EntityId,
841 cx: &mut ViewContext<Self>,
842 ) -> Task<Result<()>> {
843 let item_ids: Vec<_> = self
844 .items()
845 .rev()
846 .take_while(|item| item.item_id() != item_id)
847 .map(|item| item.item_id())
848 .collect();
849 self.close_items(cx, SaveIntent::Close, move |item_id| {
850 item_ids.contains(&item_id)
851 })
852 }
853
854 pub fn close_all_items(
855 &mut self,
856 action: &CloseAllItems,
857 cx: &mut ViewContext<Self>,
858 ) -> Option<Task<Result<()>>> {
859 if self.items.is_empty() {
860 return None;
861 }
862
863 Some(
864 self.close_items(cx, action.save_intent.unwrap_or(SaveIntent::Close), |_| {
865 true
866 }),
867 )
868 }
869
870 pub(super) fn file_names_for_prompt(
871 items: &mut dyn Iterator<Item = &Box<dyn ItemHandle>>,
872 all_dirty_items: usize,
873 cx: &AppContext,
874 ) -> (String, String) {
875 /// Quantity of item paths displayed in prompt prior to cutoff..
876 const FILE_NAMES_CUTOFF_POINT: usize = 10;
877 let mut file_names: Vec<_> = items
878 .filter_map(|item| {
879 item.project_path(cx).and_then(|project_path| {
880 project_path
881 .path
882 .file_name()
883 .and_then(|name| name.to_str().map(ToOwned::to_owned))
884 })
885 })
886 .take(FILE_NAMES_CUTOFF_POINT)
887 .collect();
888 let should_display_followup_text =
889 all_dirty_items > FILE_NAMES_CUTOFF_POINT || file_names.len() != all_dirty_items;
890 if should_display_followup_text {
891 let not_shown_files = all_dirty_items - file_names.len();
892 if not_shown_files == 1 {
893 file_names.push(".. 1 file not shown".into());
894 } else {
895 file_names.push(format!(".. {} files not shown", not_shown_files).into());
896 }
897 }
898 (
899 format!(
900 "Do you want to save changes to the following {} files?",
901 all_dirty_items
902 ),
903 file_names.join("\n"),
904 )
905 }
906
907 pub fn close_items(
908 &mut self,
909 cx: &mut ViewContext<Pane>,
910 mut save_intent: SaveIntent,
911 should_close: impl Fn(EntityId) -> bool,
912 ) -> Task<Result<()>> {
913 // Find the items to close.
914 let mut items_to_close = Vec::new();
915 let mut dirty_items = Vec::new();
916 for item in &self.items {
917 if should_close(item.item_id()) {
918 items_to_close.push(item.boxed_clone());
919 if item.is_dirty(cx) {
920 dirty_items.push(item.boxed_clone());
921 }
922 }
923 }
924
925 // If a buffer is open both in a singleton editor and in a multibuffer, make sure
926 // to focus the singleton buffer when prompting to save that buffer, as opposed
927 // to focusing the multibuffer, because this gives the user a more clear idea
928 // of what content they would be saving.
929 items_to_close.sort_by_key(|item| !item.is_singleton(cx));
930
931 let workspace = self.workspace.clone();
932 cx.spawn(|pane, mut cx| async move {
933 if save_intent == SaveIntent::Close && dirty_items.len() > 1 {
934 let answer = pane.update(&mut cx, |_, cx| {
935 let (prompt, detail) =
936 Self::file_names_for_prompt(&mut dirty_items.iter(), dirty_items.len(), cx);
937 cx.prompt(
938 PromptLevel::Warning,
939 &prompt,
940 Some(&detail),
941 &["Save all", "Discard all", "Cancel"],
942 )
943 })?;
944 match answer.await {
945 Ok(0) => save_intent = SaveIntent::SaveAll,
946 Ok(1) => save_intent = SaveIntent::Skip,
947 _ => {}
948 }
949 }
950 let mut saved_project_items_ids = HashSet::default();
951 for item in items_to_close.clone() {
952 // Find the item's current index and its set of project item models. Avoid
953 // storing these in advance, in case they have changed since this task
954 // was started.
955 let (item_ix, mut project_item_ids) = pane.update(&mut cx, |pane, cx| {
956 (pane.index_for_item(&*item), item.project_item_model_ids(cx))
957 })?;
958 let item_ix = if let Some(ix) = item_ix {
959 ix
960 } else {
961 continue;
962 };
963
964 // Check if this view has any project items that are not open anywhere else
965 // in the workspace, AND that the user has not already been prompted to save.
966 // If there are any such project entries, prompt the user to save this item.
967 let project = workspace.update(&mut cx, |workspace, cx| {
968 for item in workspace.items(cx) {
969 if !items_to_close
970 .iter()
971 .any(|item_to_close| item_to_close.item_id() == item.item_id())
972 {
973 let other_project_item_ids = item.project_item_model_ids(cx);
974 project_item_ids.retain(|id| !other_project_item_ids.contains(id));
975 }
976 }
977 workspace.project().clone()
978 })?;
979 let should_save = project_item_ids
980 .iter()
981 .any(|id| saved_project_items_ids.insert(*id));
982
983 if should_save
984 && !Self::save_item(
985 project.clone(),
986 &pane,
987 item_ix,
988 &*item,
989 save_intent,
990 &mut cx,
991 )
992 .await?
993 {
994 break;
995 }
996
997 // Remove the item from the pane.
998 pane.update(&mut cx, |pane, cx| {
999 if let Some(item_ix) = pane
1000 .items
1001 .iter()
1002 .position(|i| i.item_id() == item.item_id())
1003 {
1004 pane.remove_item(item_ix, false, cx);
1005 }
1006 })
1007 .ok();
1008 }
1009
1010 pane.update(&mut cx, |_, cx| cx.notify()).ok();
1011 Ok(())
1012 })
1013 }
1014
1015 pub fn remove_item(
1016 &mut self,
1017 item_index: usize,
1018 activate_pane: bool,
1019 cx: &mut ViewContext<Self>,
1020 ) {
1021 self.activation_history
1022 .retain(|&history_entry| history_entry != self.items[item_index].item_id());
1023
1024 if item_index == self.active_item_index {
1025 let index_to_activate = self
1026 .activation_history
1027 .pop()
1028 .and_then(|last_activated_item| {
1029 self.items.iter().enumerate().find_map(|(index, item)| {
1030 (item.item_id() == last_activated_item).then_some(index)
1031 })
1032 })
1033 // We didn't have a valid activation history entry, so fallback
1034 // to activating the item to the left
1035 .unwrap_or_else(|| item_index.min(self.items.len()).saturating_sub(1));
1036
1037 let should_activate = activate_pane || self.has_focus(cx);
1038 if self.items.len() == 1 && should_activate {
1039 self.focus_handle.focus(cx);
1040 } else {
1041 self.activate_item(index_to_activate, should_activate, should_activate, cx);
1042 }
1043 }
1044
1045 let item = self.items.remove(item_index);
1046
1047 cx.emit(Event::RemoveItem {
1048 item_id: item.item_id(),
1049 });
1050 if self.items.is_empty() {
1051 item.deactivated(cx);
1052 self.update_toolbar(cx);
1053 cx.emit(Event::Remove);
1054 }
1055
1056 if item_index < self.active_item_index {
1057 self.active_item_index -= 1;
1058 }
1059
1060 self.nav_history.set_mode(NavigationMode::ClosingItem);
1061 item.deactivated(cx);
1062 self.nav_history.set_mode(NavigationMode::Normal);
1063
1064 if let Some(path) = item.project_path(cx) {
1065 let abs_path = self
1066 .nav_history
1067 .0
1068 .lock()
1069 .paths_by_item
1070 .get(&item.item_id())
1071 .and_then(|(_, abs_path)| abs_path.clone());
1072
1073 self.nav_history
1074 .0
1075 .lock()
1076 .paths_by_item
1077 .insert(item.item_id(), (path, abs_path));
1078 } else {
1079 self.nav_history
1080 .0
1081 .lock()
1082 .paths_by_item
1083 .remove(&item.item_id());
1084 }
1085
1086 if self.items.is_empty() && self.zoomed {
1087 cx.emit(Event::ZoomOut);
1088 }
1089
1090 cx.notify();
1091 }
1092
1093 pub async fn save_item(
1094 project: Model<Project>,
1095 pane: &WeakView<Pane>,
1096 item_ix: usize,
1097 item: &dyn ItemHandle,
1098 save_intent: SaveIntent,
1099 cx: &mut AsyncWindowContext,
1100 ) -> Result<bool> {
1101 const CONFLICT_MESSAGE: &str =
1102 "This file has changed on disk since you started editing it. Do you want to overwrite it?";
1103
1104 if save_intent == SaveIntent::Skip {
1105 return Ok(true);
1106 }
1107
1108 let (mut has_conflict, mut is_dirty, mut can_save, can_save_as) = cx.update(|cx| {
1109 (
1110 item.has_conflict(cx),
1111 item.is_dirty(cx),
1112 item.can_save(cx),
1113 item.is_singleton(cx),
1114 )
1115 })?;
1116
1117 // when saving a single buffer, we ignore whether or not it's dirty.
1118 if save_intent == SaveIntent::Save {
1119 is_dirty = true;
1120 }
1121
1122 if save_intent == SaveIntent::SaveAs {
1123 is_dirty = true;
1124 has_conflict = false;
1125 can_save = false;
1126 }
1127
1128 if save_intent == SaveIntent::Overwrite {
1129 has_conflict = false;
1130 }
1131
1132 if has_conflict && can_save {
1133 let answer = pane.update(cx, |pane, cx| {
1134 pane.activate_item(item_ix, true, true, cx);
1135 cx.prompt(
1136 PromptLevel::Warning,
1137 CONFLICT_MESSAGE,
1138 None,
1139 &["Overwrite", "Discard", "Cancel"],
1140 )
1141 })?;
1142 match answer.await {
1143 Ok(0) => pane.update(cx, |_, cx| item.save(project, cx))?.await?,
1144 Ok(1) => pane.update(cx, |_, cx| item.reload(project, cx))?.await?,
1145 _ => return Ok(false),
1146 }
1147 } else if is_dirty && (can_save || can_save_as) {
1148 if save_intent == SaveIntent::Close {
1149 let will_autosave = cx.update(|cx| {
1150 matches!(
1151 WorkspaceSettings::get_global(cx).autosave,
1152 AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange
1153 ) && Self::can_autosave_item(&*item, cx)
1154 })?;
1155 if !will_autosave {
1156 let answer = pane.update(cx, |pane, cx| {
1157 pane.activate_item(item_ix, true, true, cx);
1158 let prompt = dirty_message_for(item.project_path(cx));
1159 cx.prompt(
1160 PromptLevel::Warning,
1161 &prompt,
1162 None,
1163 &["Save", "Don't Save", "Cancel"],
1164 )
1165 })?;
1166 match answer.await {
1167 Ok(0) => {}
1168 Ok(1) => return Ok(true), // Don't save this file
1169 _ => return Ok(false), // Cancel
1170 }
1171 }
1172 }
1173
1174 if can_save {
1175 pane.update(cx, |_, cx| item.save(project, cx))?.await?;
1176 } else if can_save_as {
1177 let start_abs_path = project
1178 .update(cx, |project, cx| {
1179 let worktree = project.visible_worktrees(cx).next()?;
1180 Some(worktree.read(cx).as_local()?.abs_path().to_path_buf())
1181 })?
1182 .unwrap_or_else(|| Path::new("").into());
1183
1184 let abs_path = cx.update(|cx| cx.prompt_for_new_path(&start_abs_path))?;
1185 if let Some(abs_path) = abs_path.await.ok().flatten() {
1186 pane.update(cx, |_, cx| item.save_as(project, abs_path, cx))?
1187 .await?;
1188 } else {
1189 return Ok(false);
1190 }
1191 }
1192 }
1193 Ok(true)
1194 }
1195
1196 fn can_autosave_item(item: &dyn ItemHandle, cx: &AppContext) -> bool {
1197 let is_deleted = item.project_entry_ids(cx).is_empty();
1198 item.is_dirty(cx) && !item.has_conflict(cx) && item.can_save(cx) && !is_deleted
1199 }
1200
1201 pub fn autosave_item(
1202 item: &dyn ItemHandle,
1203 project: Model<Project>,
1204 cx: &mut WindowContext,
1205 ) -> Task<Result<()>> {
1206 if Self::can_autosave_item(item, cx) {
1207 item.save(project, cx)
1208 } else {
1209 Task::ready(Ok(()))
1210 }
1211 }
1212
1213 pub fn focus(&mut self, cx: &mut ViewContext<Pane>) {
1214 cx.focus(&self.focus_handle);
1215 }
1216
1217 pub fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
1218 if let Some(active_item) = self.active_item() {
1219 let focus_handle = active_item.focus_handle(cx);
1220 cx.focus(&focus_handle);
1221 }
1222 }
1223
1224 pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext<Self>) {
1225 cx.emit(Event::Split(direction));
1226 }
1227
1228 pub fn toolbar(&self) -> &View<Toolbar> {
1229 &self.toolbar
1230 }
1231
1232 pub fn handle_deleted_project_item(
1233 &mut self,
1234 entry_id: ProjectEntryId,
1235 cx: &mut ViewContext<Pane>,
1236 ) -> Option<()> {
1237 let (item_index_to_delete, item_id) = self.items().enumerate().find_map(|(i, item)| {
1238 if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
1239 Some((i, item.item_id()))
1240 } else {
1241 None
1242 }
1243 })?;
1244
1245 self.remove_item(item_index_to_delete, false, cx);
1246 self.nav_history.remove_item(item_id);
1247
1248 Some(())
1249 }
1250
1251 fn update_toolbar(&mut self, cx: &mut ViewContext<Self>) {
1252 let active_item = self
1253 .items
1254 .get(self.active_item_index)
1255 .map(|item| item.as_ref());
1256 self.toolbar.update(cx, |toolbar, cx| {
1257 toolbar.set_active_item(active_item, cx);
1258 });
1259 }
1260
1261 fn update_status_bar(&mut self, cx: &mut ViewContext<Self>) {
1262 let workspace = self.workspace.clone();
1263 let pane = cx.view().clone();
1264
1265 cx.window_context().defer(move |cx| {
1266 let Ok(status_bar) = workspace.update(cx, |workspace, _| workspace.status_bar.clone())
1267 else {
1268 return;
1269 };
1270
1271 status_bar.update(cx, move |status_bar, cx| {
1272 status_bar.set_active_pane(&pane, cx);
1273 });
1274 });
1275 }
1276
1277 fn render_tab(
1278 &self,
1279 ix: usize,
1280 item: &Box<dyn ItemHandle>,
1281 detail: usize,
1282 cx: &mut ViewContext<'_, Pane>,
1283 ) -> impl IntoElement {
1284 let is_active = ix == self.active_item_index;
1285
1286 let label = item.tab_content(Some(detail), is_active, cx);
1287 let close_side = &ItemSettings::get_global(cx).close_position;
1288
1289 let indicator = maybe!({
1290 let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
1291 (true, _) => Color::Warning,
1292 (_, true) => Color::Accent,
1293 (false, false) => return None,
1294 };
1295
1296 Some(Indicator::dot().color(indicator_color))
1297 });
1298
1299 let item_id = item.item_id();
1300 let is_first_item = ix == 0;
1301 let is_last_item = ix == self.items.len() - 1;
1302 let position_relative_to_active_item = ix.cmp(&self.active_item_index);
1303
1304 let tab = Tab::new(ix)
1305 .position(if is_first_item {
1306 TabPosition::First
1307 } else if is_last_item {
1308 TabPosition::Last
1309 } else {
1310 TabPosition::Middle(position_relative_to_active_item)
1311 })
1312 .close_side(match close_side {
1313 ClosePosition::Left => ui::TabCloseSide::Start,
1314 ClosePosition::Right => ui::TabCloseSide::End,
1315 })
1316 .selected(is_active)
1317 .on_click(
1318 cx.listener(move |pane: &mut Self, _, cx| pane.activate_item(ix, true, true, cx)),
1319 )
1320 // TODO: This should be a click listener with the middle mouse button instead of a mouse down listener.
1321 .on_mouse_down(
1322 MouseButton::Middle,
1323 cx.listener(move |pane, _event, cx| {
1324 pane.close_item_by_id(item_id, SaveIntent::Close, cx)
1325 .detach_and_log_err(cx);
1326 }),
1327 )
1328 .on_drag(
1329 DraggedTab {
1330 item: item.boxed_clone(),
1331 pane: cx.view().clone(),
1332 detail,
1333 is_active,
1334 ix,
1335 },
1336 |tab, cx| cx.new_view(|_| tab.clone()),
1337 )
1338 .drag_over::<DraggedTab>(|tab, _, cx| {
1339 tab.bg(cx.theme().colors().drop_target_background)
1340 })
1341 .drag_over::<ProjectEntryId>(|tab, _, cx| {
1342 tab.bg(cx.theme().colors().drop_target_background)
1343 })
1344 .when_some(self.can_drop_predicate.clone(), |this, p| {
1345 this.can_drop(move |a, cx| p(a, cx))
1346 })
1347 .on_drop(cx.listener(move |this, dragged_tab: &DraggedTab, cx| {
1348 this.drag_split_direction = None;
1349 this.handle_tab_drop(dragged_tab, ix, cx)
1350 }))
1351 .on_drop(cx.listener(move |this, entry_id: &ProjectEntryId, cx| {
1352 this.drag_split_direction = None;
1353 this.handle_project_entry_drop(entry_id, cx)
1354 }))
1355 .on_drop(cx.listener(move |this, paths, cx| {
1356 this.drag_split_direction = None;
1357 this.handle_external_paths_drop(paths, cx)
1358 }))
1359 .when_some(item.tab_tooltip_text(cx), |tab, text| {
1360 tab.tooltip(move |cx| Tooltip::text(text.clone(), cx))
1361 })
1362 .start_slot::<Indicator>(indicator)
1363 .end_slot(
1364 IconButton::new("close tab", IconName::Close)
1365 .shape(IconButtonShape::Square)
1366 .icon_color(Color::Muted)
1367 .size(ButtonSize::None)
1368 .icon_size(IconSize::XSmall)
1369 .on_click(cx.listener(move |pane, _, cx| {
1370 pane.close_item_by_id(item_id, SaveIntent::Close, cx)
1371 .detach_and_log_err(cx);
1372 })),
1373 )
1374 .child(label);
1375
1376 let single_entry_to_resolve = {
1377 let item_entries = self.items[ix].project_entry_ids(cx);
1378 if item_entries.len() == 1 {
1379 Some(item_entries[0])
1380 } else {
1381 None
1382 }
1383 };
1384
1385 let pane = cx.view().downgrade();
1386 right_click_menu(ix).trigger(tab).menu(move |cx| {
1387 let pane = pane.clone();
1388 ContextMenu::build(cx, move |mut menu, cx| {
1389 if let Some(pane) = pane.upgrade() {
1390 menu = menu
1391 .entry(
1392 "Close",
1393 Some(Box::new(CloseActiveItem { save_intent: None })),
1394 cx.handler_for(&pane, move |pane, cx| {
1395 pane.close_item_by_id(item_id, SaveIntent::Close, cx)
1396 .detach_and_log_err(cx);
1397 }),
1398 )
1399 .entry(
1400 "Close Others",
1401 Some(Box::new(CloseInactiveItems)),
1402 cx.handler_for(&pane, move |pane, cx| {
1403 pane.close_items(cx, SaveIntent::Close, |id| id != item_id)
1404 .detach_and_log_err(cx);
1405 }),
1406 )
1407 .separator()
1408 .entry(
1409 "Close Left",
1410 Some(Box::new(CloseItemsToTheLeft)),
1411 cx.handler_for(&pane, move |pane, cx| {
1412 pane.close_items_to_the_left_by_id(item_id, cx)
1413 .detach_and_log_err(cx);
1414 }),
1415 )
1416 .entry(
1417 "Close Right",
1418 Some(Box::new(CloseItemsToTheRight)),
1419 cx.handler_for(&pane, move |pane, cx| {
1420 pane.close_items_to_the_right_by_id(item_id, cx)
1421 .detach_and_log_err(cx);
1422 }),
1423 )
1424 .separator()
1425 .entry(
1426 "Close Clean",
1427 Some(Box::new(CloseCleanItems)),
1428 cx.handler_for(&pane, move |pane, cx| {
1429 pane.close_clean_items(&CloseCleanItems, cx)
1430 .map(|task| task.detach_and_log_err(cx));
1431 }),
1432 )
1433 .entry(
1434 "Close All",
1435 Some(Box::new(CloseAllItems { save_intent: None })),
1436 cx.handler_for(&pane, |pane, cx| {
1437 pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
1438 .map(|task| task.detach_and_log_err(cx));
1439 }),
1440 );
1441
1442 if let Some(entry) = single_entry_to_resolve {
1443 let entry_id = entry.to_proto();
1444 menu = menu.separator().entry(
1445 "Reveal In Project Panel",
1446 Some(Box::new(RevealInProjectPanel {
1447 entry_id: Some(entry_id),
1448 })),
1449 cx.handler_for(&pane, move |pane, cx| {
1450 pane.project.update(cx, |_, cx| {
1451 cx.emit(project::Event::RevealInProjectPanel(
1452 ProjectEntryId::from_proto(entry_id),
1453 ))
1454 });
1455 }),
1456 );
1457 }
1458 }
1459
1460 menu
1461 })
1462 })
1463 }
1464
1465 fn render_tab_bar(&mut self, cx: &mut ViewContext<'_, Pane>) -> impl IntoElement {
1466 TabBar::new("tab_bar")
1467 .track_scroll(self.tab_bar_scroll_handle.clone())
1468 .when(self.display_nav_history_buttons, |tab_bar| {
1469 tab_bar.start_child(
1470 h_flex()
1471 .gap_2()
1472 .child(
1473 IconButton::new("navigate_backward", IconName::ArrowLeft)
1474 .icon_size(IconSize::Small)
1475 .on_click({
1476 let view = cx.view().clone();
1477 move |_, cx| view.update(cx, Self::navigate_backward)
1478 })
1479 .disabled(!self.can_navigate_backward())
1480 .tooltip(|cx| Tooltip::for_action("Go Back", &GoBack, cx)),
1481 )
1482 .child(
1483 IconButton::new("navigate_forward", IconName::ArrowRight)
1484 .icon_size(IconSize::Small)
1485 .on_click({
1486 let view = cx.view().clone();
1487 move |_, cx| view.update(cx, Self::navigate_forward)
1488 })
1489 .disabled(!self.can_navigate_forward())
1490 .tooltip(|cx| Tooltip::for_action("Go Forward", &GoForward, cx)),
1491 ),
1492 )
1493 })
1494 .when(self.has_focus(cx), |tab_bar| {
1495 tab_bar.end_child({
1496 let render_tab_buttons = self.render_tab_bar_buttons.clone();
1497 render_tab_buttons(self, cx)
1498 })
1499 })
1500 .children(
1501 self.items
1502 .iter()
1503 .enumerate()
1504 .zip(self.tab_details(cx))
1505 .map(|((ix, item), detail)| self.render_tab(ix, item, detail, cx)),
1506 )
1507 .child(
1508 div()
1509 .min_w_6()
1510 // HACK: This empty child is currently necessary to force the drop target to appear
1511 // despite us setting a min width above.
1512 .child("")
1513 .h_full()
1514 .flex_grow()
1515 .drag_over::<DraggedTab>(|bar, _, cx| {
1516 bar.bg(cx.theme().colors().drop_target_background)
1517 })
1518 .drag_over::<ProjectEntryId>(|bar, _, cx| {
1519 bar.bg(cx.theme().colors().drop_target_background)
1520 })
1521 .on_drop(cx.listener(move |this, dragged_tab: &DraggedTab, cx| {
1522 this.drag_split_direction = None;
1523 this.handle_tab_drop(dragged_tab, this.items.len(), cx)
1524 }))
1525 .on_drop(cx.listener(move |this, entry_id: &ProjectEntryId, cx| {
1526 this.drag_split_direction = None;
1527 this.handle_project_entry_drop(entry_id, cx)
1528 }))
1529 .on_drop(cx.listener(move |this, paths, cx| {
1530 this.drag_split_direction = None;
1531 this.handle_external_paths_drop(paths, cx)
1532 })),
1533 )
1534 }
1535
1536 fn render_menu_overlay(menu: &View<ContextMenu>) -> Div {
1537 div()
1538 .absolute()
1539 .z_index(1)
1540 .bottom_0()
1541 .right_0()
1542 .size_0()
1543 .child(overlay().anchor(AnchorCorner::TopRight).child(menu.clone()))
1544 }
1545
1546 fn tab_details(&self, cx: &AppContext) -> Vec<usize> {
1547 let mut tab_details = self.items.iter().map(|_| 0).collect::<Vec<_>>();
1548
1549 let mut tab_descriptions = HashMap::default();
1550 let mut done = false;
1551 while !done {
1552 done = true;
1553
1554 // Store item indices by their tab description.
1555 for (ix, (item, detail)) in self.items.iter().zip(&tab_details).enumerate() {
1556 if let Some(description) = item.tab_description(*detail, cx) {
1557 if *detail == 0
1558 || Some(&description) != item.tab_description(detail - 1, cx).as_ref()
1559 {
1560 tab_descriptions
1561 .entry(description)
1562 .or_insert(Vec::new())
1563 .push(ix);
1564 }
1565 }
1566 }
1567
1568 // If two or more items have the same tab description, increase eir level
1569 // of detail and try again.
1570 for (_, item_ixs) in tab_descriptions.drain() {
1571 if item_ixs.len() > 1 {
1572 done = false;
1573 for ix in item_ixs {
1574 tab_details[ix] += 1;
1575 }
1576 }
1577 }
1578 }
1579
1580 tab_details
1581 }
1582
1583 pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
1584 self.zoomed = zoomed;
1585 cx.notify();
1586 }
1587
1588 pub fn is_zoomed(&self) -> bool {
1589 self.zoomed
1590 }
1591
1592 fn handle_drag_move<T>(&mut self, event: &DragMoveEvent<T>, cx: &mut ViewContext<Self>) {
1593 if !self.can_split {
1594 return;
1595 }
1596
1597 let edge_width = cx.rem_size() * 8;
1598 let cursor = event.event.position;
1599 let direction = if cursor.x < event.bounds.left() + edge_width {
1600 Some(SplitDirection::Left)
1601 } else if cursor.x > event.bounds.right() - edge_width {
1602 Some(SplitDirection::Right)
1603 } else if cursor.y < event.bounds.top() + edge_width {
1604 Some(SplitDirection::Up)
1605 } else if cursor.y > event.bounds.bottom() - edge_width {
1606 Some(SplitDirection::Down)
1607 } else {
1608 None
1609 };
1610
1611 if direction != self.drag_split_direction {
1612 self.drag_split_direction = direction;
1613 }
1614 }
1615
1616 fn handle_tab_drop(
1617 &mut self,
1618 dragged_tab: &DraggedTab,
1619 ix: usize,
1620 cx: &mut ViewContext<'_, Self>,
1621 ) {
1622 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
1623 if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_tab, cx) {
1624 return;
1625 }
1626 }
1627 let mut to_pane = cx.view().clone();
1628 let split_direction = self.drag_split_direction;
1629 let item_id = dragged_tab.item.item_id();
1630 let from_pane = dragged_tab.pane.clone();
1631 self.workspace
1632 .update(cx, |_, cx| {
1633 cx.defer(move |workspace, cx| {
1634 if let Some(split_direction) = split_direction {
1635 to_pane = workspace.split_pane(to_pane, split_direction, cx);
1636 }
1637 workspace.move_item(from_pane, to_pane, item_id, ix, cx);
1638 });
1639 })
1640 .log_err();
1641 }
1642
1643 fn handle_project_entry_drop(
1644 &mut self,
1645 project_entry_id: &ProjectEntryId,
1646 cx: &mut ViewContext<'_, Self>,
1647 ) {
1648 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
1649 if let ControlFlow::Break(()) = custom_drop_handle(self, project_entry_id, cx) {
1650 return;
1651 }
1652 }
1653 let mut to_pane = cx.view().clone();
1654 let split_direction = self.drag_split_direction;
1655 let project_entry_id = *project_entry_id;
1656 self.workspace
1657 .update(cx, |_, cx| {
1658 cx.defer(move |workspace, cx| {
1659 if let Some(path) = workspace
1660 .project()
1661 .read(cx)
1662 .path_for_entry(project_entry_id, cx)
1663 {
1664 if let Some(split_direction) = split_direction {
1665 to_pane = workspace.split_pane(to_pane, split_direction, cx);
1666 }
1667 workspace
1668 .open_path(path, Some(to_pane.downgrade()), true, cx)
1669 .detach_and_log_err(cx);
1670 }
1671 });
1672 })
1673 .log_err();
1674 }
1675
1676 fn handle_external_paths_drop(
1677 &mut self,
1678 paths: &ExternalPaths,
1679 cx: &mut ViewContext<'_, Self>,
1680 ) {
1681 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
1682 if let ControlFlow::Break(()) = custom_drop_handle(self, paths, cx) {
1683 return;
1684 }
1685 }
1686 let mut to_pane = cx.view().clone();
1687 let mut split_direction = self.drag_split_direction;
1688 let paths = paths.paths().to_vec();
1689 self.workspace
1690 .update(cx, |workspace, cx| {
1691 let fs = Arc::clone(workspace.project().read(cx).fs());
1692 cx.spawn(|workspace, mut cx| async move {
1693 let mut is_file_checks = FuturesUnordered::new();
1694 for path in &paths {
1695 is_file_checks.push(fs.is_file(path))
1696 }
1697 let mut has_files_to_open = false;
1698 while let Some(is_file) = is_file_checks.next().await {
1699 if is_file {
1700 has_files_to_open = true;
1701 break;
1702 }
1703 }
1704 drop(is_file_checks);
1705 if !has_files_to_open {
1706 split_direction = None;
1707 }
1708
1709 if let Some(open_task) = workspace
1710 .update(&mut cx, |workspace, cx| {
1711 if let Some(split_direction) = split_direction {
1712 to_pane = workspace.split_pane(to_pane, split_direction, cx);
1713 }
1714 workspace.open_paths(
1715 paths,
1716 OpenVisible::OnlyDirectories,
1717 Some(to_pane.downgrade()),
1718 cx,
1719 )
1720 })
1721 .ok()
1722 {
1723 let _opened_items: Vec<_> = open_task.await;
1724 }
1725 })
1726 .detach();
1727 })
1728 .log_err();
1729 }
1730
1731 pub fn display_nav_history_buttons(&mut self, display: bool) {
1732 self.display_nav_history_buttons = display;
1733 }
1734}
1735
1736impl FocusableView for Pane {
1737 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
1738 self.focus_handle.clone()
1739 }
1740}
1741
1742impl Render for Pane {
1743 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
1744 v_flex()
1745 .key_context("Pane")
1746 .track_focus(&self.focus_handle)
1747 .size_full()
1748 .flex_none()
1749 .overflow_hidden()
1750 .on_action(cx.listener(|pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx)))
1751 .on_action(cx.listener(|pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx)))
1752 .on_action(
1753 cx.listener(|pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx)),
1754 )
1755 .on_action(cx.listener(|pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx)))
1756 .on_action(cx.listener(|pane, _: &GoBack, cx| pane.navigate_backward(cx)))
1757 .on_action(cx.listener(|pane, _: &GoForward, cx| pane.navigate_forward(cx)))
1758 .on_action(cx.listener(Pane::toggle_zoom))
1759 .on_action(cx.listener(|pane: &mut Pane, action: &ActivateItem, cx| {
1760 pane.activate_item(action.0, true, true, cx);
1761 }))
1762 .on_action(cx.listener(|pane: &mut Pane, _: &ActivateLastItem, cx| {
1763 pane.activate_item(pane.items.len() - 1, true, true, cx);
1764 }))
1765 .on_action(cx.listener(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
1766 pane.activate_prev_item(true, cx);
1767 }))
1768 .on_action(cx.listener(|pane: &mut Pane, _: &ActivateNextItem, cx| {
1769 pane.activate_next_item(true, cx);
1770 }))
1771 .on_action(
1772 cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
1773 pane.close_active_item(action, cx)
1774 .map(|task| task.detach_and_log_err(cx));
1775 }),
1776 )
1777 .on_action(
1778 cx.listener(|pane: &mut Self, action: &CloseInactiveItems, cx| {
1779 pane.close_inactive_items(action, cx)
1780 .map(|task| task.detach_and_log_err(cx));
1781 }),
1782 )
1783 .on_action(
1784 cx.listener(|pane: &mut Self, action: &CloseCleanItems, cx| {
1785 pane.close_clean_items(action, cx)
1786 .map(|task| task.detach_and_log_err(cx));
1787 }),
1788 )
1789 .on_action(
1790 cx.listener(|pane: &mut Self, action: &CloseItemsToTheLeft, cx| {
1791 pane.close_items_to_the_left(action, cx)
1792 .map(|task| task.detach_and_log_err(cx));
1793 }),
1794 )
1795 .on_action(
1796 cx.listener(|pane: &mut Self, action: &CloseItemsToTheRight, cx| {
1797 pane.close_items_to_the_right(action, cx)
1798 .map(|task| task.detach_and_log_err(cx));
1799 }),
1800 )
1801 .on_action(cx.listener(|pane: &mut Self, action: &CloseAllItems, cx| {
1802 pane.close_all_items(action, cx)
1803 .map(|task| task.detach_and_log_err(cx));
1804 }))
1805 .on_action(
1806 cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
1807 pane.close_active_item(action, cx)
1808 .map(|task| task.detach_and_log_err(cx));
1809 }),
1810 )
1811 .on_action(
1812 cx.listener(|pane: &mut Self, action: &RevealInProjectPanel, cx| {
1813 let entry_id = action
1814 .entry_id
1815 .map(ProjectEntryId::from_proto)
1816 .or_else(|| pane.active_item()?.project_entry_ids(cx).first().copied());
1817 if let Some(entry_id) = entry_id {
1818 pane.project.update(cx, |_, cx| {
1819 cx.emit(project::Event::RevealInProjectPanel(entry_id))
1820 });
1821 }
1822 }),
1823 )
1824 .when(self.active_item().is_some(), |pane| {
1825 pane.child(self.render_tab_bar(cx))
1826 })
1827 .child({
1828 let has_worktrees = self.project.read(cx).worktrees().next().is_some();
1829 // main content
1830 div()
1831 .flex_1()
1832 .relative()
1833 .group("")
1834 .on_drag_move::<DraggedTab>(cx.listener(Self::handle_drag_move))
1835 .on_drag_move::<ProjectEntryId>(cx.listener(Self::handle_drag_move))
1836 .on_drag_move::<ExternalPaths>(cx.listener(Self::handle_drag_move))
1837 .map(|div| {
1838 if let Some(item) = self.active_item() {
1839 div.v_flex()
1840 .child(self.toolbar.clone())
1841 .child(item.to_any())
1842 } else {
1843 let placeholder = div.h_flex().size_full().justify_center();
1844 if has_worktrees {
1845 placeholder
1846 } else {
1847 placeholder.child(
1848 Label::new("Open a file or project to get started.")
1849 .color(Color::Muted),
1850 )
1851 }
1852 }
1853 })
1854 .child(
1855 // drag target
1856 div()
1857 .z_index(1)
1858 .invisible()
1859 .absolute()
1860 .bg(theme::color_alpha(
1861 cx.theme().colors().drop_target_background,
1862 0.75,
1863 ))
1864 .group_drag_over::<DraggedTab>("", |style| style.visible())
1865 .group_drag_over::<ProjectEntryId>("", |style| style.visible())
1866 .group_drag_over::<ExternalPaths>("", |style| style.visible())
1867 .when_some(self.can_drop_predicate.clone(), |this, p| {
1868 this.can_drop(move |a, cx| p(a, cx))
1869 })
1870 .on_drop(cx.listener(move |this, dragged_tab, cx| {
1871 this.handle_tab_drop(dragged_tab, this.active_item_index(), cx)
1872 }))
1873 .on_drop(cx.listener(move |this, entry_id, cx| {
1874 this.handle_project_entry_drop(entry_id, cx)
1875 }))
1876 .on_drop(cx.listener(move |this, paths, cx| {
1877 this.handle_external_paths_drop(paths, cx)
1878 }))
1879 .map(|div| match self.drag_split_direction {
1880 None => div.top_0().left_0().right_0().bottom_0(),
1881 Some(SplitDirection::Up) => div.top_0().left_0().right_0().h_32(),
1882 Some(SplitDirection::Down) => {
1883 div.left_0().bottom_0().right_0().h_32()
1884 }
1885 Some(SplitDirection::Left) => {
1886 div.top_0().left_0().bottom_0().w_32()
1887 }
1888 Some(SplitDirection::Right) => {
1889 div.top_0().bottom_0().right_0().w_32()
1890 }
1891 }),
1892 )
1893 })
1894 .on_mouse_down(
1895 MouseButton::Navigate(NavigationDirection::Back),
1896 cx.listener(|pane, _, cx| {
1897 if let Some(workspace) = pane.workspace.upgrade() {
1898 let pane = cx.view().downgrade();
1899 cx.window_context().defer(move |cx| {
1900 workspace.update(cx, |workspace, cx| {
1901 workspace.go_back(pane, cx).detach_and_log_err(cx)
1902 })
1903 })
1904 }
1905 }),
1906 )
1907 .on_mouse_down(
1908 MouseButton::Navigate(NavigationDirection::Forward),
1909 cx.listener(|pane, _, cx| {
1910 if let Some(workspace) = pane.workspace.upgrade() {
1911 let pane = cx.view().downgrade();
1912 cx.window_context().defer(move |cx| {
1913 workspace.update(cx, |workspace, cx| {
1914 workspace.go_forward(pane, cx).detach_and_log_err(cx)
1915 })
1916 })
1917 }
1918 }),
1919 )
1920 }
1921}
1922
1923impl ItemNavHistory {
1924 pub fn push<D: 'static + Send + Any>(&mut self, data: Option<D>, cx: &mut WindowContext) {
1925 self.history.push(data, self.item.clone(), cx);
1926 }
1927
1928 pub fn pop_backward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
1929 self.history.pop(NavigationMode::GoingBack, cx)
1930 }
1931
1932 pub fn pop_forward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
1933 self.history.pop(NavigationMode::GoingForward, cx)
1934 }
1935}
1936
1937impl NavHistory {
1938 pub fn for_each_entry(
1939 &self,
1940 cx: &AppContext,
1941 mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
1942 ) {
1943 let borrowed_history = self.0.lock();
1944 borrowed_history
1945 .forward_stack
1946 .iter()
1947 .chain(borrowed_history.backward_stack.iter())
1948 .chain(borrowed_history.closed_stack.iter())
1949 .for_each(|entry| {
1950 if let Some(project_and_abs_path) =
1951 borrowed_history.paths_by_item.get(&entry.item.id())
1952 {
1953 f(entry, project_and_abs_path.clone());
1954 } else if let Some(item) = entry.item.upgrade() {
1955 if let Some(path) = item.project_path(cx) {
1956 f(entry, (path, None));
1957 }
1958 }
1959 })
1960 }
1961
1962 pub fn set_mode(&mut self, mode: NavigationMode) {
1963 self.0.lock().mode = mode;
1964 }
1965
1966 pub fn mode(&self) -> NavigationMode {
1967 self.0.lock().mode
1968 }
1969
1970 pub fn disable(&mut self) {
1971 self.0.lock().mode = NavigationMode::Disabled;
1972 }
1973
1974 pub fn enable(&mut self) {
1975 self.0.lock().mode = NavigationMode::Normal;
1976 }
1977
1978 pub fn pop(&mut self, mode: NavigationMode, cx: &mut WindowContext) -> Option<NavigationEntry> {
1979 let mut state = self.0.lock();
1980 let entry = match mode {
1981 NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
1982 return None
1983 }
1984 NavigationMode::GoingBack => &mut state.backward_stack,
1985 NavigationMode::GoingForward => &mut state.forward_stack,
1986 NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
1987 }
1988 .pop_back();
1989 if entry.is_some() {
1990 state.did_update(cx);
1991 }
1992 entry
1993 }
1994
1995 pub fn push<D: 'static + Send + Any>(
1996 &mut self,
1997 data: Option<D>,
1998 item: Arc<dyn WeakItemHandle>,
1999 cx: &mut WindowContext,
2000 ) {
2001 let state = &mut *self.0.lock();
2002 match state.mode {
2003 NavigationMode::Disabled => {}
2004 NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
2005 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2006 state.backward_stack.pop_front();
2007 }
2008 state.backward_stack.push_back(NavigationEntry {
2009 item,
2010 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2011 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2012 });
2013 state.forward_stack.clear();
2014 }
2015 NavigationMode::GoingBack => {
2016 if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2017 state.forward_stack.pop_front();
2018 }
2019 state.forward_stack.push_back(NavigationEntry {
2020 item,
2021 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2022 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2023 });
2024 }
2025 NavigationMode::GoingForward => {
2026 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2027 state.backward_stack.pop_front();
2028 }
2029 state.backward_stack.push_back(NavigationEntry {
2030 item,
2031 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2032 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2033 });
2034 }
2035 NavigationMode::ClosingItem => {
2036 if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2037 state.closed_stack.pop_front();
2038 }
2039 state.closed_stack.push_back(NavigationEntry {
2040 item,
2041 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2042 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2043 });
2044 }
2045 }
2046 state.did_update(cx);
2047 }
2048
2049 pub fn remove_item(&mut self, item_id: EntityId) {
2050 let mut state = self.0.lock();
2051 state.paths_by_item.remove(&item_id);
2052 state
2053 .backward_stack
2054 .retain(|entry| entry.item.id() != item_id);
2055 state
2056 .forward_stack
2057 .retain(|entry| entry.item.id() != item_id);
2058 state
2059 .closed_stack
2060 .retain(|entry| entry.item.id() != item_id);
2061 }
2062
2063 pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option<PathBuf>)> {
2064 self.0.lock().paths_by_item.get(&item_id).cloned()
2065 }
2066}
2067
2068impl NavHistoryState {
2069 pub fn did_update(&self, cx: &mut WindowContext) {
2070 if let Some(pane) = self.pane.upgrade() {
2071 cx.defer(move |cx| {
2072 pane.update(cx, |pane, cx| pane.history_updated(cx));
2073 });
2074 }
2075 }
2076}
2077
2078fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
2079 let path = buffer_path
2080 .as_ref()
2081 .and_then(|p| p.path.to_str())
2082 .unwrap_or("This buffer");
2083 let path = truncate_and_remove_front(path, 80);
2084 format!("{path} contains unsaved edits. Do you want to save it?")
2085}
2086
2087#[cfg(test)]
2088mod tests {
2089 use super::*;
2090 use crate::item::test::{TestItem, TestProjectItem};
2091 use gpui::{TestAppContext, VisualTestContext};
2092 use project::FakeFs;
2093 use settings::SettingsStore;
2094 use theme::LoadThemes;
2095
2096 #[gpui::test]
2097 async fn test_remove_active_empty(cx: &mut TestAppContext) {
2098 init_test(cx);
2099 let fs = FakeFs::new(cx.executor());
2100
2101 let project = Project::test(fs, None, cx).await;
2102 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2103 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2104
2105 pane.update(cx, |pane, cx| {
2106 assert!(pane
2107 .close_active_item(&CloseActiveItem { save_intent: None }, cx)
2108 .is_none())
2109 });
2110 }
2111
2112 #[gpui::test]
2113 async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
2114 init_test(cx);
2115 let fs = FakeFs::new(cx.executor());
2116
2117 let project = Project::test(fs, None, cx).await;
2118 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2119 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2120
2121 // 1. Add with a destination index
2122 // a. Add before the active item
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(0),
2130 cx,
2131 );
2132 });
2133 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
2134
2135 // b. Add after the active item
2136 set_labeled_items(&pane, ["A", "B*", "C"], cx);
2137 pane.update(cx, |pane, cx| {
2138 pane.add_item(
2139 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2140 false,
2141 false,
2142 Some(2),
2143 cx,
2144 );
2145 });
2146 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
2147
2148 // c. Add at the end of the item list (including off the length)
2149 set_labeled_items(&pane, ["A", "B*", "C"], cx);
2150 pane.update(cx, |pane, cx| {
2151 pane.add_item(
2152 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2153 false,
2154 false,
2155 Some(5),
2156 cx,
2157 );
2158 });
2159 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2160
2161 // 2. Add without a destination index
2162 // a. Add with active item at the start of the item list
2163 set_labeled_items(&pane, ["A*", "B", "C"], cx);
2164 pane.update(cx, |pane, cx| {
2165 pane.add_item(
2166 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2167 false,
2168 false,
2169 None,
2170 cx,
2171 );
2172 });
2173 set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
2174
2175 // b. Add with active item at the end of the item list
2176 set_labeled_items(&pane, ["A", "B", "C*"], cx);
2177 pane.update(cx, |pane, cx| {
2178 pane.add_item(
2179 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2180 false,
2181 false,
2182 None,
2183 cx,
2184 );
2185 });
2186 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2187 }
2188
2189 #[gpui::test]
2190 async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
2191 init_test(cx);
2192 let fs = FakeFs::new(cx.executor());
2193
2194 let project = Project::test(fs, None, cx).await;
2195 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2196 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2197
2198 // 1. Add with a destination index
2199 // 1a. Add before 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(0), cx);
2203 });
2204 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
2205
2206 // 1b. Add after the active item
2207 let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
2208 pane.update(cx, |pane, cx| {
2209 pane.add_item(d, false, false, Some(2), cx);
2210 });
2211 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
2212
2213 // 1c. Add at the end of the item list (including off the length)
2214 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
2215 pane.update(cx, |pane, cx| {
2216 pane.add_item(a, false, false, Some(5), cx);
2217 });
2218 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
2219
2220 // 1d. Add same item to active index
2221 let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
2222 pane.update(cx, |pane, cx| {
2223 pane.add_item(b, false, false, Some(1), cx);
2224 });
2225 assert_item_labels(&pane, ["A", "B*", "C"], cx);
2226
2227 // 1e. Add item to index after same item in last position
2228 let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
2229 pane.update(cx, |pane, cx| {
2230 pane.add_item(c, false, false, Some(2), cx);
2231 });
2232 assert_item_labels(&pane, ["A", "B", "C*"], cx);
2233
2234 // 2. Add without a destination index
2235 // 2a. Add with active item at the start of the item list
2236 let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
2237 pane.update(cx, |pane, cx| {
2238 pane.add_item(d, false, false, None, cx);
2239 });
2240 assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
2241
2242 // 2b. Add with active item at the end of the item list
2243 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
2244 pane.update(cx, |pane, cx| {
2245 pane.add_item(a, false, false, None, cx);
2246 });
2247 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
2248
2249 // 2c. Add active item to active item at end of list
2250 let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
2251 pane.update(cx, |pane, cx| {
2252 pane.add_item(c, false, false, None, cx);
2253 });
2254 assert_item_labels(&pane, ["A", "B", "C*"], cx);
2255
2256 // 2d. Add active item to active item at start of list
2257 let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
2258 pane.update(cx, |pane, cx| {
2259 pane.add_item(a, false, false, None, cx);
2260 });
2261 assert_item_labels(&pane, ["A*", "B", "C"], cx);
2262 }
2263
2264 #[gpui::test]
2265 async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
2266 init_test(cx);
2267 let fs = FakeFs::new(cx.executor());
2268
2269 let project = Project::test(fs, None, cx).await;
2270 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2271 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2272
2273 // singleton view
2274 pane.update(cx, |pane, cx| {
2275 pane.add_item(
2276 Box::new(cx.new_view(|cx| {
2277 TestItem::new(cx)
2278 .with_singleton(true)
2279 .with_label("buffer 1")
2280 .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
2281 })),
2282 false,
2283 false,
2284 None,
2285 cx,
2286 );
2287 });
2288 assert_item_labels(&pane, ["buffer 1*"], cx);
2289
2290 // new singleton view with the same project entry
2291 pane.update(cx, |pane, cx| {
2292 pane.add_item(
2293 Box::new(cx.new_view(|cx| {
2294 TestItem::new(cx)
2295 .with_singleton(true)
2296 .with_label("buffer 1")
2297 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
2298 })),
2299 false,
2300 false,
2301 None,
2302 cx,
2303 );
2304 });
2305 assert_item_labels(&pane, ["buffer 1*"], cx);
2306
2307 // new singleton view with different project entry
2308 pane.update(cx, |pane, cx| {
2309 pane.add_item(
2310 Box::new(cx.new_view(|cx| {
2311 TestItem::new(cx)
2312 .with_singleton(true)
2313 .with_label("buffer 2")
2314 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
2315 })),
2316 false,
2317 false,
2318 None,
2319 cx,
2320 );
2321 });
2322 assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
2323
2324 // new multibuffer view with the same project entry
2325 pane.update(cx, |pane, cx| {
2326 pane.add_item(
2327 Box::new(cx.new_view(|cx| {
2328 TestItem::new(cx)
2329 .with_singleton(false)
2330 .with_label("multibuffer 1")
2331 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
2332 })),
2333 false,
2334 false,
2335 None,
2336 cx,
2337 );
2338 });
2339 assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
2340
2341 // another multibuffer view with the same project entry
2342 pane.update(cx, |pane, cx| {
2343 pane.add_item(
2344 Box::new(cx.new_view(|cx| {
2345 TestItem::new(cx)
2346 .with_singleton(false)
2347 .with_label("multibuffer 1b")
2348 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
2349 })),
2350 false,
2351 false,
2352 None,
2353 cx,
2354 );
2355 });
2356 assert_item_labels(
2357 &pane,
2358 ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
2359 cx,
2360 );
2361 }
2362
2363 #[gpui::test]
2364 async fn test_remove_item_ordering(cx: &mut TestAppContext) {
2365 init_test(cx);
2366 let fs = FakeFs::new(cx.executor());
2367
2368 let project = Project::test(fs, None, cx).await;
2369 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2370 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2371
2372 add_labeled_item(&pane, "A", false, cx);
2373 add_labeled_item(&pane, "B", false, cx);
2374 add_labeled_item(&pane, "C", false, cx);
2375 add_labeled_item(&pane, "D", false, cx);
2376 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2377
2378 pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
2379 add_labeled_item(&pane, "1", false, cx);
2380 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
2381
2382 pane.update(cx, |pane, cx| {
2383 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
2384 })
2385 .unwrap()
2386 .await
2387 .unwrap();
2388 assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
2389
2390 pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
2391 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2392
2393 pane.update(cx, |pane, cx| {
2394 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
2395 })
2396 .unwrap()
2397 .await
2398 .unwrap();
2399 assert_item_labels(&pane, ["A", "B*", "C"], cx);
2400
2401 pane.update(cx, |pane, cx| {
2402 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
2403 })
2404 .unwrap()
2405 .await
2406 .unwrap();
2407 assert_item_labels(&pane, ["A", "C*"], cx);
2408
2409 pane.update(cx, |pane, cx| {
2410 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
2411 })
2412 .unwrap()
2413 .await
2414 .unwrap();
2415 assert_item_labels(&pane, ["A*"], cx);
2416 }
2417
2418 #[gpui::test]
2419 async fn test_close_inactive_items(cx: &mut TestAppContext) {
2420 init_test(cx);
2421 let fs = FakeFs::new(cx.executor());
2422
2423 let project = Project::test(fs, None, cx).await;
2424 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2425 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2426
2427 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
2428
2429 pane.update(cx, |pane, cx| {
2430 pane.close_inactive_items(&CloseInactiveItems, cx)
2431 })
2432 .unwrap()
2433 .await
2434 .unwrap();
2435 assert_item_labels(&pane, ["C*"], cx);
2436 }
2437
2438 #[gpui::test]
2439 async fn test_close_clean_items(cx: &mut TestAppContext) {
2440 init_test(cx);
2441 let fs = FakeFs::new(cx.executor());
2442
2443 let project = Project::test(fs, None, cx).await;
2444 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2445 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2446
2447 add_labeled_item(&pane, "A", true, cx);
2448 add_labeled_item(&pane, "B", false, cx);
2449 add_labeled_item(&pane, "C", true, cx);
2450 add_labeled_item(&pane, "D", false, cx);
2451 add_labeled_item(&pane, "E", false, cx);
2452 assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
2453
2454 pane.update(cx, |pane, cx| pane.close_clean_items(&CloseCleanItems, cx))
2455 .unwrap()
2456 .await
2457 .unwrap();
2458 assert_item_labels(&pane, ["A^", "C*^"], cx);
2459 }
2460
2461 #[gpui::test]
2462 async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
2463 init_test(cx);
2464 let fs = FakeFs::new(cx.executor());
2465
2466 let project = Project::test(fs, None, cx).await;
2467 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2468 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2469
2470 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
2471
2472 pane.update(cx, |pane, cx| {
2473 pane.close_items_to_the_left(&CloseItemsToTheLeft, cx)
2474 })
2475 .unwrap()
2476 .await
2477 .unwrap();
2478 assert_item_labels(&pane, ["C*", "D", "E"], cx);
2479 }
2480
2481 #[gpui::test]
2482 async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
2483 init_test(cx);
2484 let fs = FakeFs::new(cx.executor());
2485
2486 let project = Project::test(fs, None, cx).await;
2487 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2488 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2489
2490 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
2491
2492 pane.update(cx, |pane, cx| {
2493 pane.close_items_to_the_right(&CloseItemsToTheRight, cx)
2494 })
2495 .unwrap()
2496 .await
2497 .unwrap();
2498 assert_item_labels(&pane, ["A", "B", "C*"], cx);
2499 }
2500
2501 #[gpui::test]
2502 async fn test_close_all_items(cx: &mut TestAppContext) {
2503 init_test(cx);
2504 let fs = FakeFs::new(cx.executor());
2505
2506 let project = Project::test(fs, None, cx).await;
2507 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2508 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2509
2510 add_labeled_item(&pane, "A", false, cx);
2511 add_labeled_item(&pane, "B", false, cx);
2512 add_labeled_item(&pane, "C", false, cx);
2513 assert_item_labels(&pane, ["A", "B", "C*"], cx);
2514
2515 pane.update(cx, |pane, cx| {
2516 pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
2517 })
2518 .unwrap()
2519 .await
2520 .unwrap();
2521 assert_item_labels(&pane, [], cx);
2522
2523 add_labeled_item(&pane, "A", true, cx);
2524 add_labeled_item(&pane, "B", true, cx);
2525 add_labeled_item(&pane, "C", true, cx);
2526 assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
2527
2528 let save = pane
2529 .update(cx, |pane, cx| {
2530 pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
2531 })
2532 .unwrap();
2533
2534 cx.executor().run_until_parked();
2535 cx.simulate_prompt_answer(2);
2536 save.await.unwrap();
2537 assert_item_labels(&pane, [], cx);
2538 }
2539
2540 fn init_test(cx: &mut TestAppContext) {
2541 cx.update(|cx| {
2542 let settings_store = SettingsStore::test(cx);
2543 cx.set_global(settings_store);
2544 theme::init(LoadThemes::JustBase, cx);
2545 crate::init_settings(cx);
2546 Project::init_settings(cx);
2547 });
2548 }
2549
2550 fn add_labeled_item(
2551 pane: &View<Pane>,
2552 label: &str,
2553 is_dirty: bool,
2554 cx: &mut VisualTestContext,
2555 ) -> Box<View<TestItem>> {
2556 pane.update(cx, |pane, cx| {
2557 let labeled_item = Box::new(
2558 cx.new_view(|cx| TestItem::new(cx).with_label(label).with_dirty(is_dirty)),
2559 );
2560 pane.add_item(labeled_item.clone(), false, false, None, cx);
2561 labeled_item
2562 })
2563 }
2564
2565 fn set_labeled_items<const COUNT: usize>(
2566 pane: &View<Pane>,
2567 labels: [&str; COUNT],
2568 cx: &mut VisualTestContext,
2569 ) -> [Box<View<TestItem>>; COUNT] {
2570 pane.update(cx, |pane, cx| {
2571 pane.items.clear();
2572 let mut active_item_index = 0;
2573
2574 let mut index = 0;
2575 let items = labels.map(|mut label| {
2576 if label.ends_with("*") {
2577 label = label.trim_end_matches("*");
2578 active_item_index = index;
2579 }
2580
2581 let labeled_item = Box::new(cx.new_view(|cx| TestItem::new(cx).with_label(label)));
2582 pane.add_item(labeled_item.clone(), false, false, None, cx);
2583 index += 1;
2584 labeled_item
2585 });
2586
2587 pane.activate_item(active_item_index, false, false, cx);
2588
2589 items
2590 })
2591 }
2592
2593 // Assert the item label, with the active item label suffixed with a '*'
2594 fn assert_item_labels<const COUNT: usize>(
2595 pane: &View<Pane>,
2596 expected_states: [&str; COUNT],
2597 cx: &mut VisualTestContext,
2598 ) {
2599 pane.update(cx, |pane, cx| {
2600 let actual_states = pane
2601 .items
2602 .iter()
2603 .enumerate()
2604 .map(|(ix, item)| {
2605 let mut state = item
2606 .to_any()
2607 .downcast::<TestItem>()
2608 .unwrap()
2609 .read(cx)
2610 .label
2611 .clone();
2612 if ix == pane.active_item_index {
2613 state.push('*');
2614 }
2615 if item.is_dirty(cx) {
2616 state.push('^');
2617 }
2618 state
2619 })
2620 .collect::<Vec<_>>();
2621
2622 assert_eq!(
2623 actual_states, expected_states,
2624 "pane items do not match expectation"
2625 );
2626 })
2627 }
2628}
2629
2630impl Render for DraggedTab {
2631 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2632 let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
2633 let label = self.item.tab_content(Some(self.detail), false, cx);
2634 Tab::new("")
2635 .selected(self.is_active)
2636 .child(label)
2637 .render(cx)
2638 .font(ui_font)
2639 }
2640}