Add `editor::GoToParentModule` for rust-analyzer backed projects (#29755)

Kirill Bulatov and Julia Ryan created

Support rust-analyzer's "go to parent module" action


https://rust-analyzer.github.io/book/contributing/lsp-extensions.html#parent-module

Release Notes:

- Added `editor::GoToParentModule` for rust-analyzer backed projects

---------

Co-authored-by: Julia Ryan <juliaryan3.14@gmail.com>

Change summary

crates/collab/src/rpc.rs                        |   1 
crates/editor/src/actions.rs                    |   1 
crates/editor/src/rust_analyzer_ext.rs          |  95 ++++++++++++++
crates/project/src/lsp_command.rs               | 116 +++++++++---------
crates/project/src/lsp_store/lsp_ext_command.rs | 108 +++++++++++++++++
crates/proto/proto/lsp.proto                    |  10 +
crates/proto/proto/zed.proto                    |   5 
crates/proto/src/proto.rs                       |   4 
8 files changed, 276 insertions(+), 64 deletions(-)

Detailed changes

crates/collab/src/rpc.rs 🔗

@@ -327,6 +327,7 @@ impl Server {
             .add_request_handler(
                 forward_read_only_project_request::<proto::LspExtSwitchSourceHeader>,
             )
+            .add_request_handler(forward_read_only_project_request::<proto::LspExtGoToParentModule>)
             .add_request_handler(
                 forward_read_only_project_request::<proto::LanguageServerIdForName>,
             )

crates/editor/src/actions.rs 🔗

@@ -308,6 +308,7 @@ actions!(
         GoToImplementation,
         GoToImplementationSplit,
         GoToNextChange,
+        GoToParentModule,
         GoToPreviousChange,
         GoToPreviousDiagnostic,
         GoToTypeDefinition,

crates/editor/src/rust_analyzer_ext.rs 🔗

@@ -4,15 +4,19 @@ use anyhow::Context as _;
 use gpui::{App, AppContext as _, Context, Entity, Window};
 use language::{Capability, Language, proto::serialize_anchor};
 use multi_buffer::MultiBuffer;
-use project::lsp_store::{
-    lsp_ext_command::{DocsUrls, ExpandMacro, ExpandedMacro},
-    rust_analyzer_ext::RUST_ANALYZER_NAME,
+use project::{
+    lsp_command::location_link_from_proto,
+    lsp_store::{
+        lsp_ext_command::{DocsUrls, ExpandMacro, ExpandedMacro},
+        rust_analyzer_ext::RUST_ANALYZER_NAME,
+    },
 };
 use rpc::proto;
 use text::ToPointUtf16;
 
 use crate::{
-    Editor, ExpandMacroRecursively, OpenDocs, element::register_action,
+    Editor, ExpandMacroRecursively, GoToParentModule, GotoDefinitionKind, OpenDocs,
+    element::register_action, hover_links::HoverLink,
     lsp_ext::find_specific_language_server_in_selection,
 };
 
@@ -30,11 +34,94 @@ pub fn apply_related_actions(editor: &Entity<Editor>, window: &mut Window, cx: &
         .filter_map(|buffer| buffer.read(cx).language())
         .any(|language| is_rust_language(language))
     {
+        register_action(&editor, window, go_to_parent_module);
         register_action(&editor, window, expand_macro_recursively);
         register_action(&editor, window, open_docs);
     }
 }
 
+pub fn go_to_parent_module(
+    editor: &mut Editor,
+    _: &GoToParentModule,
+    window: &mut Window,
+    cx: &mut Context<Editor>,
+) {
+    if editor.selections.count() == 0 {
+        return;
+    }
+    let Some(project) = &editor.project else {
+        return;
+    };
+
+    let server_lookup = find_specific_language_server_in_selection(
+        editor,
+        cx,
+        is_rust_language,
+        RUST_ANALYZER_NAME,
+    );
+
+    let project = project.clone();
+    let lsp_store = project.read(cx).lsp_store();
+    let upstream_client = lsp_store.read(cx).upstream_client();
+    cx.spawn_in(window, async move |editor, cx| {
+        let Some((trigger_anchor, _, server_to_query, buffer)) = server_lookup.await else {
+            return anyhow::Ok(());
+        };
+
+        let location_links = if let Some((client, project_id)) = upstream_client {
+            let buffer_id = buffer.update(cx, |buffer, _| buffer.remote_id())?;
+
+            let request = proto::LspExtGoToParentModule {
+                project_id,
+                buffer_id: buffer_id.to_proto(),
+                position: Some(serialize_anchor(&trigger_anchor.text_anchor)),
+            };
+            let response = client
+                .request(request)
+                .await
+                .context("lsp ext go to parent module proto request")?;
+            futures::future::join_all(
+                response
+                    .links
+                    .into_iter()
+                    .map(|link| location_link_from_proto(link, lsp_store.clone(), cx)),
+            )
+            .await
+            .into_iter()
+            .collect::<anyhow::Result<_>>()
+            .context("go to parent module via collab")?
+        } else {
+            let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot())?;
+            let position = trigger_anchor.text_anchor.to_point_utf16(&buffer_snapshot);
+            project
+                .update(cx, |project, cx| {
+                    project.request_lsp(
+                        buffer,
+                        project::LanguageServerToQuery::Other(server_to_query),
+                        project::lsp_store::lsp_ext_command::GoToParentModule { position },
+                        cx,
+                    )
+                })?
+                .await
+                .context("go to parent module")?
+        };
+
+        editor
+            .update_in(cx, |editor, window, cx| {
+                editor.navigate_to_hover_links(
+                    Some(GotoDefinitionKind::Declaration),
+                    location_links.into_iter().map(HoverLink::Text).collect(),
+                    false,
+                    window,
+                    cx,
+                )
+            })?
+            .await?;
+        Ok(())
+    })
+    .detach_and_log_err(cx);
+}
+
 pub fn expand_macro_recursively(
     editor: &mut Editor,
     _: &ExpandMacroRecursively,

crates/project/src/lsp_command.rs 🔗

@@ -13,7 +13,7 @@ use client::proto::{self, PeerId};
 use clock::Global;
 use collections::HashSet;
 use futures::future;
-use gpui::{App, AsyncApp, Entity};
+use gpui::{App, AsyncApp, Entity, Task};
 use language::{
     Anchor, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CharKind, OffsetRangeExt, PointUtf16,
     ToOffset, ToPointUtf16, Transaction, Unclipped,
@@ -966,7 +966,7 @@ fn language_server_for_buffer(
         .ok_or_else(|| anyhow!("no language server found for buffer"))
 }
 
-async fn location_links_from_proto(
+pub async fn location_links_from_proto(
     proto_links: Vec<proto::LocationLink>,
     lsp_store: Entity<LspStore>,
     mut cx: AsyncApp,
@@ -974,70 +974,72 @@ async fn location_links_from_proto(
     let mut links = Vec::new();
 
     for link in proto_links {
-        links.push(location_link_from_proto(link, &lsp_store, &mut cx).await?)
+        links.push(location_link_from_proto(link, lsp_store.clone(), &mut cx).await?)
     }
 
     Ok(links)
 }
 
-pub async fn location_link_from_proto(
+pub fn location_link_from_proto(
     link: proto::LocationLink,
-    lsp_store: &Entity<LspStore>,
+    lsp_store: Entity<LspStore>,
     cx: &mut AsyncApp,
-) -> Result<LocationLink> {
-    let origin = match link.origin {
-        Some(origin) => {
-            let buffer_id = BufferId::new(origin.buffer_id)?;
-            let buffer = lsp_store
-                .update(cx, |lsp_store, cx| {
-                    lsp_store.wait_for_remote_buffer(buffer_id, cx)
-                })?
-                .await?;
-            let start = origin
-                .start
-                .and_then(deserialize_anchor)
-                .ok_or_else(|| anyhow!("missing origin start"))?;
-            let end = origin
-                .end
-                .and_then(deserialize_anchor)
-                .ok_or_else(|| anyhow!("missing origin end"))?;
-            buffer
-                .update(cx, |buffer, _| buffer.wait_for_anchors([start, end]))?
-                .await?;
-            Some(Location {
-                buffer,
-                range: start..end,
-            })
-        }
-        None => None,
-    };
+) -> Task<Result<LocationLink>> {
+    cx.spawn(async move |cx| {
+        let origin = match link.origin {
+            Some(origin) => {
+                let buffer_id = BufferId::new(origin.buffer_id)?;
+                let buffer = lsp_store
+                    .update(cx, |lsp_store, cx| {
+                        lsp_store.wait_for_remote_buffer(buffer_id, cx)
+                    })?
+                    .await?;
+                let start = origin
+                    .start
+                    .and_then(deserialize_anchor)
+                    .ok_or_else(|| anyhow!("missing origin start"))?;
+                let end = origin
+                    .end
+                    .and_then(deserialize_anchor)
+                    .ok_or_else(|| anyhow!("missing origin end"))?;
+                buffer
+                    .update(cx, |buffer, _| buffer.wait_for_anchors([start, end]))?
+                    .await?;
+                Some(Location {
+                    buffer,
+                    range: start..end,
+                })
+            }
+            None => None,
+        };
 
-    let target = link.target.ok_or_else(|| anyhow!("missing target"))?;
-    let buffer_id = BufferId::new(target.buffer_id)?;
-    let buffer = lsp_store
-        .update(cx, |lsp_store, cx| {
-            lsp_store.wait_for_remote_buffer(buffer_id, cx)
-        })?
-        .await?;
-    let start = target
-        .start
-        .and_then(deserialize_anchor)
-        .ok_or_else(|| anyhow!("missing target start"))?;
-    let end = target
-        .end
-        .and_then(deserialize_anchor)
-        .ok_or_else(|| anyhow!("missing target end"))?;
-    buffer
-        .update(cx, |buffer, _| buffer.wait_for_anchors([start, end]))?
-        .await?;
-    let target = Location {
-        buffer,
-        range: start..end,
-    };
-    Ok(LocationLink { origin, target })
+        let target = link.target.ok_or_else(|| anyhow!("missing target"))?;
+        let buffer_id = BufferId::new(target.buffer_id)?;
+        let buffer = lsp_store
+            .update(cx, |lsp_store, cx| {
+                lsp_store.wait_for_remote_buffer(buffer_id, cx)
+            })?
+            .await?;
+        let start = target
+            .start
+            .and_then(deserialize_anchor)
+            .ok_or_else(|| anyhow!("missing target start"))?;
+        let end = target
+            .end
+            .and_then(deserialize_anchor)
+            .ok_or_else(|| anyhow!("missing target end"))?;
+        buffer
+            .update(cx, |buffer, _| buffer.wait_for_anchors([start, end]))?
+            .await?;
+        let target = Location {
+            buffer,
+            range: start..end,
+        };
+        Ok(LocationLink { origin, target })
+    })
 }
 
-async fn location_links_from_lsp(
+pub async fn location_links_from_lsp(
     message: Option<lsp::GotoDefinitionResponse>,
     lsp_store: Entity<LspStore>,
     buffer: Entity<Buffer>,
@@ -1178,7 +1180,7 @@ pub async fn location_link_from_lsp(
     })
 }
 
-fn location_links_to_proto(
+pub fn location_links_to_proto(
     links: Vec<LocationLink>,
     lsp_store: &mut LspStore,
     peer_id: PeerId,

crates/project/src/lsp_store/lsp_ext_command.rs 🔗

@@ -2,9 +2,10 @@ use crate::{
     LocationLink,
     lsp_command::{
         LspCommand, location_link_from_lsp, location_link_from_proto, location_link_to_proto,
+        location_links_from_lsp, location_links_from_proto, location_links_to_proto,
     },
     lsp_store::LspStore,
-    make_text_document_identifier,
+    make_lsp_text_document_position, make_text_document_identifier,
 };
 use anyhow::{Context as _, Result};
 use async_trait::async_trait;
@@ -301,6 +302,19 @@ pub struct SwitchSourceHeaderResult(pub String);
 #[serde(rename_all = "camelCase")]
 pub struct SwitchSourceHeader;
 
+#[derive(Debug)]
+pub struct GoToParentModule {
+    pub position: PointUtf16,
+}
+
+pub struct LspGoToParentModule {}
+
+impl lsp::request::Request for LspGoToParentModule {
+    type Params = lsp::TextDocumentPositionParams;
+    type Result = Option<Vec<lsp::LocationLink>>;
+    const METHOD: &'static str = "experimental/parentModule";
+}
+
 #[async_trait(?Send)]
 impl LspCommand for SwitchSourceHeader {
     type Response = SwitchSourceHeaderResult;
@@ -379,6 +393,96 @@ impl LspCommand for SwitchSourceHeader {
     }
 }
 
+#[async_trait(?Send)]
+impl LspCommand for GoToParentModule {
+    type Response = Vec<LocationLink>;
+    type LspRequest = LspGoToParentModule;
+    type ProtoRequest = proto::LspExtGoToParentModule;
+
+    fn display_name(&self) -> &str {
+        "Go to parent module"
+    }
+
+    fn to_lsp(
+        &self,
+        path: &Path,
+        _: &Buffer,
+        _: &Arc<LanguageServer>,
+        _: &App,
+    ) -> Result<lsp::TextDocumentPositionParams> {
+        make_lsp_text_document_position(path, self.position)
+    }
+
+    async fn response_from_lsp(
+        self,
+        links: Option<Vec<lsp::LocationLink>>,
+        lsp_store: Entity<LspStore>,
+        buffer: Entity<Buffer>,
+        server_id: LanguageServerId,
+        cx: AsyncApp,
+    ) -> anyhow::Result<Vec<LocationLink>> {
+        location_links_from_lsp(
+            links.map(lsp::GotoDefinitionResponse::Link),
+            lsp_store,
+            buffer,
+            server_id,
+            cx,
+        )
+        .await
+    }
+
+    fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::LspExtGoToParentModule {
+        proto::LspExtGoToParentModule {
+            project_id,
+            buffer_id: buffer.remote_id().to_proto(),
+            position: Some(language::proto::serialize_anchor(
+                &buffer.anchor_before(self.position),
+            )),
+        }
+    }
+
+    async fn from_proto(
+        request: Self::ProtoRequest,
+        _: Entity<LspStore>,
+        buffer: Entity<Buffer>,
+        mut cx: AsyncApp,
+    ) -> anyhow::Result<Self> {
+        let position = request
+            .position
+            .and_then(deserialize_anchor)
+            .context("bad request with bad position")?;
+        Ok(Self {
+            position: buffer.update(&mut cx, |buffer, _| position.to_point_utf16(buffer))?,
+        })
+    }
+
+    fn response_to_proto(
+        links: Vec<LocationLink>,
+        lsp_store: &mut LspStore,
+        peer_id: PeerId,
+        _: &clock::Global,
+        cx: &mut App,
+    ) -> proto::LspExtGoToParentModuleResponse {
+        proto::LspExtGoToParentModuleResponse {
+            links: location_links_to_proto(links, lsp_store, peer_id, cx),
+        }
+    }
+
+    async fn response_from_proto(
+        self,
+        message: proto::LspExtGoToParentModuleResponse,
+        lsp_store: Entity<LspStore>,
+        _: Entity<Buffer>,
+        cx: AsyncApp,
+    ) -> anyhow::Result<Vec<LocationLink>> {
+        location_links_from_proto(message.links, lsp_store, cx).await
+    }
+
+    fn buffer_id_from_proto(message: &proto::LspExtGoToParentModule) -> Result<BufferId> {
+        BufferId::new(message.buffer_id)
+    }
+}
+
 // https://rust-analyzer.github.io/book/contributing/lsp-extensions.html#runnables
 // Taken from https://github.com/rust-lang/rust-analyzer/blob/a73a37a757a58b43a796d3eb86a1f7dfd0036659/crates/rust-analyzer/src/lsp/ext.rs#L425-L489
 pub enum Runnables {}
@@ -633,7 +737,7 @@ impl LspCommand for GetLspRunnables {
         for lsp_runnable in message.runnables {
             let location = match lsp_runnable.location {
                 Some(location) => {
-                    Some(location_link_from_proto(location, &lsp_store, &mut cx).await?)
+                    Some(location_link_from_proto(location, lsp_store.clone(), &mut cx).await?)
                 }
                 None => None,
             };

crates/proto/proto/lsp.proto 🔗

@@ -182,6 +182,16 @@ message LspExtSwitchSourceHeaderResponse {
     string target_file = 1;
 }
 
+message LspExtGoToParentModule {
+    uint64 project_id = 1;
+    uint64 buffer_id = 2;
+    Anchor position = 3;
+}
+
+message LspExtGoToParentModuleResponse {
+    repeated LocationLink links = 1;
+}
+
 message GetCompletionsResponse {
     repeated Completion completions = 1;
     repeated VectorClockEntry version = 2;

crates/proto/proto/zed.proto 🔗

@@ -378,7 +378,10 @@ message Envelope {
         GetDebugAdapterBinary get_debug_adapter_binary = 339;
         DebugAdapterBinary debug_adapter_binary = 340;
         RunDebugLocators run_debug_locators = 341;
-        DebugRequest debug_request = 342; // current max
+        DebugRequest debug_request = 342;
+
+        LspExtGoToParentModule lsp_ext_go_to_parent_module = 343;
+        LspExtGoToParentModuleResponse lsp_ext_go_to_parent_module_response = 344;// current max
     }
 
     reserved 87 to 88;

crates/proto/src/proto.rs 🔗

@@ -169,6 +169,8 @@ messages!(
     (LspExtRunnablesResponse, Background),
     (LspExtSwitchSourceHeader, Background),
     (LspExtSwitchSourceHeaderResponse, Background),
+    (LspExtGoToParentModule, Background),
+    (LspExtGoToParentModuleResponse, Background),
     (MarkNotificationRead, Foreground),
     (MoveChannel, Foreground),
     (MultiLspQuery, Background),
@@ -422,6 +424,7 @@ request_messages!(
     (CreateContext, CreateContextResponse),
     (SynchronizeContexts, SynchronizeContextsResponse),
     (LspExtSwitchSourceHeader, LspExtSwitchSourceHeaderResponse),
+    (LspExtGoToParentModule, LspExtGoToParentModuleResponse),
     (AddWorktree, AddWorktreeResponse),
     (ShutdownRemoteServer, Ack),
     (RemoveWorktree, Ack),
@@ -544,6 +547,7 @@ entity_messages!(
     UpdateContext,
     SynchronizeContexts,
     LspExtSwitchSourceHeader,
+    LspExtGoToParentModule,
     LanguageServerLog,
     Toast,
     HideToast,