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