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