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