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