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

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>
This commit is contained in:
Kirill Bulatov 2025-05-01 21:28:05 +03:00 committed by GitHub
parent 50ec26c163
commit 2a319efade
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 276 additions and 64 deletions

View file

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

View file

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

View file

@ -4,15 +4,19 @@ use anyhow::Context as _;
use gpui::{App, AppContext as _, Context, Entity, Window}; use gpui::{App, AppContext as _, Context, Entity, Window};
use language::{Capability, Language, proto::serialize_anchor}; use language::{Capability, Language, proto::serialize_anchor};
use multi_buffer::MultiBuffer; use multi_buffer::MultiBuffer;
use project::lsp_store::{ use project::{
lsp_command::location_link_from_proto,
lsp_store::{
lsp_ext_command::{DocsUrls, ExpandMacro, ExpandedMacro}, lsp_ext_command::{DocsUrls, ExpandMacro, ExpandedMacro},
rust_analyzer_ext::RUST_ANALYZER_NAME, rust_analyzer_ext::RUST_ANALYZER_NAME,
},
}; };
use rpc::proto; use rpc::proto;
use text::ToPointUtf16; use text::ToPointUtf16;
use crate::{ 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, 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()) .filter_map(|buffer| buffer.read(cx).language())
.any(|language| is_rust_language(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, expand_macro_recursively);
register_action(&editor, window, open_docs); 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( pub fn expand_macro_recursively(
editor: &mut Editor, editor: &mut Editor,
_: &ExpandMacroRecursively, _: &ExpandMacroRecursively,

View file

@ -13,7 +13,7 @@ use client::proto::{self, PeerId};
use clock::Global; use clock::Global;
use collections::HashSet; use collections::HashSet;
use futures::future; use futures::future;
use gpui::{App, AsyncApp, Entity}; use gpui::{App, AsyncApp, Entity, Task};
use language::{ use language::{
Anchor, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CharKind, OffsetRangeExt, PointUtf16, Anchor, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CharKind, OffsetRangeExt, PointUtf16,
ToOffset, ToPointUtf16, Transaction, Unclipped, ToOffset, ToPointUtf16, Transaction, Unclipped,
@ -966,7 +966,7 @@ fn language_server_for_buffer(
.ok_or_else(|| anyhow!("no language server found 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>, proto_links: Vec<proto::LocationLink>,
lsp_store: Entity<LspStore>, lsp_store: Entity<LspStore>,
mut cx: AsyncApp, mut cx: AsyncApp,
@ -974,17 +974,18 @@ async fn location_links_from_proto(
let mut links = Vec::new(); let mut links = Vec::new();
for link in proto_links { 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) Ok(links)
} }
pub async fn location_link_from_proto( pub fn location_link_from_proto(
link: proto::LocationLink, link: proto::LocationLink,
lsp_store: &Entity<LspStore>, lsp_store: Entity<LspStore>,
cx: &mut AsyncApp, cx: &mut AsyncApp,
) -> Result<LocationLink> { ) -> Task<Result<LocationLink>> {
cx.spawn(async move |cx| {
let origin = match link.origin { let origin = match link.origin {
Some(origin) => { Some(origin) => {
let buffer_id = BufferId::new(origin.buffer_id)?; let buffer_id = BufferId::new(origin.buffer_id)?;
@ -1035,9 +1036,10 @@ pub async fn location_link_from_proto(
range: start..end, range: start..end,
}; };
Ok(LocationLink { origin, target }) Ok(LocationLink { origin, target })
})
} }
async fn location_links_from_lsp( pub async fn location_links_from_lsp(
message: Option<lsp::GotoDefinitionResponse>, message: Option<lsp::GotoDefinitionResponse>,
lsp_store: Entity<LspStore>, lsp_store: Entity<LspStore>,
buffer: Entity<Buffer>, 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>, links: Vec<LocationLink>,
lsp_store: &mut LspStore, lsp_store: &mut LspStore,
peer_id: PeerId, peer_id: PeerId,

View file

@ -2,9 +2,10 @@ use crate::{
LocationLink, LocationLink,
lsp_command::{ lsp_command::{
LspCommand, location_link_from_lsp, location_link_from_proto, location_link_to_proto, 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, lsp_store::LspStore,
make_text_document_identifier, make_lsp_text_document_position, make_text_document_identifier,
}; };
use anyhow::{Context as _, Result}; use anyhow::{Context as _, Result};
use async_trait::async_trait; use async_trait::async_trait;
@ -301,6 +302,19 @@ pub struct SwitchSourceHeaderResult(pub String);
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct SwitchSourceHeader; 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)] #[async_trait(?Send)]
impl LspCommand for SwitchSourceHeader { impl LspCommand for SwitchSourceHeader {
type Response = SwitchSourceHeaderResult; 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 // 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 // Taken from https://github.com/rust-lang/rust-analyzer/blob/a73a37a757a58b43a796d3eb86a1f7dfd0036659/crates/rust-analyzer/src/lsp/ext.rs#L425-L489
pub enum Runnables {} pub enum Runnables {}
@ -633,7 +737,7 @@ impl LspCommand for GetLspRunnables {
for lsp_runnable in message.runnables { for lsp_runnable in message.runnables {
let location = match lsp_runnable.location { let location = match lsp_runnable.location {
Some(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, None => None,
}; };

View file

@ -182,6 +182,16 @@ message LspExtSwitchSourceHeaderResponse {
string target_file = 1; 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 { message GetCompletionsResponse {
repeated Completion completions = 1; repeated Completion completions = 1;
repeated VectorClockEntry version = 2; repeated VectorClockEntry version = 2;

View file

@ -378,7 +378,10 @@ message Envelope {
GetDebugAdapterBinary get_debug_adapter_binary = 339; GetDebugAdapterBinary get_debug_adapter_binary = 339;
DebugAdapterBinary debug_adapter_binary = 340; DebugAdapterBinary debug_adapter_binary = 340;
RunDebugLocators run_debug_locators = 341; 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; reserved 87 to 88;

View file

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