Introduce a test to verify navigation

Antonio Scandurra created

Change summary

crates/editor/src/editor.rs |   4 
crates/zed/src/zed.rs       | 169 ++++++++++++++++++++++++++++++++++++---
2 files changed, 158 insertions(+), 15 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -1138,8 +1138,8 @@ impl Editor {
         self.update_selections(selections, autoscroll, cx);
     }
 
-    #[cfg(test)]
-    fn select_display_ranges<'a, T>(&mut self, ranges: T, cx: &mut ViewContext<Self>)
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn select_display_ranges<'a, T>(&mut self, ranges: T, cx: &mut ViewContext<Self>)
     where
         T: IntoIterator<Item = &'a Range<DisplayPoint>>,
     {

crates/zed/src/zed.rs 🔗

@@ -124,8 +124,8 @@ fn quit(_: &Quit, cx: &mut gpui::MutableAppContext) {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use editor::Editor;
-    use gpui::MutableAppContext;
+    use editor::{DisplayPoint, Editor};
+    use gpui::{MutableAppContext, TestAppContext, ViewHandle};
     use project::ProjectPath;
     use serde_json::json;
     use std::{
@@ -136,11 +136,11 @@ mod tests {
     use theme::DEFAULT_THEME_NAME;
     use util::test::temp_tree;
     use workspace::{
-        open_paths, pane, ItemView, ItemViewHandle, OpenNew, SplitDirection, WorkspaceHandle,
+        open_paths, pane, ItemView, ItemViewHandle, OpenNew, Pane, SplitDirection, WorkspaceHandle,
     };
 
     #[gpui::test]
-    async fn test_open_paths_action(mut cx: gpui::TestAppContext) {
+    async fn test_open_paths_action(mut cx: TestAppContext) {
         let app_state = cx.update(test_app_state);
         let dir = temp_tree(json!({
             "a": {
@@ -193,7 +193,7 @@ mod tests {
     }
 
     #[gpui::test]
-    async fn test_new_empty_workspace(mut cx: gpui::TestAppContext) {
+    async fn test_new_empty_workspace(mut cx: TestAppContext) {
         let app_state = cx.update(test_app_state);
         cx.update(|cx| {
             workspace::init(cx);
@@ -230,7 +230,7 @@ mod tests {
     }
 
     #[gpui::test]
-    async fn test_open_entry(mut cx: gpui::TestAppContext) {
+    async fn test_open_entry(mut cx: TestAppContext) {
         let app_state = cx.update(test_app_state);
         app_state
             .fs
@@ -350,7 +350,7 @@ mod tests {
     }
 
     #[gpui::test]
-    async fn test_open_paths(mut cx: gpui::TestAppContext) {
+    async fn test_open_paths(mut cx: TestAppContext) {
         let app_state = cx.update(test_app_state);
         let fs = app_state.fs.as_fake();
         fs.insert_dir("/dir1").await.unwrap();
@@ -420,7 +420,7 @@ mod tests {
     }
 
     #[gpui::test]
-    async fn test_save_conflicting_item(mut cx: gpui::TestAppContext) {
+    async fn test_save_conflicting_item(mut cx: TestAppContext) {
         let app_state = cx.update(test_app_state);
         let fs = app_state.fs.as_fake();
         fs.insert_tree("/root", json!({ "a.txt": "" })).await;
@@ -469,7 +469,7 @@ mod tests {
     }
 
     #[gpui::test]
-    async fn test_open_and_save_new_file(mut cx: gpui::TestAppContext) {
+    async fn test_open_and_save_new_file(mut cx: TestAppContext) {
         let app_state = cx.update(test_app_state);
         app_state.fs.as_fake().insert_dir("/root").await.unwrap();
         let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx));
@@ -585,9 +585,7 @@ mod tests {
     }
 
     #[gpui::test]
-    async fn test_setting_language_when_saving_as_single_file_worktree(
-        mut cx: gpui::TestAppContext,
-    ) {
+    async fn test_setting_language_when_saving_as_single_file_worktree(mut cx: TestAppContext) {
         let app_state = cx.update(test_app_state);
         app_state.fs.as_fake().insert_dir("/root").await.unwrap();
         let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx));
@@ -630,7 +628,7 @@ mod tests {
     }
 
     #[gpui::test]
-    async fn test_pane_actions(mut cx: gpui::TestAppContext) {
+    async fn test_pane_actions(mut cx: TestAppContext) {
         cx.update(|cx| pane::init(cx));
         let app_state = cx.update(test_app_state);
         app_state
@@ -693,6 +691,151 @@ mod tests {
         });
     }
 
+    #[gpui::test]
+    async fn test_navigation(mut cx: TestAppContext) {
+        let app_state = cx.update(test_app_state);
+        app_state
+            .fs
+            .as_fake()
+            .insert_tree(
+                "/root",
+                json!({
+                    "a": {
+                        "file1": "contents 1",
+                        "file2": "contents 2",
+                        "file3": "contents 3",
+                    },
+                }),
+            )
+            .await;
+        let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx));
+        let (_, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
+        workspace
+            .update(&mut cx, |workspace, cx| {
+                workspace.add_worktree(Path::new("/root"), cx)
+            })
+            .await
+            .unwrap();
+        cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
+            .await;
+        let entries = cx.read(|cx| workspace.file_project_paths(cx));
+        let file1 = entries[0].clone();
+        let file2 = entries[1].clone();
+        let file3 = entries[2].clone();
+
+        let editor1 = workspace
+            .update(&mut cx, |w, cx| w.open_path(file1.clone(), cx))
+            .await
+            .unwrap()
+            .to_any()
+            .downcast::<Editor>()
+            .unwrap();
+        editor1.update(&mut cx, |editor, cx| {
+            editor.select_display_ranges(&[DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)], cx);
+        });
+        let editor2 = workspace
+            .update(&mut cx, |w, cx| w.open_path(file2.clone(), cx))
+            .await
+            .unwrap()
+            .to_any()
+            .downcast::<Editor>()
+            .unwrap();
+        let editor3 = workspace
+            .update(&mut cx, |w, cx| w.open_path(file3.clone(), cx))
+            .await
+            .unwrap()
+            .to_any()
+            .downcast::<Editor>()
+            .unwrap();
+        editor3.update(&mut cx, |editor, cx| {
+            editor.select_display_ranges(&[DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2)], cx);
+        });
+        assert_eq!(
+            active_location(&workspace, &mut cx),
+            (file3.clone(), DisplayPoint::new(0, 2))
+        );
+
+        workspace
+            .update(&mut cx, |w, cx| Pane::go_back(w, cx))
+            .await;
+        assert_eq!(
+            active_location(&workspace, &mut cx),
+            (file2.clone(), DisplayPoint::new(0, 0))
+        );
+
+        workspace
+            .update(&mut cx, |w, cx| Pane::go_back(w, cx))
+            .await;
+        assert_eq!(
+            active_location(&workspace, &mut cx),
+            (file1.clone(), DisplayPoint::new(0, 1))
+        );
+
+        // Go back one more time and ensure we don't navigate past the first item in the history.
+        workspace
+            .update(&mut cx, |w, cx| Pane::go_back(w, cx))
+            .await;
+        assert_eq!(
+            active_location(&workspace, &mut cx),
+            (file1.clone(), DisplayPoint::new(0, 1))
+        );
+
+        workspace
+            .update(&mut cx, |w, cx| Pane::go_forward(w, cx))
+            .await;
+        assert_eq!(
+            active_location(&workspace, &mut cx),
+            (file2.clone(), DisplayPoint::new(0, 0))
+        );
+
+        // Go forward to an item that has been closed, ensuring it gets re-opened at the same
+        // location.
+        workspace.update(&mut cx, |workspace, cx| {
+            workspace
+                .active_pane()
+                .update(cx, |pane, cx| pane.close_item(editor3.id(), cx));
+            drop(editor3);
+        });
+        workspace
+            .update(&mut cx, |w, cx| Pane::go_forward(w, cx))
+            .await;
+        assert_eq!(
+            active_location(&workspace, &mut cx),
+            (file3.clone(), DisplayPoint::new(0, 2))
+        );
+
+        // Go back to an item that has been closed and removed from disk, ensuring it gets skipped.
+        workspace
+            .update(&mut cx, |workspace, cx| {
+                workspace
+                    .active_pane()
+                    .update(cx, |pane, cx| pane.close_item(editor2.id(), cx));
+                drop(editor2);
+                app_state.fs.as_fake().remove(Path::new("/root/a/file2"))
+            })
+            .await
+            .unwrap();
+        workspace
+            .update(&mut cx, |w, cx| Pane::go_back(w, cx))
+            .await;
+        assert_eq!(
+            active_location(&workspace, &mut cx),
+            (file1.clone(), DisplayPoint::new(0, 1))
+        );
+
+        fn active_location(
+            workspace: &ViewHandle<Workspace>,
+            cx: &mut TestAppContext,
+        ) -> (ProjectPath, DisplayPoint) {
+            workspace.update(cx, |workspace, cx| {
+                let item = workspace.active_item(cx).unwrap();
+                let editor = item.to_any().downcast::<Editor>().unwrap();
+                let selections = editor.update(cx, |editor, cx| editor.selected_display_ranges(cx));
+                (item.project_path(cx).unwrap(), selections[0].start)
+            })
+        }
+    }
+
     #[gpui::test]
     fn test_bundled_themes(cx: &mut MutableAppContext) {
         let app_state = test_app_state(cx);