lsp: Retrieve links to documentation for the given symbol (#19233)

Closes #18924 

Release Notes:

- Added an `editor:OpenDocs` action to open links to documentation via
rust-analyzer
This commit is contained in:
Lu Wan 2024-11-16 10:23:49 -08:00 committed by GitHub
parent f9990b42fa
commit 2d3476530e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 213 additions and 2 deletions

View file

@ -297,6 +297,7 @@ gpui::actions!(
OpenExcerptsSplit,
OpenProposedChangesEditor,
OpenFile,
OpenDocs,
OpenPermalinkToLine,
OpenUrl,
Outdent,

View file

@ -1,3 +1,5 @@
use std::{fs, path::Path};
use anyhow::Context as _;
use gpui::{Context, View, ViewContext, VisualContext, WindowContext};
use language::Language;
@ -7,7 +9,7 @@ use text::ToPointUtf16;
use crate::{
element::register_action, lsp_ext::find_specific_language_server_in_selection, Editor,
ExpandMacroRecursively,
ExpandMacroRecursively, OpenDocs,
};
const RUST_ANALYZER_NAME: &str = "rust-analyzer";
@ -24,6 +26,7 @@ pub fn apply_related_actions(editor: &View<Editor>, cx: &mut WindowContext) {
.is_some()
{
register_action(editor, cx, expand_macro_recursively);
register_action(editor, cx, open_docs);
}
}
@ -94,3 +97,64 @@ pub fn expand_macro_recursively(
})
.detach_and_log_err(cx);
}
pub fn open_docs(editor: &mut Editor, _: &OpenDocs, cx: &mut ViewContext<'_, Editor>) {
if editor.selections.count() == 0 {
return;
}
let Some(project) = &editor.project else {
return;
};
let Some(workspace) = editor.workspace() else {
return;
};
let Some((trigger_anchor, _rust_language, server_to_query, buffer)) =
find_specific_language_server_in_selection(
editor,
cx,
is_rust_language,
RUST_ANALYZER_NAME,
)
else {
return;
};
let project = project.clone();
let buffer_snapshot = buffer.read(cx).snapshot();
let position = trigger_anchor.text_anchor.to_point_utf16(&buffer_snapshot);
let open_docs_task = project.update(cx, |project, cx| {
project.request_lsp(
buffer,
project::LanguageServerToQuery::Other(server_to_query),
project::lsp_ext_command::OpenDocs { position },
cx,
)
});
cx.spawn(|_editor, mut cx| async move {
let docs_urls = open_docs_task.await.context("open docs")?;
if docs_urls.is_empty() {
log::debug!("Empty docs urls for position {position:?}");
return Ok(());
} else {
log::debug!("{:?}", docs_urls);
}
workspace.update(&mut cx, |_workspace, cx| {
// Check if the local document exists, otherwise fallback to the online document.
// Open with the default browser.
if let Some(local_url) = docs_urls.local {
if fs::metadata(Path::new(&local_url[8..])).is_ok() {
cx.open_url(&local_url);
return;
}
}
if let Some(web_url) = docs_urls.web {
cx.open_url(&web_url);
}
})
})
.detach_and_log_err(cx);
}

View file

@ -762,6 +762,7 @@ impl LanguageServer {
}),
experimental: Some(json!({
"serverStatusNotification": true,
"localDocs": true,
})),
window: Some(WindowClientCapabilities {
work_done_progress: Some(true),

View file

@ -134,6 +134,132 @@ impl LspCommand for ExpandMacro {
}
}
pub enum LspOpenDocs {}
impl lsp::request::Request for LspOpenDocs {
type Params = OpenDocsParams;
type Result = Option<DocsUrls>;
const METHOD: &'static str = "experimental/externalDocs";
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct OpenDocsParams {
pub text_document: lsp::TextDocumentIdentifier,
pub position: lsp::Position,
}
#[derive(Serialize, Deserialize, Debug, Default)]
#[serde(rename_all = "camelCase")]
pub struct DocsUrls {
pub web: Option<String>,
pub local: Option<String>,
}
impl DocsUrls {
pub fn is_empty(&self) -> bool {
self.web.is_none() && self.local.is_none()
}
}
#[derive(Debug)]
pub struct OpenDocs {
pub position: PointUtf16,
}
#[async_trait(?Send)]
impl LspCommand for OpenDocs {
type Response = DocsUrls;
type LspRequest = LspOpenDocs;
type ProtoRequest = proto::LspExtOpenDocs;
fn to_lsp(
&self,
path: &Path,
_: &Buffer,
_: &Arc<LanguageServer>,
_: &AppContext,
) -> OpenDocsParams {
OpenDocsParams {
text_document: lsp::TextDocumentIdentifier {
uri: lsp::Url::from_file_path(path).unwrap(),
},
position: point_to_lsp(self.position),
}
}
async fn response_from_lsp(
self,
message: Option<DocsUrls>,
_: Model<LspStore>,
_: Model<Buffer>,
_: LanguageServerId,
_: AsyncAppContext,
) -> anyhow::Result<DocsUrls> {
Ok(message
.map(|message| DocsUrls {
web: message.web,
local: message.local,
})
.unwrap_or_default())
}
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::LspExtOpenDocs {
proto::LspExtOpenDocs {
project_id,
buffer_id: buffer.remote_id().into(),
position: Some(language::proto::serialize_anchor(
&buffer.anchor_before(self.position),
)),
}
}
async fn from_proto(
message: Self::ProtoRequest,
_: Model<LspStore>,
buffer: Model<Buffer>,
mut cx: AsyncAppContext,
) -> anyhow::Result<Self> {
let position = message
.position
.and_then(deserialize_anchor)
.context("invalid position")?;
Ok(Self {
position: buffer.update(&mut cx, |buffer, _| position.to_point_utf16(buffer))?,
})
}
fn response_to_proto(
response: DocsUrls,
_: &mut LspStore,
_: PeerId,
_: &clock::Global,
_: &mut AppContext,
) -> proto::LspExtOpenDocsResponse {
proto::LspExtOpenDocsResponse {
web: response.web,
local: response.local,
}
}
async fn response_from_proto(
self,
message: proto::LspExtOpenDocsResponse,
_: Model<LspStore>,
_: Model<Buffer>,
_: AsyncAppContext,
) -> anyhow::Result<DocsUrls> {
Ok(DocsUrls {
web: message.web,
local: message.local,
})
}
fn buffer_id_from_proto(message: &proto::LspExtOpenDocs) -> Result<BufferId> {
BufferId::new(message.buffer_id)
}
}
pub enum LspSwitchSourceHeader {}
impl lsp::request::Request for LspSwitchSourceHeader {

View file

@ -276,6 +276,7 @@ message Envelope {
LanguageServerPromptRequest language_server_prompt_request = 268;
LanguageServerPromptResponse language_server_prompt_response = 269;
GitBranches git_branches = 270;
GitBranchesResponse git_branches_response = 271;
@ -293,7 +294,10 @@ message Envelope {
GetPanicFiles get_panic_files = 280;
GetPanicFilesResponse get_panic_files_response = 281;
CancelLanguageServerWork cancel_language_server_work = 282; // current max
CancelLanguageServerWork cancel_language_server_work = 282;
LspExtOpenDocs lsp_ext_open_docs = 283;
LspExtOpenDocsResponse lsp_ext_open_docs_response = 284; // current max
}
reserved 87 to 88;
@ -2024,6 +2028,17 @@ message LspExtExpandMacroResponse {
string expansion = 2;
}
message LspExtOpenDocs {
uint64 project_id = 1;
uint64 buffer_id = 2;
Anchor position = 3;
}
message LspExtOpenDocsResponse {
optional string web = 1;
optional string local = 2;
}
message LspExtSwitchSourceHeader {
uint64 project_id = 1;
uint64 buffer_id = 2;

View file

@ -314,6 +314,8 @@ messages!(
(UsersResponse, Foreground),
(LspExtExpandMacro, Background),
(LspExtExpandMacroResponse, Background),
(LspExtOpenDocs, Background),
(LspExtOpenDocsResponse, Background),
(SetRoomParticipantRole, Foreground),
(BlameBuffer, Foreground),
(BlameBufferResponse, Foreground),
@ -464,6 +466,7 @@ request_messages!(
(UpdateProject, Ack),
(UpdateWorktree, Ack),
(LspExtExpandMacro, LspExtExpandMacroResponse),
(LspExtOpenDocs, LspExtOpenDocsResponse),
(SetRoomParticipantRole, Ack),
(BlameBuffer, BlameBufferResponse),
(RejoinRemoteProjects, RejoinRemoteProjectsResponse),
@ -552,6 +555,7 @@ entity_messages!(
UpdateWorktree,
UpdateWorktreeSettings,
LspExtExpandMacro,
LspExtOpenDocs,
AdvertiseContexts,
OpenContext,
CreateContext,