Add a way to control go to definition fallback (#27426)

Kirill Bulatov created

Follow-up of https://github.com/zed-industries/zed/pull/9243 and
https://github.com/zed-industries/zed/pull/16512

Release Notes:

- Added a way to control go to definition fallback

Change summary

assets/settings/default.json         |  5 ++
crates/editor/src/editor.rs          | 17 +++++--
crates/editor/src/editor_settings.rs | 20 +++++++++
crates/editor/src/editor_tests.rs    | 63 ++++++++++++++++++++++++++++++
crates/editor/src/hover_links.rs     |  9 +++
5 files changed, 107 insertions(+), 7 deletions(-)

Detailed changes

assets/settings/default.json 🔗

@@ -184,6 +184,11 @@
   // Whether to show the signature help after completion or a bracket pair inserted.
   // If `auto_signature_help` is enabled, this setting will be treated as enabled also.
   "show_signature_help_after_edits": false,
+  // What to do when go to definition yields no results.
+  //
+  // 1. Do nothing: `none`
+  // 2. Find references for the same symbol: `find_all_references` (default)
+  "go_to_definition_fallback": "find_all_references",
   // Whether to show wrap guides (vertical rulers) in the editor.
   // Setting this to true will show a guide at the 'preferred_line_length' value
   // if 'soft_wrap' is set to 'preferred_line_length', and will show any

crates/editor/src/editor.rs 🔗

@@ -60,6 +60,7 @@ use collections::{BTreeMap, HashMap, HashSet, VecDeque};
 use convert_case::{Case, Casing};
 use display_map::*;
 pub use display_map::{DisplayPoint, FoldPlaceholder};
+use editor_settings::GoToDefinitionFallback;
 pub use editor_settings::{
     CurrentLineHighlight, EditorSettings, ScrollBeyondLastLine, SearchSettings, ShowScrollbar,
 };
@@ -12662,15 +12663,21 @@ impl Editor {
     ) -> Task<Result<Navigated>> {
         let definition =
             self.go_to_definition_of_kind(GotoDefinitionKind::Symbol, false, window, cx);
+        let fallback_strategy = EditorSettings::get_global(cx).go_to_definition_fallback;
         cx.spawn_in(window, async move |editor, cx| {
             if definition.await? == Navigated::Yes {
                 return Ok(Navigated::Yes);
             }
-            match editor.update_in(cx, |editor, window, cx| {
-                editor.find_all_references(&FindAllReferences, window, cx)
-            })? {
-                Some(references) => references.await,
-                None => Ok(Navigated::No),
+            match fallback_strategy {
+                GoToDefinitionFallback::None => Ok(Navigated::No),
+                GoToDefinitionFallback::FindAllReferences => {
+                    match editor.update_in(cx, |editor, window, cx| {
+                        editor.find_all_references(&FindAllReferences, window, cx)
+                    })? {
+                        Some(references) => references.await,
+                        None => Ok(Navigated::No),
+                    }
+                }
             }
         })
     }

crates/editor/src/editor_settings.rs 🔗

@@ -36,6 +36,8 @@ pub struct EditorSettings {
     pub search: SearchSettings,
     pub auto_signature_help: bool,
     pub show_signature_help_after_edits: bool,
+    #[serde(default)]
+    pub go_to_definition_fallback: GoToDefinitionFallback,
     pub jupyter: Jupyter,
 }
 
@@ -211,6 +213,17 @@ pub struct SearchSettings {
     pub regex: bool,
 }
 
+/// What to do when go to definition yields no results.
+#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum GoToDefinitionFallback {
+    /// Disables the fallback.
+    None,
+    /// Looks up references of the same symbol instead.
+    #[default]
+    FindAllReferences,
+}
+
 #[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
 pub struct EditorSettingsContent {
     /// Whether the cursor blinks in the editor.
@@ -330,6 +343,13 @@ pub struct EditorSettingsContent {
     /// Default: false
     pub show_signature_help_after_edits: Option<bool>,
 
+    /// Whether to follow-up empty go to definition responses from the language server or not.
+    /// `FindAllReferences` allows to look up references of the same symbol instead.
+    /// `None` disables the fallback.
+    ///
+    /// Default: FindAllReferences
+    pub go_to_definition_fallback: Option<GoToDefinitionFallback>,
+
     /// Jupyter REPL settings.
     pub jupyter: Option<JupyterContent>,
 }

crates/editor/src/editor_tests.rs 🔗

@@ -16434,6 +16434,69 @@ async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestApp
     });
 }
 
+#[gpui::test]
+async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+    cx.update(|cx| {
+        let mut editor_settings = EditorSettings::get_global(cx).clone();
+        editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
+        EditorSettings::override_global(editor_settings, cx);
+    });
+    let mut cx = EditorLspTestContext::new_rust(
+        lsp::ServerCapabilities {
+            definition_provider: Some(lsp::OneOf::Left(true)),
+            references_provider: Some(lsp::OneOf::Left(true)),
+            ..lsp::ServerCapabilities::default()
+        },
+        cx,
+    )
+    .await;
+    let original_state = r#"fn one() {
+        let mut a = ˇtwo();
+    }
+
+    fn two() {}"#
+        .unindent();
+    cx.set_state(&original_state);
+
+    let mut go_to_definition = cx
+        .lsp
+        .set_request_handler::<lsp::request::GotoDefinition, _, _>(
+            move |_, _| async move { Ok(None) },
+        );
+    let _references = cx
+        .lsp
+        .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
+            panic!("Should not call for references with no go to definition fallback")
+        });
+
+    let navigated = cx
+        .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
+        .await
+        .expect("Failed to navigate to lookup references");
+    go_to_definition
+        .next()
+        .await
+        .expect("Should have called the go_to_definition handler");
+
+    assert_eq!(
+        navigated,
+        Navigated::No,
+        "Should have navigated to references as a fallback after empty GoToDefinition response"
+    );
+    cx.assert_editor_state(&original_state);
+    let editors = cx.update_workspace(|workspace, _, cx| {
+        workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
+    });
+    cx.update_editor(|_, _, _| {
+        assert_eq!(
+            editors.len(),
+            1,
+            "After unsuccessful fallback, no other editor should have been opened"
+        );
+    });
+}
+
 #[gpui::test]
 async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
     init_test(cx, |_| {});

crates/editor/src/hover_links.rs 🔗

@@ -1,5 +1,5 @@
 use crate::{
-    editor_settings::MultiCursorModifier,
+    editor_settings::{GoToDefinitionFallback, MultiCursorModifier},
     hover_popover::{self, InlayHover},
     scroll::ScrollAmount,
     Anchor, Editor, EditorSettings, EditorSnapshot, FindAllReferences, GoToDefinition,
@@ -174,7 +174,12 @@ impl Editor {
                     if definition_revealed == Navigated::Yes {
                         return None;
                     }
-                    editor.find_all_references(&FindAllReferences, window, cx)
+                    match EditorSettings::get_global(cx).go_to_definition_fallback {
+                        GoToDefinitionFallback::None => None,
+                        GoToDefinitionFallback::FindAllReferences => {
+                            editor.find_all_references(&FindAllReferences, window, cx)
+                        }
+                    }
                 })
                 .ok()
                 .flatten();