1use super::{ItemHandle, SplitDirection};
2use crate::{
3 dock::{icon_for_dock_anchor, MoveDock, ToggleDock},
4 toolbar::Toolbar,
5 Item, NewFile, NewSearch, NewTerminal, WeakItemHandle, Workspace,
6};
7use anyhow::Result;
8use collections::{HashMap, HashSet, VecDeque};
9use context_menu::{ContextMenu, ContextMenuItem};
10use drag_and_drop::{DragAndDrop, Draggable};
11use futures::StreamExt;
12use gpui::{
13 actions,
14 color::Color,
15 elements::*,
16 geometry::{
17 rect::RectF,
18 vector::{vec2f, Vector2F},
19 },
20 impl_actions, impl_internal_actions,
21 platform::{CursorStyle, NavigationDirection},
22 Action, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, EventContext,
23 ModelHandle, MouseButton, MutableAppContext, PromptLevel, Quad, RenderContext, Task, View,
24 ViewContext, ViewHandle, WeakViewHandle,
25};
26use project::{Project, ProjectEntryId, ProjectPath};
27use serde::Deserialize;
28use settings::{Autosave, DockAnchor, Settings};
29use std::{any::Any, cell::RefCell, cmp, mem, path::Path, rc::Rc};
30use theme::Theme;
31use util::ResultExt;
32
33#[derive(Clone, Deserialize, PartialEq)]
34pub struct ActivateItem(pub usize);
35
36actions!(
37 pane,
38 [
39 ActivatePrevItem,
40 ActivateNextItem,
41 ActivateLastItem,
42 CloseActiveItem,
43 CloseInactiveItems,
44 ReopenClosedItem,
45 SplitLeft,
46 SplitUp,
47 SplitRight,
48 SplitDown,
49 ]
50);
51
52#[derive(Clone, PartialEq)]
53pub struct CloseItem {
54 pub item_id: usize,
55 pub pane: WeakViewHandle<Pane>,
56}
57
58#[derive(Clone, PartialEq)]
59pub struct MoveItem {
60 pub item_id: usize,
61 pub from: WeakViewHandle<Pane>,
62 pub to: WeakViewHandle<Pane>,
63 pub destination_index: usize,
64}
65
66#[derive(Clone, Deserialize, PartialEq)]
67pub struct GoBack {
68 #[serde(skip_deserializing)]
69 pub pane: Option<WeakViewHandle<Pane>>,
70}
71
72#[derive(Clone, Deserialize, PartialEq)]
73pub struct GoForward {
74 #[serde(skip_deserializing)]
75 pub pane: Option<WeakViewHandle<Pane>>,
76}
77
78#[derive(Clone, PartialEq)]
79pub struct DeploySplitMenu {
80 position: Vector2F,
81}
82
83#[derive(Clone, PartialEq)]
84pub struct DeployDockMenu {
85 position: Vector2F,
86}
87
88#[derive(Clone, PartialEq)]
89pub struct DeployNewMenu {
90 position: Vector2F,
91}
92
93impl_actions!(pane, [GoBack, GoForward, ActivateItem]);
94impl_internal_actions!(
95 pane,
96 [
97 CloseItem,
98 DeploySplitMenu,
99 DeployNewMenu,
100 DeployDockMenu,
101 MoveItem
102 ]
103);
104
105const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
106
107pub fn init(cx: &mut MutableAppContext) {
108 cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| {
109 pane.activate_item(action.0, true, true, cx);
110 });
111 cx.add_action(|pane: &mut Pane, _: &ActivateLastItem, cx| {
112 pane.activate_item(pane.items.len() - 1, true, true, cx);
113 });
114 cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
115 pane.activate_prev_item(cx);
116 });
117 cx.add_action(|pane: &mut Pane, _: &ActivateNextItem, cx| {
118 pane.activate_next_item(cx);
119 });
120 cx.add_async_action(Pane::close_active_item);
121 cx.add_async_action(Pane::close_inactive_items);
122 cx.add_async_action(|workspace: &mut Workspace, action: &CloseItem, cx| {
123 let pane = action.pane.upgrade(cx)?;
124 let task = Pane::close_item(workspace, pane, action.item_id, cx);
125 Some(cx.foreground().spawn(async move {
126 task.await?;
127 Ok(())
128 }))
129 });
130 cx.add_action(
131 |workspace,
132 MoveItem {
133 from,
134 to,
135 item_id,
136 destination_index,
137 },
138 cx| {
139 // Get item handle to move
140 let from = if let Some(from) = from.upgrade(cx) {
141 from
142 } else {
143 return;
144 };
145
146 // Add item to new pane at given index
147 let to = if let Some(to) = to.upgrade(cx) {
148 to
149 } else {
150 return;
151 };
152
153 Pane::move_item(workspace, from, to, *item_id, *destination_index, cx)
154 },
155 );
156 cx.add_action(|pane: &mut Pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx));
157 cx.add_action(|pane: &mut Pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx));
158 cx.add_action(|pane: &mut Pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx));
159 cx.add_action(|pane: &mut Pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx));
160 cx.add_action(Pane::deploy_split_menu);
161 cx.add_action(Pane::deploy_new_menu);
162 cx.add_action(Pane::deploy_dock_menu);
163 cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
164 Pane::reopen_closed_item(workspace, cx).detach();
165 });
166 cx.add_action(|workspace: &mut Workspace, action: &GoBack, cx| {
167 Pane::go_back(
168 workspace,
169 action
170 .pane
171 .as_ref()
172 .and_then(|weak_handle| weak_handle.upgrade(cx)),
173 cx,
174 )
175 .detach();
176 });
177 cx.add_action(|workspace: &mut Workspace, action: &GoForward, cx| {
178 Pane::go_forward(
179 workspace,
180 action
181 .pane
182 .as_ref()
183 .and_then(|weak_handle| weak_handle.upgrade(cx)),
184 cx,
185 )
186 .detach();
187 });
188}
189
190#[derive(Debug)]
191pub enum Event {
192 Focused,
193 ActivateItem { local: bool },
194 Remove,
195 RemoveItem { item_id: usize },
196 Split(SplitDirection),
197 ChangeItemTitle,
198}
199
200pub struct Pane {
201 items: Vec<Box<dyn ItemHandle>>,
202 is_active: bool,
203 active_item_index: usize,
204 last_focused_view: Option<AnyWeakViewHandle>,
205 autoscroll: bool,
206 nav_history: Rc<RefCell<NavHistory>>,
207 toolbar: ViewHandle<Toolbar>,
208 tab_bar_context_menu: ViewHandle<ContextMenu>,
209 docked: Option<DockAnchor>,
210}
211
212pub struct ItemNavHistory {
213 history: Rc<RefCell<NavHistory>>,
214 item: Rc<dyn WeakItemHandle>,
215}
216
217struct NavHistory {
218 mode: NavigationMode,
219 backward_stack: VecDeque<NavigationEntry>,
220 forward_stack: VecDeque<NavigationEntry>,
221 closed_stack: VecDeque<NavigationEntry>,
222 paths_by_item: HashMap<usize, ProjectPath>,
223 pane: WeakViewHandle<Pane>,
224}
225
226#[derive(Copy, Clone)]
227enum NavigationMode {
228 Normal,
229 GoingBack,
230 GoingForward,
231 ClosingItem,
232 ReopeningClosedItem,
233 Disabled,
234}
235
236impl Default for NavigationMode {
237 fn default() -> Self {
238 Self::Normal
239 }
240}
241
242pub struct NavigationEntry {
243 pub item: Rc<dyn WeakItemHandle>,
244 pub data: Option<Box<dyn Any>>,
245}
246
247struct DraggedItem {
248 item: Box<dyn ItemHandle>,
249 pane: WeakViewHandle<Pane>,
250}
251
252pub enum ReorderBehavior {
253 None,
254 MoveAfterActive,
255 MoveToIndex(usize),
256}
257
258impl Pane {
259 pub fn new(docked: Option<DockAnchor>, cx: &mut ViewContext<Self>) -> Self {
260 let handle = cx.weak_handle();
261 let context_menu = cx.add_view(ContextMenu::new);
262 Self {
263 items: Vec::new(),
264 is_active: true,
265 active_item_index: 0,
266 last_focused_view: None,
267 autoscroll: false,
268 nav_history: Rc::new(RefCell::new(NavHistory {
269 mode: NavigationMode::Normal,
270 backward_stack: Default::default(),
271 forward_stack: Default::default(),
272 closed_stack: Default::default(),
273 paths_by_item: Default::default(),
274 pane: handle.clone(),
275 })),
276 toolbar: cx.add_view(|_| Toolbar::new(handle)),
277 tab_bar_context_menu: context_menu,
278 docked,
279 }
280 }
281
282 pub fn set_active(&mut self, is_active: bool, cx: &mut ViewContext<Self>) {
283 self.is_active = is_active;
284 cx.notify();
285 }
286
287 pub fn set_docked(&mut self, docked: Option<DockAnchor>, cx: &mut ViewContext<Self>) {
288 self.docked = docked;
289 cx.notify();
290 }
291
292 pub fn nav_history_for_item<T: Item>(&self, item: &ViewHandle<T>) -> ItemNavHistory {
293 ItemNavHistory {
294 history: self.nav_history.clone(),
295 item: Rc::new(item.downgrade()),
296 }
297 }
298
299 pub fn go_back(
300 workspace: &mut Workspace,
301 pane: Option<ViewHandle<Pane>>,
302 cx: &mut ViewContext<Workspace>,
303 ) -> Task<()> {
304 Self::navigate_history(
305 workspace,
306 pane.unwrap_or_else(|| workspace.active_pane().clone()),
307 NavigationMode::GoingBack,
308 cx,
309 )
310 }
311
312 pub fn go_forward(
313 workspace: &mut Workspace,
314 pane: Option<ViewHandle<Pane>>,
315 cx: &mut ViewContext<Workspace>,
316 ) -> Task<()> {
317 Self::navigate_history(
318 workspace,
319 pane.unwrap_or_else(|| workspace.active_pane().clone()),
320 NavigationMode::GoingForward,
321 cx,
322 )
323 }
324
325 pub fn reopen_closed_item(
326 workspace: &mut Workspace,
327 cx: &mut ViewContext<Workspace>,
328 ) -> Task<()> {
329 Self::navigate_history(
330 workspace,
331 workspace.active_pane().clone(),
332 NavigationMode::ReopeningClosedItem,
333 cx,
334 )
335 }
336
337 pub fn disable_history(&mut self) {
338 self.nav_history.borrow_mut().disable();
339 }
340
341 pub fn enable_history(&mut self) {
342 self.nav_history.borrow_mut().enable();
343 }
344
345 pub fn can_navigate_backward(&self) -> bool {
346 !self.nav_history.borrow().backward_stack.is_empty()
347 }
348
349 pub fn can_navigate_forward(&self) -> bool {
350 !self.nav_history.borrow().forward_stack.is_empty()
351 }
352
353 fn history_updated(&mut self, cx: &mut ViewContext<Self>) {
354 self.toolbar.update(cx, |_, cx| cx.notify());
355 }
356
357 fn navigate_history(
358 workspace: &mut Workspace,
359 pane: ViewHandle<Pane>,
360 mode: NavigationMode,
361 cx: &mut ViewContext<Workspace>,
362 ) -> Task<()> {
363 cx.focus(pane.clone());
364
365 let to_load = pane.update(cx, |pane, cx| {
366 loop {
367 // Retrieve the weak item handle from the history.
368 let entry = pane.nav_history.borrow_mut().pop(mode, cx)?;
369
370 // If the item is still present in this pane, then activate it.
371 if let Some(index) = entry
372 .item
373 .upgrade(cx)
374 .and_then(|v| pane.index_for_item(v.as_ref()))
375 {
376 let prev_active_item_index = pane.active_item_index;
377 pane.nav_history.borrow_mut().set_mode(mode);
378 pane.activate_item(index, true, true, cx);
379 pane.nav_history
380 .borrow_mut()
381 .set_mode(NavigationMode::Normal);
382
383 let mut navigated = prev_active_item_index != pane.active_item_index;
384 if let Some(data) = entry.data {
385 navigated |= pane.active_item()?.navigate(data, cx);
386 }
387
388 if navigated {
389 break None;
390 }
391 }
392 // If the item is no longer present in this pane, then retrieve its
393 // project path in order to reopen it.
394 else {
395 break pane
396 .nav_history
397 .borrow()
398 .paths_by_item
399 .get(&entry.item.id())
400 .cloned()
401 .map(|project_path| (project_path, entry));
402 }
403 }
404 });
405
406 if let Some((project_path, entry)) = to_load {
407 // If the item was no longer present, then load it again from its previous path.
408 let pane = pane.downgrade();
409 let task = workspace.load_path(project_path, cx);
410 cx.spawn(|workspace, mut cx| async move {
411 let task = task.await;
412 if let Some(pane) = pane.upgrade(&cx) {
413 let mut navigated = false;
414 if let Some((project_entry_id, build_item)) = task.log_err() {
415 let prev_active_item_id = pane.update(&mut cx, |pane, _| {
416 pane.nav_history.borrow_mut().set_mode(mode);
417 pane.active_item().map(|p| p.id())
418 });
419
420 let item = workspace.update(&mut cx, |workspace, cx| {
421 Self::open_item(
422 workspace,
423 pane.clone(),
424 project_entry_id,
425 true,
426 cx,
427 build_item,
428 )
429 });
430
431 pane.update(&mut cx, |pane, cx| {
432 navigated |= Some(item.id()) != prev_active_item_id;
433 pane.nav_history
434 .borrow_mut()
435 .set_mode(NavigationMode::Normal);
436 if let Some(data) = entry.data {
437 navigated |= item.navigate(data, cx);
438 }
439 });
440 }
441
442 if !navigated {
443 workspace
444 .update(&mut cx, |workspace, cx| {
445 Self::navigate_history(workspace, pane, mode, cx)
446 })
447 .await;
448 }
449 }
450 })
451 } else {
452 Task::ready(())
453 }
454 }
455
456 pub(crate) fn open_item(
457 workspace: &mut Workspace,
458 pane: ViewHandle<Pane>,
459 project_entry_id: ProjectEntryId,
460 focus_item: bool,
461 cx: &mut ViewContext<Workspace>,
462 build_item: impl FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
463 ) -> Box<dyn ItemHandle> {
464 let existing_item = pane.update(cx, |pane, cx| {
465 for item in pane.items.iter() {
466 if item.project_path(cx).is_some()
467 && item.project_entry_ids(cx).as_slice() == [project_entry_id]
468 {
469 let item = item.boxed_clone();
470 return Some(item);
471 }
472 }
473 None
474 });
475
476 // Even if the item exists, we re-add it to reorder it after the active item.
477 // We may revisit this behavior after adding an "activation history" for pane items.
478 let item = existing_item.unwrap_or_else(|| pane.update(cx, |_, cx| build_item(cx)));
479 Pane::add_item(workspace, &pane, item.clone(), true, focus_item, None, cx);
480 item
481 }
482
483 pub(crate) fn add_item(
484 workspace: &mut Workspace,
485 pane: &ViewHandle<Pane>,
486 item: Box<dyn ItemHandle>,
487 activate_pane: bool,
488 focus_item: bool,
489 destination_index: Option<usize>,
490 cx: &mut ViewContext<Workspace>,
491 ) {
492 // If no destination index is specified, add or move the item after the active item.
493 let mut insertion_index = {
494 let pane = pane.read(cx);
495 cmp::min(
496 if let Some(destination_index) = destination_index {
497 destination_index
498 } else {
499 pane.active_item_index + 1
500 },
501 pane.items.len(),
502 )
503 };
504
505 item.added_to_pane(workspace, pane.clone(), cx);
506
507 // Does the item already exist?
508 let project_entry_id = if item.is_singleton(cx) {
509 item.project_entry_ids(cx).get(0).copied()
510 } else {
511 None
512 };
513
514 let existing_item_index = pane.read(cx).items.iter().position(|existing_item| {
515 if existing_item.id() == item.id() {
516 true
517 } else if existing_item.is_singleton(cx) {
518 existing_item
519 .project_entry_ids(cx)
520 .get(0)
521 .map_or(false, |existing_entry_id| {
522 Some(existing_entry_id) == project_entry_id.as_ref()
523 })
524 } else {
525 false
526 }
527 });
528
529 if let Some(existing_item_index) = existing_item_index {
530 // If the item already exists, move it to the desired destination and activate it
531 pane.update(cx, |pane, cx| {
532 if existing_item_index != insertion_index {
533 cx.reparent(&item);
534 let existing_item_is_active = existing_item_index == pane.active_item_index;
535
536 // If the caller didn't specify a destination and the added item is already
537 // the active one, don't move it
538 if existing_item_is_active && destination_index.is_none() {
539 insertion_index = existing_item_index;
540 } else {
541 pane.items.remove(existing_item_index);
542 if existing_item_index < pane.active_item_index {
543 pane.active_item_index -= 1;
544 }
545 insertion_index = insertion_index.min(pane.items.len());
546
547 pane.items.insert(insertion_index, item.clone());
548
549 if existing_item_is_active {
550 pane.active_item_index = insertion_index;
551 } else if insertion_index <= pane.active_item_index {
552 pane.active_item_index += 1;
553 }
554 }
555
556 cx.notify();
557 }
558
559 pane.activate_item(insertion_index, activate_pane, focus_item, cx);
560 });
561 } else {
562 pane.update(cx, |pane, cx| {
563 cx.reparent(&item);
564 pane.items.insert(insertion_index, item);
565 if insertion_index <= pane.active_item_index {
566 pane.active_item_index += 1;
567 }
568
569 pane.activate_item(insertion_index, activate_pane, focus_item, cx);
570 cx.notify();
571 });
572 }
573 }
574
575 pub fn items(&self) -> impl Iterator<Item = &Box<dyn ItemHandle>> {
576 self.items.iter()
577 }
578
579 pub fn items_of_type<T: View>(&self) -> impl '_ + Iterator<Item = ViewHandle<T>> {
580 self.items
581 .iter()
582 .filter_map(|item| item.to_any().downcast())
583 }
584
585 pub fn active_item(&self) -> Option<Box<dyn ItemHandle>> {
586 self.items.get(self.active_item_index).cloned()
587 }
588
589 pub fn item_for_entry(
590 &self,
591 entry_id: ProjectEntryId,
592 cx: &AppContext,
593 ) -> Option<Box<dyn ItemHandle>> {
594 self.items.iter().find_map(|item| {
595 if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
596 Some(item.boxed_clone())
597 } else {
598 None
599 }
600 })
601 }
602
603 pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option<usize> {
604 self.items.iter().position(|i| i.id() == item.id())
605 }
606
607 pub fn activate_item(
608 &mut self,
609 index: usize,
610 activate_pane: bool,
611 focus_item: bool,
612 cx: &mut ViewContext<Self>,
613 ) {
614 use NavigationMode::{GoingBack, GoingForward};
615 if index < self.items.len() {
616 let prev_active_item_ix = mem::replace(&mut self.active_item_index, index);
617 if prev_active_item_ix != self.active_item_index
618 || matches!(self.nav_history.borrow().mode, GoingBack | GoingForward)
619 {
620 if let Some(prev_item) = self.items.get(prev_active_item_ix) {
621 prev_item.deactivated(cx);
622 }
623 cx.emit(Event::ActivateItem {
624 local: activate_pane,
625 });
626 }
627 self.update_toolbar(cx);
628 if focus_item {
629 self.focus_active_item(cx);
630 }
631 if activate_pane {
632 cx.emit(Event::Focused);
633 }
634 self.autoscroll = true;
635 cx.notify();
636 }
637 }
638
639 pub fn activate_prev_item(&mut self, cx: &mut ViewContext<Self>) {
640 let mut index = self.active_item_index;
641 if index > 0 {
642 index -= 1;
643 } else if !self.items.is_empty() {
644 index = self.items.len() - 1;
645 }
646 self.activate_item(index, true, true, cx);
647 }
648
649 pub fn activate_next_item(&mut self, cx: &mut ViewContext<Self>) {
650 let mut index = self.active_item_index;
651 if index + 1 < self.items.len() {
652 index += 1;
653 } else {
654 index = 0;
655 }
656 self.activate_item(index, true, true, cx);
657 }
658
659 pub fn close_active_item(
660 workspace: &mut Workspace,
661 _: &CloseActiveItem,
662 cx: &mut ViewContext<Workspace>,
663 ) -> Option<Task<Result<()>>> {
664 let pane_handle = workspace.active_pane().clone();
665 let pane = pane_handle.read(cx);
666 if pane.items.is_empty() {
667 None
668 } else {
669 let item_id_to_close = pane.items[pane.active_item_index].id();
670 let task = Self::close_items(workspace, pane_handle, cx, move |item_id| {
671 item_id == item_id_to_close
672 });
673 Some(cx.foreground().spawn(async move {
674 task.await?;
675 Ok(())
676 }))
677 }
678 }
679
680 pub fn close_inactive_items(
681 workspace: &mut Workspace,
682 _: &CloseInactiveItems,
683 cx: &mut ViewContext<Workspace>,
684 ) -> Option<Task<Result<()>>> {
685 let pane_handle = workspace.active_pane().clone();
686 let pane = pane_handle.read(cx);
687 if pane.items.is_empty() {
688 None
689 } else {
690 let active_item_id = pane.items[pane.active_item_index].id();
691 let task =
692 Self::close_items(workspace, pane_handle, cx, move |id| id != active_item_id);
693 Some(cx.foreground().spawn(async move {
694 task.await?;
695 Ok(())
696 }))
697 }
698 }
699
700 pub fn close_item(
701 workspace: &mut Workspace,
702 pane: ViewHandle<Pane>,
703 item_id_to_close: usize,
704 cx: &mut ViewContext<Workspace>,
705 ) -> Task<Result<bool>> {
706 Self::close_items(workspace, pane, cx, move |view_id| {
707 view_id == item_id_to_close
708 })
709 }
710
711 pub fn close_items(
712 workspace: &mut Workspace,
713 pane: ViewHandle<Pane>,
714 cx: &mut ViewContext<Workspace>,
715 should_close: impl 'static + Fn(usize) -> bool,
716 ) -> Task<Result<bool>> {
717 let project = workspace.project().clone();
718
719 // Find the items to close.
720 let mut items_to_close = Vec::new();
721 for item in &pane.read(cx).items {
722 if should_close(item.id()) {
723 items_to_close.push(item.boxed_clone());
724 }
725 }
726
727 // If a buffer is open both in a singleton editor and in a multibuffer, make sure
728 // to focus the singleton buffer when prompting to save that buffer, as opposed
729 // to focusing the multibuffer, because this gives the user a more clear idea
730 // of what content they would be saving.
731 items_to_close.sort_by_key(|item| !item.is_singleton(cx));
732
733 cx.spawn(|workspace, mut cx| async move {
734 let mut saved_project_entry_ids = HashSet::default();
735 for item in items_to_close.clone() {
736 // Find the item's current index and its set of project entries. Avoid
737 // storing these in advance, in case they have changed since this task
738 // was started.
739 let (item_ix, mut project_entry_ids) = pane.read_with(&cx, |pane, cx| {
740 (pane.index_for_item(&*item), item.project_entry_ids(cx))
741 });
742 let item_ix = if let Some(ix) = item_ix {
743 ix
744 } else {
745 continue;
746 };
747
748 // If an item hasn't yet been associated with a project entry, then always
749 // prompt to save it before closing it. Otherwise, check if the item has
750 // any project entries that are not open anywhere else in the workspace,
751 // AND that the user has not already been prompted to save. If there are
752 // any such project entries, prompt the user to save this item.
753 let should_save = if project_entry_ids.is_empty() {
754 true
755 } else {
756 workspace.read_with(&cx, |workspace, cx| {
757 for item in workspace.items(cx) {
758 if !items_to_close
759 .iter()
760 .any(|item_to_close| item_to_close.id() == item.id())
761 {
762 let other_project_entry_ids = item.project_entry_ids(cx);
763 project_entry_ids
764 .retain(|id| !other_project_entry_ids.contains(id));
765 }
766 }
767 });
768 project_entry_ids
769 .iter()
770 .any(|id| saved_project_entry_ids.insert(*id))
771 };
772
773 if should_save
774 && !Self::save_item(project.clone(), &pane, item_ix, &*item, true, &mut cx)
775 .await?
776 {
777 break;
778 }
779
780 // Remove the item from the pane.
781 pane.update(&mut cx, |pane, cx| {
782 if let Some(item_ix) = pane.items.iter().position(|i| i.id() == item.id()) {
783 pane.remove_item(item_ix, cx);
784 }
785 });
786 }
787
788 pane.update(&mut cx, |_, cx| cx.notify());
789 Ok(true)
790 })
791 }
792
793 fn remove_item(&mut self, item_ix: usize, cx: &mut ViewContext<Self>) {
794 if item_ix == self.active_item_index {
795 // Activate the previous item if possible.
796 // This returns the user to the previously opened tab if they closed
797 // a new item they just navigated to.
798 if item_ix > 0 {
799 self.activate_prev_item(cx);
800 } else if item_ix + 1 < self.items.len() {
801 self.activate_next_item(cx);
802 }
803 }
804
805 let item = self.items.remove(item_ix);
806 cx.emit(Event::RemoveItem { item_id: item.id() });
807 if self.items.is_empty() {
808 item.deactivated(cx);
809 self.update_toolbar(cx);
810 cx.emit(Event::Remove);
811 }
812
813 if item_ix < self.active_item_index {
814 self.active_item_index -= 1;
815 }
816
817 self.nav_history
818 .borrow_mut()
819 .set_mode(NavigationMode::ClosingItem);
820 item.deactivated(cx);
821 self.nav_history
822 .borrow_mut()
823 .set_mode(NavigationMode::Normal);
824
825 if let Some(path) = item.project_path(cx) {
826 self.nav_history
827 .borrow_mut()
828 .paths_by_item
829 .insert(item.id(), path);
830 } else {
831 self.nav_history
832 .borrow_mut()
833 .paths_by_item
834 .remove(&item.id());
835 }
836
837 cx.notify();
838 }
839
840 pub async fn save_item(
841 project: ModelHandle<Project>,
842 pane: &ViewHandle<Pane>,
843 item_ix: usize,
844 item: &dyn ItemHandle,
845 should_prompt_for_save: bool,
846 cx: &mut AsyncAppContext,
847 ) -> Result<bool> {
848 const CONFLICT_MESSAGE: &str =
849 "This file has changed on disk since you started editing it. Do you want to overwrite it?";
850 const DIRTY_MESSAGE: &str = "This file contains unsaved edits. Do you want to save it?";
851
852 let (has_conflict, is_dirty, can_save, is_singleton) = cx.read(|cx| {
853 (
854 item.has_conflict(cx),
855 item.is_dirty(cx),
856 item.can_save(cx),
857 item.is_singleton(cx),
858 )
859 });
860
861 if has_conflict && can_save {
862 let mut answer = pane.update(cx, |pane, cx| {
863 pane.activate_item(item_ix, true, true, cx);
864 cx.prompt(
865 PromptLevel::Warning,
866 CONFLICT_MESSAGE,
867 &["Overwrite", "Discard", "Cancel"],
868 )
869 });
870 match answer.next().await {
871 Some(0) => cx.update(|cx| item.save(project, cx)).await?,
872 Some(1) => cx.update(|cx| item.reload(project, cx)).await?,
873 _ => return Ok(false),
874 }
875 } else if is_dirty && (can_save || is_singleton) {
876 let will_autosave = cx.read(|cx| {
877 matches!(
878 cx.global::<Settings>().autosave,
879 Autosave::OnFocusChange | Autosave::OnWindowChange
880 ) && Self::can_autosave_item(&*item, cx)
881 });
882 let should_save = if should_prompt_for_save && !will_autosave {
883 let mut answer = pane.update(cx, |pane, cx| {
884 pane.activate_item(item_ix, true, true, cx);
885 cx.prompt(
886 PromptLevel::Warning,
887 DIRTY_MESSAGE,
888 &["Save", "Don't Save", "Cancel"],
889 )
890 });
891 match answer.next().await {
892 Some(0) => true,
893 Some(1) => false,
894 _ => return Ok(false),
895 }
896 } else {
897 true
898 };
899
900 if should_save {
901 if can_save {
902 cx.update(|cx| item.save(project, cx)).await?;
903 } else if is_singleton {
904 let start_abs_path = project
905 .read_with(cx, |project, cx| {
906 let worktree = project.visible_worktrees(cx).next()?;
907 Some(worktree.read(cx).as_local()?.abs_path().to_path_buf())
908 })
909 .unwrap_or_else(|| Path::new("").into());
910
911 let mut abs_path = cx.update(|cx| cx.prompt_for_new_path(&start_abs_path));
912 if let Some(abs_path) = abs_path.next().await.flatten() {
913 cx.update(|cx| item.save_as(project, abs_path, cx)).await?;
914 } else {
915 return Ok(false);
916 }
917 }
918 }
919 }
920 Ok(true)
921 }
922
923 fn can_autosave_item(item: &dyn ItemHandle, cx: &AppContext) -> bool {
924 let is_deleted = item.project_entry_ids(cx).is_empty();
925 item.is_dirty(cx) && !item.has_conflict(cx) && item.can_save(cx) && !is_deleted
926 }
927
928 pub fn autosave_item(
929 item: &dyn ItemHandle,
930 project: ModelHandle<Project>,
931 cx: &mut MutableAppContext,
932 ) -> Task<Result<()>> {
933 if Self::can_autosave_item(item, cx) {
934 item.save(project, cx)
935 } else {
936 Task::ready(Ok(()))
937 }
938 }
939
940 pub fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
941 if let Some(active_item) = self.active_item() {
942 cx.focus(active_item);
943 }
944 }
945
946 fn move_item(
947 workspace: &mut Workspace,
948 from: ViewHandle<Pane>,
949 to: ViewHandle<Pane>,
950 item_to_move: usize,
951 destination_index: usize,
952 cx: &mut ViewContext<Workspace>,
953 ) {
954 let item_to_move = from
955 .read(cx)
956 .items()
957 .enumerate()
958 .find(|(_, item_handle)| item_handle.id() == item_to_move);
959
960 if item_to_move.is_none() {
961 log::warn!("Tried to move item handle which was not in `from` pane. Maybe tab was closed during drop");
962 return;
963 }
964
965 let (item_ix, item_handle) = item_to_move.unwrap();
966 // This automatically removes duplicate items in the pane
967 Pane::add_item(
968 workspace,
969 &to,
970 item_handle.clone(),
971 true,
972 true,
973 Some(destination_index),
974 cx,
975 );
976
977 if from != to {
978 // Close item from previous pane
979 from.update(cx, |from, cx| {
980 from.remove_item(item_ix, cx);
981 });
982 }
983
984 cx.focus(to);
985 }
986
987 pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext<Self>) {
988 cx.emit(Event::Split(direction));
989 }
990
991 fn deploy_split_menu(&mut self, action: &DeploySplitMenu, cx: &mut ViewContext<Self>) {
992 self.tab_bar_context_menu.update(cx, |menu, cx| {
993 menu.show(
994 action.position,
995 AnchorCorner::TopRight,
996 vec![
997 ContextMenuItem::item("Split Right", SplitRight),
998 ContextMenuItem::item("Split Left", SplitLeft),
999 ContextMenuItem::item("Split Up", SplitUp),
1000 ContextMenuItem::item("Split Down", SplitDown),
1001 ],
1002 cx,
1003 );
1004 });
1005 }
1006
1007 fn deploy_dock_menu(&mut self, action: &DeployDockMenu, cx: &mut ViewContext<Self>) {
1008 self.tab_bar_context_menu.update(cx, |menu, cx| {
1009 menu.show(
1010 action.position,
1011 AnchorCorner::TopRight,
1012 vec![
1013 ContextMenuItem::item("Move Dock Right", MoveDock(DockAnchor::Right)),
1014 ContextMenuItem::item("Move Dock Bottom", MoveDock(DockAnchor::Bottom)),
1015 ContextMenuItem::item("Move Dock Maximized", MoveDock(DockAnchor::Expanded)),
1016 ],
1017 cx,
1018 );
1019 });
1020 }
1021
1022 fn deploy_new_menu(&mut self, action: &DeployNewMenu, cx: &mut ViewContext<Self>) {
1023 self.tab_bar_context_menu.update(cx, |menu, cx| {
1024 menu.show(
1025 action.position,
1026 AnchorCorner::TopRight,
1027 vec![
1028 ContextMenuItem::item("New File", NewFile),
1029 ContextMenuItem::item("New Terminal", NewTerminal),
1030 ContextMenuItem::item("New Search", NewSearch),
1031 ],
1032 cx,
1033 );
1034 });
1035 }
1036
1037 pub fn toolbar(&self) -> &ViewHandle<Toolbar> {
1038 &self.toolbar
1039 }
1040
1041 fn update_toolbar(&mut self, cx: &mut ViewContext<Self>) {
1042 let active_item = self
1043 .items
1044 .get(self.active_item_index)
1045 .map(|item| item.as_ref());
1046 self.toolbar.update(cx, |toolbar, cx| {
1047 toolbar.set_active_pane_item(active_item, cx);
1048 });
1049 }
1050
1051 fn render_tabs(&mut self, cx: &mut RenderContext<Self>) -> impl Element {
1052 let theme = cx.global::<Settings>().theme.clone();
1053 let filler_index = self.items.len();
1054
1055 enum Tabs {}
1056 enum Tab {}
1057 enum Filler {}
1058 let pane = cx.handle();
1059 MouseEventHandler::<Tabs>::new(0, cx, |_, cx| {
1060 let autoscroll = if mem::take(&mut self.autoscroll) {
1061 Some(self.active_item_index)
1062 } else {
1063 None
1064 };
1065
1066 let pane_active = self.is_active;
1067
1068 let mut row = Flex::row().scrollable::<Tabs, _>(1, autoscroll, cx);
1069 for (ix, (item, detail)) in self
1070 .items
1071 .iter()
1072 .cloned()
1073 .zip(self.tab_details(cx))
1074 .enumerate()
1075 {
1076 let detail = if detail == 0 { None } else { Some(detail) };
1077 let tab_active = ix == self.active_item_index;
1078
1079 row.add_child({
1080 MouseEventHandler::<Tab>::new(ix, cx, {
1081 let item = item.clone();
1082 let pane = pane.clone();
1083 let detail = detail.clone();
1084
1085 let theme = cx.global::<Settings>().theme.clone();
1086
1087 move |mouse_state, cx| {
1088 let tab_style =
1089 theme.workspace.tab_bar.tab_style(pane_active, tab_active);
1090 let hovered = mouse_state.hovered;
1091 Self::render_tab(
1092 &item,
1093 pane,
1094 detail,
1095 hovered,
1096 Self::tab_overlay_color(hovered, theme.as_ref(), cx),
1097 tab_style,
1098 cx,
1099 )
1100 }
1101 })
1102 .with_cursor_style(if pane_active && tab_active {
1103 CursorStyle::Arrow
1104 } else {
1105 CursorStyle::PointingHand
1106 })
1107 .on_down(MouseButton::Left, move |_, cx| {
1108 cx.dispatch_action(ActivateItem(ix));
1109 })
1110 .on_click(MouseButton::Middle, {
1111 let item = item.clone();
1112 let pane = pane.clone();
1113 move |_, cx: &mut EventContext| {
1114 cx.dispatch_action(CloseItem {
1115 item_id: item.id(),
1116 pane: pane.clone(),
1117 })
1118 }
1119 })
1120 .on_up(MouseButton::Left, {
1121 let pane = pane.clone();
1122 move |_, cx: &mut EventContext| Pane::handle_dropped_item(&pane, ix, cx)
1123 })
1124 .as_draggable(
1125 DraggedItem {
1126 item,
1127 pane: pane.clone(),
1128 },
1129 {
1130 let theme = cx.global::<Settings>().theme.clone();
1131
1132 let detail = detail.clone();
1133 move |dragged_item, cx: &mut RenderContext<Workspace>| {
1134 let tab_style = &theme.workspace.tab_bar.dragged_tab;
1135 Self::render_tab(
1136 &dragged_item.item,
1137 dragged_item.pane.clone(),
1138 detail,
1139 false,
1140 None,
1141 &tab_style,
1142 cx,
1143 )
1144 }
1145 },
1146 )
1147 .boxed()
1148 })
1149 }
1150
1151 // Use the inactive tab style along with the current pane's active status to decide how to render
1152 // the filler
1153 let filler_style = theme.workspace.tab_bar.tab_style(pane_active, false);
1154 row.add_child(
1155 MouseEventHandler::<Filler>::new(0, cx, |mouse_state, cx| {
1156 let mut filler = Empty::new()
1157 .contained()
1158 .with_style(filler_style.container)
1159 .with_border(filler_style.container.border);
1160
1161 if let Some(overlay) = Self::tab_overlay_color(mouse_state.hovered, &theme, cx)
1162 {
1163 filler = filler.with_overlay_color(overlay);
1164 }
1165
1166 filler.boxed()
1167 })
1168 .flex(1., true)
1169 .named("filler"),
1170 );
1171
1172 row.boxed()
1173 })
1174 .on_up(MouseButton::Left, move |_, cx| {
1175 Pane::handle_dropped_item(&pane, filler_index, cx)
1176 })
1177 }
1178
1179 fn tab_details(&self, cx: &AppContext) -> Vec<usize> {
1180 let mut tab_details = (0..self.items.len()).map(|_| 0).collect::<Vec<_>>();
1181
1182 let mut tab_descriptions = HashMap::default();
1183 let mut done = false;
1184 while !done {
1185 done = true;
1186
1187 // Store item indices by their tab description.
1188 for (ix, (item, detail)) in self.items.iter().zip(&tab_details).enumerate() {
1189 if let Some(description) = item.tab_description(*detail, cx) {
1190 if *detail == 0
1191 || Some(&description) != item.tab_description(detail - 1, cx).as_ref()
1192 {
1193 tab_descriptions
1194 .entry(description)
1195 .or_insert(Vec::new())
1196 .push(ix);
1197 }
1198 }
1199 }
1200
1201 // If two or more items have the same tab description, increase their level
1202 // of detail and try again.
1203 for (_, item_ixs) in tab_descriptions.drain() {
1204 if item_ixs.len() > 1 {
1205 done = false;
1206 for ix in item_ixs {
1207 tab_details[ix] += 1;
1208 }
1209 }
1210 }
1211 }
1212
1213 tab_details
1214 }
1215
1216 fn render_tab<V: View>(
1217 item: &Box<dyn ItemHandle>,
1218 pane: WeakViewHandle<Pane>,
1219 detail: Option<usize>,
1220 hovered: bool,
1221 overlay: Option<Color>,
1222 tab_style: &theme::Tab,
1223 cx: &mut RenderContext<V>,
1224 ) -> ElementBox {
1225 let title = item.tab_content(detail, &tab_style, cx);
1226
1227 let mut tab = Flex::row()
1228 .with_child(
1229 Align::new({
1230 let diameter = 7.0;
1231 let icon_color = if item.has_conflict(cx) {
1232 Some(tab_style.icon_conflict)
1233 } else if item.is_dirty(cx) {
1234 Some(tab_style.icon_dirty)
1235 } else {
1236 None
1237 };
1238
1239 ConstrainedBox::new(
1240 Canvas::new(move |bounds, _, cx| {
1241 if let Some(color) = icon_color {
1242 let square = RectF::new(bounds.origin(), vec2f(diameter, diameter));
1243 cx.scene.push_quad(Quad {
1244 bounds: square,
1245 background: Some(color),
1246 border: Default::default(),
1247 corner_radius: diameter / 2.,
1248 });
1249 }
1250 })
1251 .boxed(),
1252 )
1253 .with_width(diameter)
1254 .with_height(diameter)
1255 .boxed()
1256 })
1257 .boxed(),
1258 )
1259 .with_child(
1260 Container::new(Align::new(title).boxed())
1261 .with_style(ContainerStyle {
1262 margin: Margin {
1263 left: tab_style.spacing,
1264 right: tab_style.spacing,
1265 ..Default::default()
1266 },
1267 ..Default::default()
1268 })
1269 .boxed(),
1270 )
1271 .with_child(
1272 Align::new(
1273 ConstrainedBox::new(if hovered {
1274 let item_id = item.id();
1275 enum TabCloseButton {}
1276 let icon = Svg::new("icons/x_mark_thin_8.svg");
1277 MouseEventHandler::<TabCloseButton>::new(item_id, cx, |mouse_state, _| {
1278 if mouse_state.hovered {
1279 icon.with_color(tab_style.icon_close_active).boxed()
1280 } else {
1281 icon.with_color(tab_style.icon_close).boxed()
1282 }
1283 })
1284 .with_padding(Padding::uniform(4.))
1285 .with_cursor_style(CursorStyle::PointingHand)
1286 .on_click(MouseButton::Left, {
1287 let pane = pane.clone();
1288 move |_, cx| {
1289 cx.dispatch_action(CloseItem {
1290 item_id,
1291 pane: pane.clone(),
1292 })
1293 }
1294 })
1295 .on_click(MouseButton::Middle, |_, cx| cx.propogate_event())
1296 .named("close-tab-icon")
1297 } else {
1298 Empty::new().boxed()
1299 })
1300 .with_width(tab_style.icon_width)
1301 .boxed(),
1302 )
1303 .boxed(),
1304 )
1305 .contained()
1306 .with_style(tab_style.container);
1307
1308 if let Some(overlay) = overlay {
1309 tab = tab.with_overlay_color(overlay);
1310 }
1311
1312 tab.constrained().with_height(tab_style.height).boxed()
1313 }
1314
1315 fn handle_dropped_item(pane: &WeakViewHandle<Pane>, index: usize, cx: &mut EventContext) {
1316 if let Some((_, dragged_item)) = cx
1317 .global::<DragAndDrop<Workspace>>()
1318 .currently_dragged::<DraggedItem>(cx.window_id)
1319 {
1320 cx.dispatch_action(MoveItem {
1321 item_id: dragged_item.item.id(),
1322 from: dragged_item.pane.clone(),
1323 to: pane.clone(),
1324 destination_index: index,
1325 })
1326 } else {
1327 cx.propogate_event();
1328 }
1329 }
1330
1331 fn tab_overlay_color(
1332 hovered: bool,
1333 theme: &Theme,
1334 cx: &mut RenderContext<Self>,
1335 ) -> Option<Color> {
1336 if hovered
1337 && cx
1338 .global::<DragAndDrop<Workspace>>()
1339 .currently_dragged::<DraggedItem>(cx.window_id())
1340 .is_some()
1341 {
1342 Some(theme.workspace.tab_bar.drop_target_overlay_color)
1343 } else {
1344 None
1345 }
1346 }
1347}
1348
1349impl Entity for Pane {
1350 type Event = Event;
1351}
1352
1353impl View for Pane {
1354 fn ui_name() -> &'static str {
1355 "Pane"
1356 }
1357
1358 fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
1359 let this = cx.handle();
1360
1361 enum MouseNavigationHandler {}
1362
1363 Stack::new()
1364 .with_child(
1365 MouseEventHandler::<MouseNavigationHandler>::new(0, cx, |_, cx| {
1366 if let Some(active_item) = self.active_item() {
1367 Flex::column()
1368 .with_child({
1369 let mut tab_row = Flex::row()
1370 .with_child(self.render_tabs(cx).flex(1., true).named("tabs"));
1371
1372 // Render pane buttons
1373 let theme = cx.global::<Settings>().theme.clone();
1374 if self.is_active {
1375 tab_row.add_child(
1376 Flex::row()
1377 // New menu
1378 .with_child(tab_bar_button(
1379 0,
1380 "icons/plus_12.svg",
1381 cx,
1382 |position| DeployNewMenu { position },
1383 ))
1384 .with_child(
1385 self.docked
1386 .map(|anchor| {
1387 // Add the dock menu button if this pane is a dock
1388 let dock_icon =
1389 icon_for_dock_anchor(anchor);
1390
1391 tab_bar_button(
1392 2,
1393 dock_icon,
1394 cx,
1395 |position| DeployDockMenu { position },
1396 )
1397 })
1398 .unwrap_or_else(|| {
1399 // Add the split menu if this pane is not a dock
1400 tab_bar_button(
1401 1,
1402 "icons/split_12.svg",
1403 cx,
1404 |position| DeployNewMenu { position },
1405 )
1406 }),
1407 )
1408 // Add the close dock button if this pane is a dock
1409 .with_children(self.docked.map(|_| {
1410 tab_bar_button(3, "icons/x_mark_12.svg", cx, |_| {
1411 ToggleDock
1412 })
1413 }))
1414 .contained()
1415 .with_style(theme.workspace.tab_bar.container)
1416 .boxed(),
1417 )
1418 }
1419
1420 tab_row
1421 .constrained()
1422 .with_height(theme.workspace.tab_bar.height)
1423 .contained()
1424 .with_style(theme.workspace.tab_bar.container)
1425 .named("tab bar")
1426 })
1427 .with_child(ChildView::new(&self.toolbar).boxed())
1428 .with_child(ChildView::new(active_item).flex(1., true).boxed())
1429 .boxed()
1430 } else {
1431 enum EmptyPane {}
1432 let theme = cx.global::<Settings>().theme.clone();
1433
1434 MouseEventHandler::<EmptyPane>::new(0, cx, |_, _| {
1435 Empty::new()
1436 .contained()
1437 .with_background_color(theme.workspace.background)
1438 .boxed()
1439 })
1440 .on_down(MouseButton::Left, |_, cx| {
1441 cx.focus_parent_view();
1442 })
1443 .on_up(MouseButton::Left, {
1444 let pane = this.clone();
1445 move |_, cx: &mut EventContext| Pane::handle_dropped_item(&pane, 0, cx)
1446 })
1447 .boxed()
1448 }
1449 })
1450 .on_down(MouseButton::Navigate(NavigationDirection::Back), {
1451 let this = this.clone();
1452 move |_, cx| {
1453 cx.dispatch_action(GoBack {
1454 pane: Some(this.clone()),
1455 });
1456 }
1457 })
1458 .on_down(MouseButton::Navigate(NavigationDirection::Forward), {
1459 let this = this.clone();
1460 move |_, cx| {
1461 cx.dispatch_action(GoForward {
1462 pane: Some(this.clone()),
1463 })
1464 }
1465 })
1466 .boxed(),
1467 )
1468 .with_child(ChildView::new(&self.tab_bar_context_menu).boxed())
1469 .named("pane")
1470 }
1471
1472 fn on_focus_in(&mut self, focused: AnyViewHandle, cx: &mut ViewContext<Self>) {
1473 if cx.is_self_focused() {
1474 if let Some(last_focused_view) = self
1475 .last_focused_view
1476 .as_ref()
1477 .and_then(|handle| handle.upgrade(cx))
1478 {
1479 cx.focus(last_focused_view);
1480 } else {
1481 self.focus_active_item(cx);
1482 }
1483 } else {
1484 self.last_focused_view = Some(focused.downgrade());
1485 }
1486 cx.emit(Event::Focused);
1487 }
1488}
1489
1490fn tab_bar_button<A: Action>(
1491 index: usize,
1492 icon: &'static str,
1493 cx: &mut RenderContext<Pane>,
1494 action_builder: impl 'static + Fn(Vector2F) -> A,
1495) -> ElementBox {
1496 enum TabBarButton {}
1497
1498 MouseEventHandler::<TabBarButton>::new(index, cx, |mouse_state, cx| {
1499 let theme = &cx.global::<Settings>().theme.workspace.tab_bar;
1500 let style = theme.pane_button.style_for(mouse_state, false);
1501 Svg::new(icon)
1502 .with_color(style.color)
1503 .constrained()
1504 .with_width(style.icon_width)
1505 .aligned()
1506 .constrained()
1507 .with_width(style.button_width)
1508 .with_height(style.button_width)
1509 .aligned()
1510 .boxed()
1511 })
1512 .with_cursor_style(CursorStyle::PointingHand)
1513 .on_click(MouseButton::Left, move |e, cx| {
1514 cx.dispatch_action(action_builder(e.region.lower_right()));
1515 })
1516 .boxed()
1517}
1518
1519impl ItemNavHistory {
1520 pub fn push<D: 'static + Any>(&self, data: Option<D>, cx: &mut MutableAppContext) {
1521 self.history.borrow_mut().push(data, self.item.clone(), cx);
1522 }
1523
1524 pub fn pop_backward(&self, cx: &mut MutableAppContext) -> Option<NavigationEntry> {
1525 self.history.borrow_mut().pop(NavigationMode::GoingBack, cx)
1526 }
1527
1528 pub fn pop_forward(&self, cx: &mut MutableAppContext) -> Option<NavigationEntry> {
1529 self.history
1530 .borrow_mut()
1531 .pop(NavigationMode::GoingForward, cx)
1532 }
1533}
1534
1535impl NavHistory {
1536 fn set_mode(&mut self, mode: NavigationMode) {
1537 self.mode = mode;
1538 }
1539
1540 fn disable(&mut self) {
1541 self.mode = NavigationMode::Disabled;
1542 }
1543
1544 fn enable(&mut self) {
1545 self.mode = NavigationMode::Normal;
1546 }
1547
1548 fn pop(&mut self, mode: NavigationMode, cx: &mut MutableAppContext) -> Option<NavigationEntry> {
1549 let entry = match mode {
1550 NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
1551 return None
1552 }
1553 NavigationMode::GoingBack => &mut self.backward_stack,
1554 NavigationMode::GoingForward => &mut self.forward_stack,
1555 NavigationMode::ReopeningClosedItem => &mut self.closed_stack,
1556 }
1557 .pop_back();
1558 if entry.is_some() {
1559 self.did_update(cx);
1560 }
1561 entry
1562 }
1563
1564 fn push<D: 'static + Any>(
1565 &mut self,
1566 data: Option<D>,
1567 item: Rc<dyn WeakItemHandle>,
1568 cx: &mut MutableAppContext,
1569 ) {
1570 match self.mode {
1571 NavigationMode::Disabled => {}
1572 NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
1573 if self.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
1574 self.backward_stack.pop_front();
1575 }
1576 self.backward_stack.push_back(NavigationEntry {
1577 item,
1578 data: data.map(|data| Box::new(data) as Box<dyn Any>),
1579 });
1580 self.forward_stack.clear();
1581 }
1582 NavigationMode::GoingBack => {
1583 if self.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
1584 self.forward_stack.pop_front();
1585 }
1586 self.forward_stack.push_back(NavigationEntry {
1587 item,
1588 data: data.map(|data| Box::new(data) as Box<dyn Any>),
1589 });
1590 }
1591 NavigationMode::GoingForward => {
1592 if self.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
1593 self.backward_stack.pop_front();
1594 }
1595 self.backward_stack.push_back(NavigationEntry {
1596 item,
1597 data: data.map(|data| Box::new(data) as Box<dyn Any>),
1598 });
1599 }
1600 NavigationMode::ClosingItem => {
1601 if self.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
1602 self.closed_stack.pop_front();
1603 }
1604 self.closed_stack.push_back(NavigationEntry {
1605 item,
1606 data: data.map(|data| Box::new(data) as Box<dyn Any>),
1607 });
1608 }
1609 }
1610 self.did_update(cx);
1611 }
1612
1613 fn did_update(&self, cx: &mut MutableAppContext) {
1614 if let Some(pane) = self.pane.upgrade(cx) {
1615 cx.defer(move |cx| pane.update(cx, |pane, cx| pane.history_updated(cx)));
1616 }
1617 }
1618}
1619
1620#[cfg(test)]
1621mod tests {
1622 use super::*;
1623 use crate::tests::TestItem;
1624 use gpui::TestAppContext;
1625 use project::FakeFs;
1626
1627 #[gpui::test]
1628 async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
1629 cx.foreground().forbid_parking();
1630 Settings::test_async(cx);
1631 let fs = FakeFs::new(cx.background());
1632
1633 let project = Project::test(fs, None, cx).await;
1634 let (_, workspace) =
1635 cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
1636 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
1637
1638 // 1. Add with a destination index
1639 // a. Add before the active item
1640 set_labeled_items(&workspace, &pane, ["A", "B*", "C"], cx);
1641 workspace.update(cx, |workspace, cx| {
1642 Pane::add_item(
1643 workspace,
1644 &pane,
1645 Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
1646 false,
1647 false,
1648 Some(0),
1649 cx,
1650 );
1651 });
1652 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
1653
1654 // b. Add after the active item
1655 set_labeled_items(&workspace, &pane, ["A", "B*", "C"], cx);
1656 workspace.update(cx, |workspace, cx| {
1657 Pane::add_item(
1658 workspace,
1659 &pane,
1660 Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
1661 false,
1662 false,
1663 Some(2),
1664 cx,
1665 );
1666 });
1667 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
1668
1669 // c. Add at the end of the item list (including off the length)
1670 set_labeled_items(&workspace, &pane, ["A", "B*", "C"], cx);
1671 workspace.update(cx, |workspace, cx| {
1672 Pane::add_item(
1673 workspace,
1674 &pane,
1675 Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
1676 false,
1677 false,
1678 Some(5),
1679 cx,
1680 );
1681 });
1682 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
1683
1684 // 2. Add without a destination index
1685 // a. Add with active item at the start of the item list
1686 set_labeled_items(&workspace, &pane, ["A*", "B", "C"], cx);
1687 workspace.update(cx, |workspace, cx| {
1688 Pane::add_item(
1689 workspace,
1690 &pane,
1691 Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
1692 false,
1693 false,
1694 None,
1695 cx,
1696 );
1697 });
1698 set_labeled_items(&workspace, &pane, ["A", "D*", "B", "C"], cx);
1699
1700 // b. Add with active item at the end of the item list
1701 set_labeled_items(&workspace, &pane, ["A", "B", "C*"], cx);
1702 workspace.update(cx, |workspace, cx| {
1703 Pane::add_item(
1704 workspace,
1705 &pane,
1706 Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
1707 false,
1708 false,
1709 None,
1710 cx,
1711 );
1712 });
1713 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
1714 }
1715
1716 #[gpui::test]
1717 async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
1718 cx.foreground().forbid_parking();
1719 Settings::test_async(cx);
1720 let fs = FakeFs::new(cx.background());
1721
1722 let project = Project::test(fs, None, cx).await;
1723 let (_, workspace) =
1724 cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
1725 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
1726
1727 // 1. Add with a destination index
1728 // 1a. Add before the active item
1729 let [_, _, _, d] = set_labeled_items(&workspace, &pane, ["A", "B*", "C", "D"], cx);
1730 workspace.update(cx, |workspace, cx| {
1731 Pane::add_item(workspace, &pane, d, false, false, Some(0), cx);
1732 });
1733 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
1734
1735 // 1b. Add after the active item
1736 let [_, _, _, d] = set_labeled_items(&workspace, &pane, ["A", "B*", "C", "D"], cx);
1737 workspace.update(cx, |workspace, cx| {
1738 Pane::add_item(workspace, &pane, d, false, false, Some(2), cx);
1739 });
1740 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
1741
1742 // 1c. Add at the end of the item list (including off the length)
1743 let [a, _, _, _] = set_labeled_items(&workspace, &pane, ["A", "B*", "C", "D"], cx);
1744 workspace.update(cx, |workspace, cx| {
1745 Pane::add_item(workspace, &pane, a, false, false, Some(5), cx);
1746 });
1747 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
1748
1749 // 1d. Add same item to active index
1750 let [_, b, _] = set_labeled_items(&workspace, &pane, ["A", "B*", "C"], cx);
1751 workspace.update(cx, |workspace, cx| {
1752 Pane::add_item(workspace, &pane, b, false, false, Some(1), cx);
1753 });
1754 assert_item_labels(&pane, ["A", "B*", "C"], cx);
1755
1756 // 1e. Add item to index after same item in last position
1757 let [_, _, c] = set_labeled_items(&workspace, &pane, ["A", "B*", "C"], cx);
1758 workspace.update(cx, |workspace, cx| {
1759 Pane::add_item(workspace, &pane, c, false, false, Some(2), cx);
1760 });
1761 assert_item_labels(&pane, ["A", "B", "C*"], cx);
1762
1763 // 2. Add without a destination index
1764 // 2a. Add with active item at the start of the item list
1765 let [_, _, _, d] = set_labeled_items(&workspace, &pane, ["A*", "B", "C", "D"], cx);
1766 workspace.update(cx, |workspace, cx| {
1767 Pane::add_item(workspace, &pane, d, false, false, None, cx);
1768 });
1769 assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
1770
1771 // 2b. Add with active item at the end of the item list
1772 let [a, _, _, _] = set_labeled_items(&workspace, &pane, ["A", "B", "C", "D*"], cx);
1773 workspace.update(cx, |workspace, cx| {
1774 Pane::add_item(workspace, &pane, a, false, false, None, cx);
1775 });
1776 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
1777
1778 // 2c. Add active item to active item at end of list
1779 let [_, _, c] = set_labeled_items(&workspace, &pane, ["A", "B", "C*"], cx);
1780 workspace.update(cx, |workspace, cx| {
1781 Pane::add_item(workspace, &pane, c, false, false, None, cx);
1782 });
1783 assert_item_labels(&pane, ["A", "B", "C*"], cx);
1784
1785 // 2d. Add active item to active item at start of list
1786 let [a, _, _] = set_labeled_items(&workspace, &pane, ["A*", "B", "C"], cx);
1787 workspace.update(cx, |workspace, cx| {
1788 Pane::add_item(workspace, &pane, a, false, false, None, cx);
1789 });
1790 assert_item_labels(&pane, ["A*", "B", "C"], cx);
1791 }
1792
1793 #[gpui::test]
1794 async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
1795 cx.foreground().forbid_parking();
1796 Settings::test_async(cx);
1797 let fs = FakeFs::new(cx.background());
1798
1799 let project = Project::test(fs, None, cx).await;
1800 let (_, workspace) =
1801 cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
1802 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
1803
1804 // singleton view
1805 workspace.update(cx, |workspace, cx| {
1806 let item = TestItem::new()
1807 .with_singleton(true)
1808 .with_label("buffer 1")
1809 .with_project_entry_ids(&[1]);
1810
1811 Pane::add_item(
1812 workspace,
1813 &pane,
1814 Box::new(cx.add_view(|_| item)),
1815 false,
1816 false,
1817 None,
1818 cx,
1819 );
1820 });
1821 assert_item_labels(&pane, ["buffer 1*"], cx);
1822
1823 // new singleton view with the same project entry
1824 workspace.update(cx, |workspace, cx| {
1825 let item = TestItem::new()
1826 .with_singleton(true)
1827 .with_label("buffer 1")
1828 .with_project_entry_ids(&[1]);
1829
1830 Pane::add_item(
1831 workspace,
1832 &pane,
1833 Box::new(cx.add_view(|_| item)),
1834 false,
1835 false,
1836 None,
1837 cx,
1838 );
1839 });
1840 assert_item_labels(&pane, ["buffer 1*"], cx);
1841
1842 // new singleton view with different project entry
1843 workspace.update(cx, |workspace, cx| {
1844 let item = TestItem::new()
1845 .with_singleton(true)
1846 .with_label("buffer 2")
1847 .with_project_entry_ids(&[2]);
1848
1849 Pane::add_item(
1850 workspace,
1851 &pane,
1852 Box::new(cx.add_view(|_| item)),
1853 false,
1854 false,
1855 None,
1856 cx,
1857 );
1858 });
1859 assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
1860
1861 // new multibuffer view with the same project entry
1862 workspace.update(cx, |workspace, cx| {
1863 let item = TestItem::new()
1864 .with_singleton(false)
1865 .with_label("multibuffer 1")
1866 .with_project_entry_ids(&[1]);
1867
1868 Pane::add_item(
1869 workspace,
1870 &pane,
1871 Box::new(cx.add_view(|_| item)),
1872 false,
1873 false,
1874 None,
1875 cx,
1876 );
1877 });
1878 assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
1879
1880 // another multibuffer view with the same project entry
1881 workspace.update(cx, |workspace, cx| {
1882 let item = TestItem::new()
1883 .with_singleton(false)
1884 .with_label("multibuffer 1b")
1885 .with_project_entry_ids(&[1]);
1886
1887 Pane::add_item(
1888 workspace,
1889 &pane,
1890 Box::new(cx.add_view(|_| item)),
1891 false,
1892 false,
1893 None,
1894 cx,
1895 );
1896 });
1897 assert_item_labels(
1898 &pane,
1899 ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
1900 cx,
1901 );
1902 }
1903
1904 fn set_labeled_items<const COUNT: usize>(
1905 workspace: &ViewHandle<Workspace>,
1906 pane: &ViewHandle<Pane>,
1907 labels: [&str; COUNT],
1908 cx: &mut TestAppContext,
1909 ) -> [Box<ViewHandle<TestItem>>; COUNT] {
1910 pane.update(cx, |pane, _| {
1911 pane.items.clear();
1912 });
1913
1914 workspace.update(cx, |workspace, cx| {
1915 let mut active_item_index = 0;
1916
1917 let mut index = 0;
1918 let items = labels.map(|mut label| {
1919 if label.ends_with("*") {
1920 label = label.trim_end_matches("*");
1921 active_item_index = index;
1922 }
1923
1924 let labeled_item = Box::new(cx.add_view(|_| TestItem::new().with_label(label)));
1925 Pane::add_item(
1926 workspace,
1927 pane,
1928 labeled_item.clone(),
1929 false,
1930 false,
1931 None,
1932 cx,
1933 );
1934 index += 1;
1935 labeled_item
1936 });
1937
1938 pane.update(cx, |pane, cx| {
1939 pane.activate_item(active_item_index, false, false, cx)
1940 });
1941
1942 items
1943 })
1944 }
1945
1946 // Assert the item label, with the active item label suffixed with a '*'
1947 fn assert_item_labels<const COUNT: usize>(
1948 pane: &ViewHandle<Pane>,
1949 expected_states: [&str; COUNT],
1950 cx: &mut TestAppContext,
1951 ) {
1952 pane.read_with(cx, |pane, cx| {
1953 let actual_states = pane
1954 .items
1955 .iter()
1956 .enumerate()
1957 .map(|(ix, item)| {
1958 let mut state = item
1959 .to_any()
1960 .downcast::<TestItem>()
1961 .unwrap()
1962 .read(cx)
1963 .label
1964 .clone();
1965 if ix == pane.active_item_index {
1966 state.push('*');
1967 }
1968 state
1969 })
1970 .collect::<Vec<_>>();
1971
1972 assert_eq!(
1973 actual_states, expected_states,
1974 "pane items do not match expectation"
1975 );
1976 })
1977 }
1978}