about summary refs log tree commit diff
diff options
context:
space:
mode:
authorZac Pullar-Strecker <zacmps@gmail.com>2020-08-30 20:02:29 +1200
committerZac Pullar-Strecker <zacmps@gmail.com>2020-10-08 14:59:31 +1300
commitbfda0d25834250a3adbcd0d26953a1cdc6662e7f (patch)
tree439fa97a999360cb5fe4602e7ab26d66aa6a3662
parente95e666b106b2f63ab2b350e656c9e8b96441fa7 (diff)
downloadrust-bfda0d25834250a3adbcd0d26953a1cdc6662e7f.tar.gz
rust-bfda0d25834250a3adbcd0d26953a1cdc6662e7f.zip
WIP: Command to open docs under cursor
-rw-r--r--crates/ide/src/lib.rs8
-rw-r--r--crates/ide/src/link_rewrite.rs82
-rw-r--r--crates/rust-analyzer/src/handlers.rs15
-rw-r--r--crates/rust-analyzer/src/lsp_ext.rs28
-rw-r--r--crates/rust-analyzer/src/main_loop.rs1
-rw-r--r--editors/code/package.json9
-rw-r--r--editors/code/src/commands.ts25
-rw-r--r--editors/code/src/lsp_ext.ts11
-rw-r--r--editors/code/src/main.ts1
9 files changed, 176 insertions, 4 deletions
diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs
index 57f3581b6bd..64536959793 100644
--- a/crates/ide/src/lib.rs
+++ b/crates/ide/src/lib.rs
@@ -382,6 +382,14 @@ impl Analysis {
         self.with_db(|db| hover::hover(db, position, links_in_hover, markdown))
     }
 
+    /// Return URL(s) for the documentation of the symbol under the cursor.
+    pub fn get_doc_url(
+        &self,
+        position: FilePosition,
+    ) -> Cancelable<Option<link_rewrite::DocumentationLink>> {
+        self.with_db(|db| link_rewrite::get_doc_url(db, &position))
+    }
+
     /// Computes parameter information for the given call expression.
     pub fn call_info(&self, position: FilePosition) -> Cancelable<Option<CallInfo>> {
         self.with_db(|db| call_info::call_info(db, position))
diff --git a/crates/ide/src/link_rewrite.rs b/crates/ide/src/link_rewrite.rs
index c317a2379b4..80005c06edf 100644
--- a/crates/ide/src/link_rewrite.rs
+++ b/crates/ide/src/link_rewrite.rs
@@ -8,6 +8,16 @@ use pulldown_cmark::{CowStr, Event, LinkType, Options, Parser, Tag};
 use pulldown_cmark_to_cmark::{cmark_with_options, Options as CmarkOptions};
 use url::Url;
 
+use crate::{FilePosition, Semantics};
+use hir::{get_doc_link, resolve_doc_link};
+use ide_db::{
+    defs::{classify_name, classify_name_ref, Definition},
+    RootDatabase,
+};
+use syntax::{ast, match_ast, AstNode, SyntaxKind::*, SyntaxToken, TokenAtOffset, T};
+
+pub type DocumentationLink = String;
+
 /// Rewrite documentation links in markdown to point to an online host (e.g. docs.rs)
 pub fn rewrite_links(db: &RootDatabase, markdown: &str, definition: &Definition) -> String {
     let doc = Parser::new_with_broken_link_callback(
@@ -80,6 +90,37 @@ pub fn remove_links(markdown: &str) -> String {
     out
 }
 
+pub fn get_doc_link<T: Resolvable + Clone>(db: &dyn HirDatabase, definition: &T) -> Option<String> {
+    eprintln!("hir::doc_links::get_doc_link");
+    let module_def = definition.clone().try_into_module_def()?;
+
+    get_doc_link_impl(db, &module_def)
+}
+
+// TODO:
+// BUG: For Option
+// Returns https://doc.rust-lang.org/nightly/core/prelude/v1/enum.Option.html#variant.Some
+// Instead of https://doc.rust-lang.org/nightly/core/option/enum.Option.html
+//
+// BUG: For methods
+// import_map.path_of(ns) fails, is not designed to resolve methods
+fn get_doc_link_impl(db: &dyn HirDatabase, moddef: &ModuleDef) -> Option<String> {
+    eprintln!("get_doc_link_impl: {:#?}", moddef);
+    let ns = ItemInNs::Types(moddef.clone().into());
+
+    let module = moddef.module(db)?;
+    let krate = module.krate();
+    let import_map = db.import_map(krate.into());
+    let base = once(krate.display_name(db).unwrap())
+        .chain(import_map.path_of(ns).unwrap().segments.iter().map(|name| format!("{}", name)))
+        .join("/");
+
+    get_doc_url(db, &krate)
+        .and_then(|url| url.join(&base).ok())
+        .and_then(|url| get_symbol_filename(db, &moddef).as_deref().and_then(|f| url.join(f).ok()))
+        .map(|url| url.into_string())
+}
+
 fn rewrite_intra_doc_link(
     db: &RootDatabase,
     def: Definition,
@@ -138,7 +179,34 @@ fn rewrite_url_link(db: &RootDatabase, def: ModuleDef, target: &str) -> Option<S
         .map(|url| url.into_string())
 }
 
-// Rewrites a markdown document, resolving links using `callback` and additionally striping prefixes/suffixes on link titles.
+// FIXME: This should either be moved, or the module should be renamed.
+/// Retrieve a link to documentation for the given symbol.
+pub fn get_doc_url(db: &RootDatabase, position: &FilePosition) -> Option<DocumentationLink> {
+    let sema = Semantics::new(db);
+    let file = sema.parse(position.file_id).syntax().clone();
+    let token = pick_best(file.token_at_offset(position.offset))?;
+    let token = sema.descend_into_macros(token);
+
+    let node = token.parent();
+    let definition = match_ast! {
+        match node {
+            ast::NameRef(name_ref) => classify_name_ref(&sema, &name_ref).map(|d| d.definition(sema.db)),
+            ast::Name(name) => classify_name(&sema, &name).map(|d| d.definition(sema.db)),
+            _ => None,
+        }
+    };
+
+    match definition? {
+        Definition::Macro(t) => get_doc_link(db, &t),
+        Definition::Field(t) => get_doc_link(db, &t),
+        Definition::ModuleDef(t) => get_doc_link(db, &t),
+        Definition::SelfType(t) => get_doc_link(db, &t),
+        Definition::Local(t) => get_doc_link(db, &t),
+        Definition::TypeParam(t) => get_doc_link(db, &t),
+    }
+}
+
+/// Rewrites a markdown document, applying 'callback' to each link.
 fn map_links<'e>(
     events: impl Iterator<Item = Event<'e>>,
     callback: impl Fn(&str, &str) -> (String, String),
@@ -275,3 +343,15 @@ fn get_symbol_filename(db: &RootDatabase, definition: &ModuleDef) -> Option<Stri
         ModuleDef::Static(s) => format!("static.{}.html", s.name(db)?),
     })
 }
+
+fn pick_best(tokens: TokenAtOffset<SyntaxToken>) -> Option<SyntaxToken> {
+    return tokens.max_by_key(priority);
+    fn priority(n: &SyntaxToken) -> usize {
+        match n.kind() {
+            IDENT | INT_NUMBER => 3,
+            T!['('] | T![')'] => 2,
+            kind if kind.is_trivia() => 0,
+            _ => 1,
+        }
+    }
+}
diff --git a/crates/rust-analyzer/src/handlers.rs b/crates/rust-analyzer/src/handlers.rs
index 468655f9c39..ec8c8fecdd6 100644
--- a/crates/rust-analyzer/src/handlers.rs
+++ b/crates/rust-analyzer/src/handlers.rs
@@ -34,7 +34,7 @@ use crate::{
     config::RustfmtConfig,
     from_json, from_proto,
     global_state::{GlobalState, GlobalStateSnapshot},
-    lsp_ext::{self, InlayHint, InlayHintsParams},
+    lsp_ext::{self, DocumentationLink, InlayHint, InlayHintsParams, OpenDocsParams},
     to_proto, LspError, Result,
 };
 
@@ -1310,6 +1310,19 @@ pub(crate) fn handle_semantic_tokens_range(
     Ok(Some(semantic_tokens.into()))
 }
 
+pub(crate) fn handle_open_docs(
+    snap: GlobalStateSnapshot,
+    params: OpenDocsParams,
+) -> Result<DocumentationLink> {
+    let _p = profile::span("handle_open_docs");
+    let position = from_proto::file_position(&snap, params.position)?;
+
+    // FIXME: Propogate or ignore this error instead of panicking.
+    let remote = snap.analysis.get_doc_url(position)?.unwrap();
+
+    Ok(DocumentationLink { remote })
+}
+
 fn implementation_title(count: usize) -> String {
     if count == 1 {
         "1 implementation".into()
diff --git a/crates/rust-analyzer/src/lsp_ext.rs b/crates/rust-analyzer/src/lsp_ext.rs
index fee0bb69c70..83a20802f65 100644
--- a/crates/rust-analyzer/src/lsp_ext.rs
+++ b/crates/rust-analyzer/src/lsp_ext.rs
@@ -347,3 +347,31 @@ pub struct CommandLink {
     #[serde(skip_serializing_if = "Option::is_none")]
     pub tooltip: Option<String>,
 }
+
+pub enum OpenDocs {}
+
+impl Request for OpenDocs {
+    type Params = OpenDocsParams;
+    type Result = DocumentationLink;
+    const METHOD: &'static str = "rust-analyzer/openDocs";
+}
+
+#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct OpenDocsParams {
+    // TODO: I don't know the difference between these two methods of passing position.
+    #[serde(flatten)]
+    pub position: lsp_types::TextDocumentPositionParams,
+    // pub textDocument: lsp_types::TextDocumentIdentifier,
+    // pub position: lsp_types::Position,
+}
+
+#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct DocumentationLink {
+    pub remote: String, // TODO: Better API?
+                        // #[serde(skip_serializing_if = "Option::is_none")]
+                        // pub remote: Option<String>,
+                        // #[serde(skip_serializing_if = "Option::is_none")]
+                        // pub local: Option<String>
+}
diff --git a/crates/rust-analyzer/src/main_loop.rs b/crates/rust-analyzer/src/main_loop.rs
index 4b7ac8224ef..75f85011d9e 100644
--- a/crates/rust-analyzer/src/main_loop.rs
+++ b/crates/rust-analyzer/src/main_loop.rs
@@ -384,6 +384,7 @@ impl GlobalState {
             .on::<lsp_ext::CodeActionRequest>(handlers::handle_code_action)?
             .on::<lsp_ext::ResolveCodeActionRequest>(handlers::handle_resolve_code_action)?
             .on::<lsp_ext::HoverRequest>(handlers::handle_hover)?
+            .on::<lsp_ext::OpenDocs>(handlers::handle_open_docs)?
             .on::<lsp_types::request::OnTypeFormatting>(handlers::handle_on_type_formatting)?
             .on::<lsp_types::request::DocumentSymbolRequest>(handlers::handle_document_symbol)?
             .on::<lsp_types::request::WorkspaceSymbol>(handlers::handle_workspace_symbol)?
diff --git a/editors/code/package.json b/editors/code/package.json
index 6a712a8a82f..4bd3117fc80 100644
--- a/editors/code/package.json
+++ b/editors/code/package.json
@@ -182,6 +182,11 @@
                 "command": "rust-analyzer.toggleInlayHints",
                 "title": "Toggle inlay hints",
                 "category": "Rust Analyzer"
+            },
+            {
+                "command": "rust-analyzer.openDocs",
+                "title": "Open docs under cursor",
+                "category": "Rust Analyzer"
             }
         ],
         "keybindings": [
@@ -1044,6 +1049,10 @@
                 {
                     "command": "rust-analyzer.toggleInlayHints",
                     "when": "inRustProject"
+                },
+                {
+                    "command": "rust-analyzer.openDocs",
+                    "when": "inRustProject"
                 }
             ]
         }
diff --git a/editors/code/src/commands.ts b/editors/code/src/commands.ts
index 1a90f1b7d9a..b22cd450b07 100644
--- a/editors/code/src/commands.ts
+++ b/editors/code/src/commands.ts
@@ -419,10 +419,31 @@ export function gotoLocation(ctx: Ctx): Cmd {
     };
 }
 
+export function openDocs(ctx: Ctx): Cmd {
+    return async () => {
+        console.log("running openDocs");
+
+        const client = ctx.client;
+        const editor = vscode.window.activeTextEditor;
+        if (!editor || !client) {
+            console.log("not yet ready");
+            return
+        };
+
+        const position = editor.selection.active;
+        const textDocument = { uri: editor.document.uri.toString() };
+
+        const doclink = await client.sendRequest(ra.openDocs, { position, textDocument });
+
+        vscode.commands.executeCommand("vscode.open", vscode.Uri.parse(doclink.remote));
+    };
+
+}
+
 export function resolveCodeAction(ctx: Ctx): Cmd {
     const client = ctx.client;
-    return async (params: ra.ResolveCodeActionParams) => {
-        const item: lc.WorkspaceEdit = await client.sendRequest(ra.resolveCodeAction, params);
+    return async () => {
+        const item: lc.WorkspaceEdit = await client.sendRequest(ra.resolveCodeAction, null);
         if (!item) {
             return;
         }
diff --git a/editors/code/src/lsp_ext.ts b/editors/code/src/lsp_ext.ts
index f286b68a685..569e747bd47 100644
--- a/editors/code/src/lsp_ext.ts
+++ b/editors/code/src/lsp_ext.ts
@@ -118,3 +118,14 @@ export interface CommandLinkGroup {
     title?: string;
     commands: CommandLink[];
 }
+
+export interface DocumentationLink {
+    remote: string;
+}
+
+export interface OpenDocsParams {
+    textDocument: lc.TextDocumentIdentifier;
+    position: lc.Position;
+}
+
+export const openDocs = new lc.RequestType<OpenDocsParams, DocumentationLink, void>('rust-analyzer/openDocs');
diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts
index 2896d90ac94..09543e348a8 100644
--- a/editors/code/src/main.ts
+++ b/editors/code/src/main.ts
@@ -110,6 +110,7 @@ async function tryActivate(context: vscode.ExtensionContext) {
     ctx.registerCommand('run', commands.run);
     ctx.registerCommand('debug', commands.debug);
     ctx.registerCommand('newDebugConfig', commands.newDebugConfig);
+    ctx.registerCommand('openDocs', commands.openDocs);
 
     defaultOnEnter.dispose();
     ctx.registerCommand('onEnter', commands.onEnter);