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