about summary refs log tree commit diff
diff options
context:
space:
mode:
authorDdystopia <alexanderbabak@proton.me>2023-04-25 15:14:35 +0200
committerDdystopia <alexanderbabak@proton.me>2023-05-02 17:06:38 +0200
commitf2d933ecaf5f3d1749c5aa31c3b99c07573c7369 (patch)
treed9c7995bfe527ac4d3fc70d0ca2b4a247c4ac055
parentfc888b583d3dd06ea89a3558350dda9440c82f61 (diff)
downloadrust-f2d933ecaf5f3d1749c5aa31c3b99c07573c7369.tar.gz
rust-f2d933ecaf5f3d1749c5aa31c3b99c07573c7369.zip
Add support for local documentation links alongside web documentation links, pending for `target_dir` path and tests
-rw-r--r--crates/ide/src/doc_links.rs114
-rw-r--r--crates/ide/src/lib.rs2
-rw-r--r--crates/rust-analyzer/src/handlers/request.rs9
-rw-r--r--crates/rust-analyzer/src/lsp_ext.rs2
4 files changed, 82 insertions, 45 deletions
diff --git a/crates/ide/src/doc_links.rs b/crates/ide/src/doc_links.rs
index 8d86c615d44..a291389363b 100644
--- a/crates/ide/src/doc_links.rs
+++ b/crates/ide/src/doc_links.rs
@@ -29,8 +29,16 @@ use crate::{
     FilePosition, Semantics,
 };
 
-/// Weblink to an item's documentation.
-pub(crate) type DocumentationLink = String;
+/// Web and local links to an item's documentation.
+#[derive(Default, Debug, Clone, PartialEq, Eq)]
+pub struct DocumentationLinks {
+    /// The URL to the documentation on docs.rs.
+    /// Could be invalid.
+    pub web_url: Option<String>,
+    /// The URL to the documentation in the local file system.
+    /// Could be invalid.
+    pub local_url: Option<String>,
+}
 
 const MARKDOWN_OPTIONS: Options =
     Options::ENABLE_FOOTNOTES.union(Options::ENABLE_TABLES).union(Options::ENABLE_TASKLISTS);
@@ -119,10 +127,7 @@ pub(crate) fn remove_links(markdown: &str) -> String {
 //
 // | VS Code | **rust-analyzer: Open Docs**
 // |===
-pub(crate) fn external_docs(
-    db: &RootDatabase,
-    position: &FilePosition,
-) -> Option<DocumentationLink> {
+pub(crate) fn external_docs(db: &RootDatabase, position: &FilePosition) -> DocumentationLinks {
     let sema = &Semantics::new(db);
     let file = sema.parse(position.file_id).syntax().clone();
     let token = pick_best_token(file.token_at_offset(position.offset), |kind| match kind {
@@ -130,27 +135,30 @@ pub(crate) fn external_docs(
         T!['('] | T![')'] => 2,
         kind if kind.is_trivia() => 0,
         _ => 1,
-    })?;
+    });
+    let Some(token) = token else { return Default::default() };
     let token = sema.descend_into_macros_single(token);
 
-    let node = token.parent()?;
+    let Some(node) = token.parent() else { return Default::default() };
     let definition = match_ast! {
         match node {
-            ast::NameRef(name_ref) => match NameRefClass::classify(sema, &name_ref)? {
-                NameRefClass::Definition(def) => def,
-                NameRefClass::FieldShorthand { local_ref: _, field_ref } => {
+            ast::NameRef(name_ref) => match NameRefClass::classify(sema, &name_ref) {
+                Some(NameRefClass::Definition(def)) => def,
+                Some(NameRefClass::FieldShorthand { local_ref: _, field_ref }) => {
                     Definition::Field(field_ref)
                 }
+                None => return Default::default(),
             },
-            ast::Name(name) => match NameClass::classify(sema, &name)? {
-                NameClass::Definition(it) | NameClass::ConstReference(it) => it,
-                NameClass::PatFieldShorthand { local_def: _, field_ref } => Definition::Field(field_ref),
+            ast::Name(name) => match NameClass::classify(sema, &name) {
+                Some(NameClass::Definition(it) | NameClass::ConstReference(it)) => it,
+                Some(NameClass::PatFieldShorthand { local_def: _, field_ref }) => Definition::Field(field_ref),
+                None => return Default::default(),
             },
-            _ => return None,
+            _ => return Default::default(),
         }
     };
 
-    get_doc_link(db, definition)
+    return get_doc_links(db, definition);
 }
 
 /// Extracts all links from a given markdown text returning the definition text range, link-text
@@ -308,19 +316,34 @@ fn broken_link_clone_cb(link: BrokenLink<'_>) -> Option<(CowStr<'_>, CowStr<'_>)
 //
 // This should cease to be a problem if RFC2988 (Stable Rustdoc URLs) is implemented
 // https://github.com/rust-lang/rfcs/pull/2988
-fn get_doc_link(db: &RootDatabase, def: Definition) -> Option<String> {
-    let (target, file, frag) = filename_and_frag_for_def(db, def)?;
+fn get_doc_links(db: &RootDatabase, def: Definition) -> DocumentationLinks {
+    let Some((target, file, frag)) = filename_and_frag_for_def(db, def) else { return Default::default(); };
 
-    let mut url = get_doc_base_url(db, target)?;
+    let (mut web_url, mut local_url) = get_doc_base_urls(db, target);
 
     if let Some(path) = mod_path_of_def(db, target) {
-        url = url.join(&path).ok()?;
+        web_url = join_url(web_url, &path);
+        local_url = join_url(local_url, &path);
     }
 
-    url = url.join(&file).ok()?;
-    url.set_fragment(frag.as_deref());
+    web_url = join_url(web_url, &file);
+    local_url = join_url(local_url, &file);
+
+    set_fragment_for_url(web_url.as_mut(), frag.as_deref());
+    set_fragment_for_url(local_url.as_mut(), frag.as_deref());
 
-    Some(url.into())
+    return DocumentationLinks {
+        web_url: web_url.map(|it| it.into()),
+        local_url: local_url.map(|it| it.into()),
+    };
+
+    fn join_url(base_url: Option<Url>, path: &str) -> Option<Url> {
+        base_url.and_then(|url| url.join(path).ok())
+    }
+
+    fn set_fragment_for_url(url: Option<&mut Url>, frag: Option<&str>) {
+        url.map(|url| url.set_fragment(frag));
+    }
 }
 
 fn rewrite_intra_doc_link(
@@ -332,7 +355,7 @@ fn rewrite_intra_doc_link(
     let (link, ns) = parse_intra_doc_link(target);
 
     let resolved = resolve_doc_path_for_def(db, def, link, ns)?;
-    let mut url = get_doc_base_url(db, resolved)?;
+    let mut url = get_doc_base_urls(db, resolved).0?;
 
     let (_, file, frag) = filename_and_frag_for_def(db, resolved)?;
     if let Some(path) = mod_path_of_def(db, resolved) {
@@ -351,7 +374,7 @@ fn rewrite_url_link(db: &RootDatabase, def: Definition, target: &str) -> Option<
         return None;
     }
 
-    let mut url = get_doc_base_url(db, def)?;
+    let mut url = get_doc_base_urls(db, def).0?;
     let (def, file, frag) = filename_and_frag_for_def(db, def)?;
 
     if let Some(path) = mod_path_of_def(db, def) {
@@ -427,18 +450,26 @@ fn map_links<'e>(
 /// https://doc.rust-lang.org/std/iter/trait.Iterator.html#tymethod.next
 /// ^^^^^^^^^^^^^^^^^^^^^^^^^^
 /// ```
-fn get_doc_base_url(db: &RootDatabase, def: Definition) -> Option<Url> {
+fn get_doc_base_urls(db: &RootDatabase, def: Definition) -> (Option<Url>, Option<Url>) {
+    // TODO: get this is from `CargoWorkspace`
+    // TODO: get `CargoWorkspace` from `db`
+    let target_path = "file:///project/root/target";
+    let target_path = Url::parse(target_path).ok();
+    let local_doc_path = target_path.and_then(|url| url.join("doc").ok());
+    debug_assert!(local_doc_path.is_some(), "failed to parse local doc path");
+
     // special case base url of `BuiltinType` to core
     // https://github.com/rust-lang/rust-analyzer/issues/12250
     if let Definition::BuiltinType(..) = def {
-        return Url::parse("https://doc.rust-lang.org/nightly/core/").ok();
+        let weblink = Url::parse("https://doc.rust-lang.org/nightly/core/").ok();
+        return (weblink, local_doc_path);
     };
 
-    let krate = def.krate(db)?;
-    let display_name = krate.display_name(db)?;
+    let Some(krate) = def.krate(db) else { return Default::default() };
+    let Some(display_name) = krate.display_name(db) else { return Default::default() };
     let crate_data = &db.crate_graph()[krate.into()];
     let channel = crate_data.channel.map_or("nightly", ReleaseChannel::as_str);
-    let base = match &crate_data.origin {
+    let (web_base, local_base) = match &crate_data.origin {
         // std and co do not specify `html_root_url` any longer so we gotta handwrite this ourself.
         // FIXME: Use the toolchains channel instead of nightly
         CrateOrigin::Lang(
@@ -447,16 +478,14 @@ fn get_doc_base_url(db: &RootDatabase, def: Definition) -> Option<Url> {
             | LangCrateOrigin::ProcMacro
             | LangCrateOrigin::Std
             | LangCrateOrigin::Test),
-        ) => {
-            format!("https://doc.rust-lang.org/{channel}/{origin}")
-        }
-        CrateOrigin::Lang(_) => return None,
+        ) => (Some(format!("https://doc.rust-lang.org/{channel}/{origin}")), None),
+        CrateOrigin::Lang(_) => return (None, None),
         CrateOrigin::Rustc { name: _ } => {
-            format!("https://doc.rust-lang.org/{channel}/nightly-rustc/")
+            (Some(format!("https://doc.rust-lang.org/{channel}/nightly-rustc/")), None)
         }
         CrateOrigin::Local { repo: _, name: _ } => {
             // FIXME: These should not attempt to link to docs.rs!
-            krate.get_html_root_url(db).or_else(|| {
+            let weblink = krate.get_html_root_url(db).or_else(|| {
                 let version = krate.version(db);
                 // Fallback to docs.rs. This uses `display_name` and can never be
                 // correct, but that's what fallbacks are about.
@@ -468,10 +497,11 @@ fn get_doc_base_url(db: &RootDatabase, def: Definition) -> Option<Url> {
                     krate = display_name,
                     version = version.as_deref().unwrap_or("*")
                 ))
-            })?
+            });
+            (weblink, local_doc_path)
         }
         CrateOrigin::Library { repo: _, name } => {
-            krate.get_html_root_url(db).or_else(|| {
+            let weblink = krate.get_html_root_url(db).or_else(|| {
                 let version = krate.version(db);
                 // Fallback to docs.rs. This uses `display_name` and can never be
                 // correct, but that's what fallbacks are about.
@@ -483,10 +513,14 @@ fn get_doc_base_url(db: &RootDatabase, def: Definition) -> Option<Url> {
                     krate = name,
                     version = version.as_deref().unwrap_or("*")
                 ))
-            })?
+            });
+            (weblink, local_doc_path)
         }
     };
-    Url::parse(&base).ok()?.join(&format!("{display_name}/")).ok()
+    let web_base = web_base
+        .and_then(|it| Url::parse(&it).ok())
+        .and_then(|it| it.join(&format!("{display_name}/")).ok());
+    (web_base, local_base)
 }
 
 /// Get the filename and extension generated for a symbol by rustdoc.
diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs
index 24e2aed65a5..4a4410ad20f 100644
--- a/crates/ide/src/lib.rs
+++ b/crates/ide/src/lib.rs
@@ -471,7 +471,7 @@ impl Analysis {
     pub fn external_docs(
         &self,
         position: FilePosition,
-    ) -> Cancellable<Option<doc_links::DocumentationLink>> {
+    ) -> Cancellable<doc_links::DocumentationLinks> {
         self.with_db(|db| doc_links::external_docs(db, &position))
     }
 
diff --git a/crates/rust-analyzer/src/handlers/request.rs b/crates/rust-analyzer/src/handlers/request.rs
index f25dc74a142..47dd571054b 100644
--- a/crates/rust-analyzer/src/handlers/request.rs
+++ b/crates/rust-analyzer/src/handlers/request.rs
@@ -1535,13 +1535,16 @@ pub(crate) fn handle_semantic_tokens_range(
 pub(crate) fn handle_open_docs(
     snap: GlobalStateSnapshot,
     params: lsp_types::TextDocumentPositionParams,
-) -> Result<Option<lsp_types::Url>> {
+) -> Result<(Option<lsp_types::Url>, Option<lsp_types::Url>)> {
     let _p = profile::span("handle_open_docs");
     let position = from_proto::file_position(&snap, params)?;
 
-    let remote = snap.analysis.external_docs(position)?;
+    let Ok(remote_urls) = snap.analysis.external_docs(position) else { return Ok((None, None)); };
 
-    Ok(remote.and_then(|remote| Url::parse(&remote).ok()))
+    let web_url = remote_urls.web_url.and_then(|it| Url::parse(&it).ok());
+    let local_url = remote_urls.local_url.and_then(|it| Url::parse(&it).ok());
+
+    Ok((web_url, local_url))
 }
 
 pub(crate) fn handle_open_cargo_toml(
diff --git a/crates/rust-analyzer/src/lsp_ext.rs b/crates/rust-analyzer/src/lsp_ext.rs
index 69e7d824680..9c27f6e1c33 100644
--- a/crates/rust-analyzer/src/lsp_ext.rs
+++ b/crates/rust-analyzer/src/lsp_ext.rs
@@ -508,7 +508,7 @@ pub enum ExternalDocs {}
 
 impl Request for ExternalDocs {
     type Params = lsp_types::TextDocumentPositionParams;
-    type Result = Option<lsp_types::Url>;
+    type Result = (Option<lsp_types::Url>, Option<lsp_types::Url>);
     const METHOD: &'static str = "experimental/externalDocs";
 }