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