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