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