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