Go to reference when there's only one (#6924)

Yangze Luo and Kirill Bulatov created

Fixes #4796

- Improved Go To Definition usability when there's a single reference ([4796](https://github.com/zed-industries/zed/issues/4796))

---------

Co-authored-by: Kirill Bulatov <kirill@zed.com>

Change summary

crates/editor/src/editor.rs       | 56 ++++++++++++++++++
crates/editor/src/editor_tests.rs | 99 +++++++++++++++++++++++++++++++++
2 files changed, 153 insertions(+), 2 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -7663,12 +7663,64 @@ impl Editor {
         let workspace = self.workspace()?;
         let project = workspace.read(cx).project().clone();
         let references = project.update(cx, |project, cx| project.references(&buffer, head, cx));
-        Some(cx.spawn(|_, mut cx| async move {
-            let locations = references.await?;
+        Some(cx.spawn(|editor, mut cx| async move {
+            let mut locations = references.await?;
+            let snapshot = buffer.update(&mut cx, |buffer, _| buffer.snapshot())?;
+            let head_offset = text::ToOffset::to_offset(&head, &snapshot);
+
+            // LSP may return references that contain the item itself we requested `find_all_references` for (eg. rust-analyzer)
+            // So we will remove it from locations
+            // If there is only one reference, we will not do this filter cause it may make locations empty
+            if locations.len() > 1 {
+                cx.update(|cx| {
+                    locations.retain(|location| {
+                        // fn foo(x : i64) {
+                        //         ^
+                        //  println!(x);
+                        // }
+                        // It is ok to find reference when caret being at ^ (the end of the word)
+                        // So we turn offset into inclusive to include the end of the word
+                        !location
+                            .range
+                            .to_offset(location.buffer.read(cx))
+                            .to_inclusive()
+                            .contains(&head_offset)
+                    });
+                })?;
+            }
+
             if locations.is_empty() {
                 return Ok(());
             }
 
+            // If there is one reference, just open it directly
+            if locations.len() == 1 {
+                let target = locations.pop().unwrap();
+
+                return editor.update(&mut cx, |editor, cx| {
+                    let range = target.range.to_offset(target.buffer.read(cx));
+                    let range = editor.range_for_match(&range);
+
+                    if Some(&target.buffer) == editor.buffer().read(cx).as_singleton().as_ref() {
+                        editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                            s.select_ranges([range]);
+                        });
+                    } else {
+                        cx.window_context().defer(move |cx| {
+                            let target_editor: View<Self> =
+                                workspace.update(cx, |workspace, cx| {
+                                    workspace.open_project_item(target.buffer.clone(), cx)
+                                });
+                            target_editor.update(cx, |target_editor, cx| {
+                                target_editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                                    s.select_ranges([range]);
+                                })
+                            })
+                        })
+                    }
+                });
+            }
+
             workspace.update(&mut cx, |workspace, cx| {
                 let title = locations
                     .first()

crates/editor/src/editor_tests.rs 🔗

@@ -8430,6 +8430,105 @@ async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
     );
 }
 
+#[gpui::test]
+async fn test_find_all_references(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut cx = EditorLspTestContext::new_rust(
+        lsp::ServerCapabilities {
+            document_formatting_provider: Some(lsp::OneOf::Left(true)),
+            ..Default::default()
+        },
+        cx,
+    )
+    .await;
+
+    cx.set_state(indoc! {"
+        fn foo(«paramˇ»: i64) {
+            println!(param);
+        }
+    "});
+
+    cx.lsp
+        .handle_request::<lsp::request::References, _, _>(move |_, _| async move {
+            Ok(Some(vec![
+                lsp::Location {
+                    uri: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(),
+                    range: lsp::Range::new(lsp::Position::new(0, 7), lsp::Position::new(0, 12)),
+                },
+                lsp::Location {
+                    uri: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(),
+                    range: lsp::Range::new(lsp::Position::new(1, 13), lsp::Position::new(1, 18)),
+                },
+            ]))
+        });
+
+    let references = cx
+        .update_editor(|editor, cx| editor.find_all_references(&FindAllReferences, cx))
+        .unwrap();
+
+    cx.executor().run_until_parked();
+
+    cx.executor().start_waiting();
+    references.await.unwrap();
+
+    cx.assert_editor_state(indoc! {"
+        fn foo(param: i64) {
+            println!(«paramˇ»);
+        }
+    "});
+
+    let references = cx
+        .update_editor(|editor, cx| editor.find_all_references(&FindAllReferences, cx))
+        .unwrap();
+
+    cx.executor().run_until_parked();
+
+    cx.executor().start_waiting();
+    references.await.unwrap();
+
+    cx.assert_editor_state(indoc! {"
+        fn foo(«paramˇ»: i64) {
+            println!(param);
+        }
+    "});
+
+    cx.set_state(indoc! {"
+        fn foo(param: i64) {
+            let a = param;
+            let aˇ = param;
+            let a = param;
+            println!(param);
+        }
+    "});
+
+    cx.lsp
+        .handle_request::<lsp::request::References, _, _>(move |_, _| async move {
+            Ok(Some(vec![lsp::Location {
+                uri: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(),
+                range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 9)),
+            }]))
+        });
+
+    let references = cx
+        .update_editor(|editor, cx| editor.find_all_references(&FindAllReferences, cx))
+        .unwrap();
+
+    cx.executor().run_until_parked();
+
+    cx.executor().start_waiting();
+    references.await.unwrap();
+
+    cx.assert_editor_state(indoc! {"
+        fn foo(param: i64) {
+            let a = param;
+            let «aˇ» = param;
+            let a = param;
+            println!(param);
+        }
+    "});
+}
+
 fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
     let point = DisplayPoint::new(row as u32, column as u32);
     point..point