Skip to content

Commit bfda0d2

Browse files
committed
WIP: Command to open docs under cursor
1 parent e95e666 commit bfda0d2

File tree

9 files changed

+176
-4
lines changed

9 files changed

+176
-4
lines changed

crates/ide/src/lib.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,14 @@ impl Analysis {
382382
self.with_db(|db| hover::hover(db, position, links_in_hover, markdown))
383383
}
384384

385+
/// Return URL(s) for the documentation of the symbol under the cursor.
386+
pub fn get_doc_url(
387+
&self,
388+
position: FilePosition,
389+
) -> Cancelable<Option<link_rewrite::DocumentationLink>> {
390+
self.with_db(|db| link_rewrite::get_doc_url(db, &position))
391+
}
392+
385393
/// Computes parameter information for the given call expression.
386394
pub fn call_info(&self, position: FilePosition) -> Cancelable<Option<CallInfo>> {
387395
self.with_db(|db| call_info::call_info(db, position))

crates/ide/src/link_rewrite.rs

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,16 @@ use pulldown_cmark::{CowStr, Event, LinkType, Options, Parser, Tag};
88
use pulldown_cmark_to_cmark::{cmark_with_options, Options as CmarkOptions};
99
use url::Url;
1010

11+
use crate::{FilePosition, Semantics};
12+
use hir::{get_doc_link, resolve_doc_link};
13+
use ide_db::{
14+
defs::{classify_name, classify_name_ref, Definition},
15+
RootDatabase,
16+
};
17+
use syntax::{ast, match_ast, AstNode, SyntaxKind::*, SyntaxToken, TokenAtOffset, T};
18+
19+
pub type DocumentationLink = String;
20+
1121
/// Rewrite documentation links in markdown to point to an online host (e.g. docs.rs)
1222
pub fn rewrite_links(db: &RootDatabase, markdown: &str, definition: &Definition) -> String {
1323
let doc = Parser::new_with_broken_link_callback(
@@ -80,6 +90,37 @@ pub fn remove_links(markdown: &str) -> String {
8090
out
8191
}
8292

93+
pub fn get_doc_link<T: Resolvable + Clone>(db: &dyn HirDatabase, definition: &T) -> Option<String> {
94+
eprintln!("hir::doc_links::get_doc_link");
95+
let module_def = definition.clone().try_into_module_def()?;
96+
97+
get_doc_link_impl(db, &module_def)
98+
}
99+
100+
// TODO:
101+
// BUG: For Option
102+
// Returns https://doc.rust-lang.org/nightly/core/prelude/v1/enum.Option.html#variant.Some
103+
// Instead of https://doc.rust-lang.org/nightly/core/option/enum.Option.html
104+
//
105+
// BUG: For methods
106+
// import_map.path_of(ns) fails, is not designed to resolve methods
107+
fn get_doc_link_impl(db: &dyn HirDatabase, moddef: &ModuleDef) -> Option<String> {
108+
eprintln!("get_doc_link_impl: {:#?}", moddef);
109+
let ns = ItemInNs::Types(moddef.clone().into());
110+
111+
let module = moddef.module(db)?;
112+
let krate = module.krate();
113+
let import_map = db.import_map(krate.into());
114+
let base = once(krate.display_name(db).unwrap())
115+
.chain(import_map.path_of(ns).unwrap().segments.iter().map(|name| format!("{}", name)))
116+
.join("/");
117+
118+
get_doc_url(db, &krate)
119+
.and_then(|url| url.join(&base).ok())
120+
.and_then(|url| get_symbol_filename(db, &moddef).as_deref().and_then(|f| url.join(f).ok()))
121+
.map(|url| url.into_string())
122+
}
123+
83124
fn rewrite_intra_doc_link(
84125
db: &RootDatabase,
85126
def: Definition,
@@ -138,7 +179,34 @@ fn rewrite_url_link(db: &RootDatabase, def: ModuleDef, target: &str) -> Option<S
138179
.map(|url| url.into_string())
139180
}
140181

141-
// Rewrites a markdown document, resolving links using `callback` and additionally striping prefixes/suffixes on link titles.
182+
// FIXME: This should either be moved, or the module should be renamed.
183+
/// Retrieve a link to documentation for the given symbol.
184+
pub fn get_doc_url(db: &RootDatabase, position: &FilePosition) -> Option<DocumentationLink> {
185+
let sema = Semantics::new(db);
186+
let file = sema.parse(position.file_id).syntax().clone();
187+
let token = pick_best(file.token_at_offset(position.offset))?;
188+
let token = sema.descend_into_macros(token);
189+
190+
let node = token.parent();
191+
let definition = match_ast! {
192+
match node {
193+
ast::NameRef(name_ref) => classify_name_ref(&sema, &name_ref).map(|d| d.definition(sema.db)),
194+
ast::Name(name) => classify_name(&sema, &name).map(|d| d.definition(sema.db)),
195+
_ => None,
196+
}
197+
};
198+
199+
match definition? {
200+
Definition::Macro(t) => get_doc_link(db, &t),
201+
Definition::Field(t) => get_doc_link(db, &t),
202+
Definition::ModuleDef(t) => get_doc_link(db, &t),
203+
Definition::SelfType(t) => get_doc_link(db, &t),
204+
Definition::Local(t) => get_doc_link(db, &t),
205+
Definition::TypeParam(t) => get_doc_link(db, &t),
206+
}
207+
}
208+
209+
/// Rewrites a markdown document, applying 'callback' to each link.
142210
fn map_links<'e>(
143211
events: impl Iterator<Item = Event<'e>>,
144212
callback: impl Fn(&str, &str) -> (String, String),
@@ -275,3 +343,15 @@ fn get_symbol_filename(db: &RootDatabase, definition: &ModuleDef) -> Option<Stri
275343
ModuleDef::Static(s) => format!("static.{}.html", s.name(db)?),
276344
})
277345
}
346+
347+
fn pick_best(tokens: TokenAtOffset<SyntaxToken>) -> Option<SyntaxToken> {
348+
return tokens.max_by_key(priority);
349+
fn priority(n: &SyntaxToken) -> usize {
350+
match n.kind() {
351+
IDENT | INT_NUMBER => 3,
352+
T!['('] | T![')'] => 2,
353+
kind if kind.is_trivia() => 0,
354+
_ => 1,
355+
}
356+
}
357+
}

crates/rust-analyzer/src/handlers.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ use crate::{
3434
config::RustfmtConfig,
3535
from_json, from_proto,
3636
global_state::{GlobalState, GlobalStateSnapshot},
37-
lsp_ext::{self, InlayHint, InlayHintsParams},
37+
lsp_ext::{self, DocumentationLink, InlayHint, InlayHintsParams, OpenDocsParams},
3838
to_proto, LspError, Result,
3939
};
4040

@@ -1310,6 +1310,19 @@ pub(crate) fn handle_semantic_tokens_range(
13101310
Ok(Some(semantic_tokens.into()))
13111311
}
13121312

1313+
pub(crate) fn handle_open_docs(
1314+
snap: GlobalStateSnapshot,
1315+
params: OpenDocsParams,
1316+
) -> Result<DocumentationLink> {
1317+
let _p = profile::span("handle_open_docs");
1318+
let position = from_proto::file_position(&snap, params.position)?;
1319+
1320+
// FIXME: Propogate or ignore this error instead of panicking.
1321+
let remote = snap.analysis.get_doc_url(position)?.unwrap();
1322+
1323+
Ok(DocumentationLink { remote })
1324+
}
1325+
13131326
fn implementation_title(count: usize) -> String {
13141327
if count == 1 {
13151328
"1 implementation".into()

crates/rust-analyzer/src/lsp_ext.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,3 +347,31 @@ pub struct CommandLink {
347347
#[serde(skip_serializing_if = "Option::is_none")]
348348
pub tooltip: Option<String>,
349349
}
350+
351+
pub enum OpenDocs {}
352+
353+
impl Request for OpenDocs {
354+
type Params = OpenDocsParams;
355+
type Result = DocumentationLink;
356+
const METHOD: &'static str = "rust-analyzer/openDocs";
357+
}
358+
359+
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
360+
#[serde(rename_all = "camelCase")]
361+
pub struct OpenDocsParams {
362+
// TODO: I don't know the difference between these two methods of passing position.
363+
#[serde(flatten)]
364+
pub position: lsp_types::TextDocumentPositionParams,
365+
// pub textDocument: lsp_types::TextDocumentIdentifier,
366+
// pub position: lsp_types::Position,
367+
}
368+
369+
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
370+
#[serde(rename_all = "camelCase")]
371+
pub struct DocumentationLink {
372+
pub remote: String, // TODO: Better API?
373+
// #[serde(skip_serializing_if = "Option::is_none")]
374+
// pub remote: Option<String>,
375+
// #[serde(skip_serializing_if = "Option::is_none")]
376+
// pub local: Option<String>
377+
}

crates/rust-analyzer/src/main_loop.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,7 @@ impl GlobalState {
384384
.on::<lsp_ext::CodeActionRequest>(handlers::handle_code_action)?
385385
.on::<lsp_ext::ResolveCodeActionRequest>(handlers::handle_resolve_code_action)?
386386
.on::<lsp_ext::HoverRequest>(handlers::handle_hover)?
387+
.on::<lsp_ext::OpenDocs>(handlers::handle_open_docs)?
387388
.on::<lsp_types::request::OnTypeFormatting>(handlers::handle_on_type_formatting)?
388389
.on::<lsp_types::request::DocumentSymbolRequest>(handlers::handle_document_symbol)?
389390
.on::<lsp_types::request::WorkspaceSymbol>(handlers::handle_workspace_symbol)?

editors/code/package.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,11 @@
182182
"command": "rust-analyzer.toggleInlayHints",
183183
"title": "Toggle inlay hints",
184184
"category": "Rust Analyzer"
185+
},
186+
{
187+
"command": "rust-analyzer.openDocs",
188+
"title": "Open docs under cursor",
189+
"category": "Rust Analyzer"
185190
}
186191
],
187192
"keybindings": [
@@ -1044,6 +1049,10 @@
10441049
{
10451050
"command": "rust-analyzer.toggleInlayHints",
10461051
"when": "inRustProject"
1052+
},
1053+
{
1054+
"command": "rust-analyzer.openDocs",
1055+
"when": "inRustProject"
10471056
}
10481057
]
10491058
}

editors/code/src/commands.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -419,10 +419,31 @@ export function gotoLocation(ctx: Ctx): Cmd {
419419
};
420420
}
421421

422+
export function openDocs(ctx: Ctx): Cmd {
423+
return async () => {
424+
console.log("running openDocs");
425+
426+
const client = ctx.client;
427+
const editor = vscode.window.activeTextEditor;
428+
if (!editor || !client) {
429+
console.log("not yet ready");
430+
return
431+
};
432+
433+
const position = editor.selection.active;
434+
const textDocument = { uri: editor.document.uri.toString() };
435+
436+
const doclink = await client.sendRequest(ra.openDocs, { position, textDocument });
437+
438+
vscode.commands.executeCommand("vscode.open", vscode.Uri.parse(doclink.remote));
439+
};
440+
441+
}
442+
422443
export function resolveCodeAction(ctx: Ctx): Cmd {
423444
const client = ctx.client;
424-
return async (params: ra.ResolveCodeActionParams) => {
425-
const item: lc.WorkspaceEdit = await client.sendRequest(ra.resolveCodeAction, params);
445+
return async () => {
446+
const item: lc.WorkspaceEdit = await client.sendRequest(ra.resolveCodeAction, null);
426447
if (!item) {
427448
return;
428449
}

editors/code/src/lsp_ext.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,3 +118,14 @@ export interface CommandLinkGroup {
118118
title?: string;
119119
commands: CommandLink[];
120120
}
121+
122+
export interface DocumentationLink {
123+
remote: string;
124+
}
125+
126+
export interface OpenDocsParams {
127+
textDocument: lc.TextDocumentIdentifier;
128+
position: lc.Position;
129+
}
130+
131+
export const openDocs = new lc.RequestType<OpenDocsParams, DocumentationLink, void>('rust-analyzer/openDocs');

editors/code/src/main.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ async function tryActivate(context: vscode.ExtensionContext) {
110110
ctx.registerCommand('run', commands.run);
111111
ctx.registerCommand('debug', commands.debug);
112112
ctx.registerCommand('newDebugConfig', commands.newDebugConfig);
113+
ctx.registerCommand('openDocs', commands.openDocs);
113114

114115
defaultOnEnter.dispose();
115116
ctx.registerCommand('onEnter', commands.onEnter);

0 commit comments

Comments
 (0)