@@ -5,6 +5,8 @@ mod tests;
55
66mod intra_doc_links;
77
8+ use std:: ffi:: OsStr ;
9+
810use pulldown_cmark:: { BrokenLink , CowStr , Event , InlineStr , LinkType , Options , Parser , Tag } ;
911use pulldown_cmark_to_cmark:: { cmark_resume_with_options, Options as CMarkOptions } ;
1012use stdx:: format_to;
@@ -29,8 +31,16 @@ use crate::{
2931 FilePosition , Semantics ,
3032} ;
3133
32- /// Weblink to an item's documentation.
33- pub ( crate ) type DocumentationLink = String ;
34+ /// Web and local links to an item's documentation.
35+ #[ derive( Default , Debug , Clone , PartialEq , Eq ) ]
36+ pub struct DocumentationLinks {
37+ /// The URL to the documentation on docs.rs.
38+ /// May not lead anywhere.
39+ pub web_url : Option < String > ,
40+ /// The URL to the documentation in the local file system.
41+ /// May not lead anywhere.
42+ pub local_url : Option < String > ,
43+ }
3444
3545const MARKDOWN_OPTIONS : Options =
3646 Options :: ENABLE_FOOTNOTES . union ( Options :: ENABLE_TABLES ) . union ( Options :: ENABLE_TASKLISTS ) ;
@@ -109,7 +119,7 @@ pub(crate) fn remove_links(markdown: &str) -> String {
109119
110120// Feature: Open Docs
111121//
112- // Retrieve a link to documentation for the given symbol.
122+ // Retrieve a links to documentation for the given symbol.
113123//
114124// The simplest way to use this feature is via the context menu. Right-click on
115125// the selected item. The context menu opens. Select **Open Docs**.
@@ -122,7 +132,9 @@ pub(crate) fn remove_links(markdown: &str) -> String {
122132pub ( crate ) fn external_docs (
123133 db : & RootDatabase ,
124134 position : & FilePosition ,
125- ) -> Option < DocumentationLink > {
135+ target_dir : Option < & OsStr > ,
136+ sysroot : Option < & OsStr > ,
137+ ) -> Option < DocumentationLinks > {
126138 let sema = & Semantics :: new ( db) ;
127139 let file = sema. parse ( position. file_id ) . syntax ( ) . clone ( ) ;
128140 let token = pick_best_token ( file. token_at_offset ( position. offset ) , |kind| match kind {
@@ -146,11 +158,11 @@ pub(crate) fn external_docs(
146158 NameClass :: Definition ( it) | NameClass :: ConstReference ( it) => it,
147159 NameClass :: PatFieldShorthand { local_def: _, field_ref } => Definition :: Field ( field_ref) ,
148160 } ,
149- _ => return None ,
161+ _ => return None
150162 }
151163 } ;
152164
153- get_doc_link ( db, definition)
165+ Some ( get_doc_links ( db, definition, target_dir , sysroot ) )
154166}
155167
156168/// Extracts all links from a given markdown text returning the definition text range, link-text
@@ -308,19 +320,35 @@ fn broken_link_clone_cb(link: BrokenLink<'_>) -> Option<(CowStr<'_>, CowStr<'_>)
308320//
309321// This should cease to be a problem if RFC2988 (Stable Rustdoc URLs) is implemented
310322// https://github.com/rust-lang/rfcs/pull/2988
311- fn get_doc_link ( db : & RootDatabase , def : Definition ) -> Option < String > {
312- let ( target, file, frag) = filename_and_frag_for_def ( db, def) ?;
323+ fn get_doc_links (
324+ db : & RootDatabase ,
325+ def : Definition ,
326+ target_dir : Option < & OsStr > ,
327+ sysroot : Option < & OsStr > ,
328+ ) -> DocumentationLinks {
329+ let join_url = |base_url : Option < Url > , path : & str | -> Option < Url > {
330+ base_url. and_then ( |url| url. join ( path) . ok ( ) )
331+ } ;
332+
333+ let Some ( ( target, file, frag) ) = filename_and_frag_for_def ( db, def) else { return Default :: default ( ) ; } ;
313334
314- let mut url = get_doc_base_url ( db, target) ? ;
335+ let ( mut web_url , mut local_url ) = get_doc_base_urls ( db, target, target_dir , sysroot ) ;
315336
316337 if let Some ( path) = mod_path_of_def ( db, target) {
317- url = url. join ( & path) . ok ( ) ?;
338+ web_url = join_url ( web_url, & path) ;
339+ local_url = join_url ( local_url, & path) ;
318340 }
319341
320- url = url. join ( & file) . ok ( ) ?;
321- url. set_fragment ( frag. as_deref ( ) ) ;
342+ web_url = join_url ( web_url, & file) ;
343+ local_url = join_url ( local_url, & file) ;
344+
345+ web_url. as_mut ( ) . map ( |url| url. set_fragment ( frag. as_deref ( ) ) ) ;
346+ local_url. as_mut ( ) . map ( |url| url. set_fragment ( frag. as_deref ( ) ) ) ;
322347
323- Some ( url. into ( ) )
348+ DocumentationLinks {
349+ web_url : web_url. map ( |it| it. into ( ) ) ,
350+ local_url : local_url. map ( |it| it. into ( ) ) ,
351+ }
324352}
325353
326354fn rewrite_intra_doc_link (
@@ -332,7 +360,7 @@ fn rewrite_intra_doc_link(
332360 let ( link, ns) = parse_intra_doc_link ( target) ;
333361
334362 let resolved = resolve_doc_path_for_def ( db, def, link, ns) ?;
335- let mut url = get_doc_base_url ( db, resolved) ?;
363+ let mut url = get_doc_base_urls ( db, resolved, None , None ) . 0 ?;
336364
337365 let ( _, file, frag) = filename_and_frag_for_def ( db, resolved) ?;
338366 if let Some ( path) = mod_path_of_def ( db, resolved) {
@@ -351,7 +379,7 @@ fn rewrite_url_link(db: &RootDatabase, def: Definition, target: &str) -> Option<
351379 return None ;
352380 }
353381
354- let mut url = get_doc_base_url ( db, def) ?;
382+ let mut url = get_doc_base_urls ( db, def, None , None ) . 0 ?;
355383 let ( def, file, frag) = filename_and_frag_for_def ( db, def) ?;
356384
357385 if let Some ( path) = mod_path_of_def ( db, def) {
@@ -426,19 +454,38 @@ fn map_links<'e>(
426454/// ```ignore
427455/// https://doc.rust-lang.org/std/iter/trait.Iterator.html#tymethod.next
428456/// ^^^^^^^^^^^^^^^^^^^^^^^^^^
457+ /// file:///project/root/target/doc/std/iter/trait.Iterator.html#tymethod.next
458+ /// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
429459/// ```
430- fn get_doc_base_url ( db : & RootDatabase , def : Definition ) -> Option < Url > {
460+ fn get_doc_base_urls (
461+ db : & RootDatabase ,
462+ def : Definition ,
463+ target_dir : Option < & OsStr > ,
464+ sysroot : Option < & OsStr > ,
465+ ) -> ( Option < Url > , Option < Url > ) {
466+ let local_doc = target_dir
467+ . and_then ( |path| path. to_str ( ) )
468+ . and_then ( |path| Url :: parse ( & format ! ( "file:///{path}/" ) ) . ok ( ) )
469+ . and_then ( |it| it. join ( "doc/" ) . ok ( ) ) ;
470+ let system_doc = sysroot
471+ . and_then ( |it| it. to_str ( ) )
472+ . map ( |sysroot| format ! ( "file:///{sysroot}/share/doc/rust/html/" ) )
473+ . and_then ( |it| Url :: parse ( & it) . ok ( ) ) ;
474+
431475 // special case base url of `BuiltinType` to core
432476 // https://github.com/rust-lang/rust-analyzer/issues/12250
433477 if let Definition :: BuiltinType ( ..) = def {
434- return Url :: parse ( "https://doc.rust-lang.org/nightly/core/" ) . ok ( ) ;
478+ let web_link = Url :: parse ( "https://doc.rust-lang.org/nightly/core/" ) . ok ( ) ;
479+ let system_link = system_doc. and_then ( |it| it. join ( "core/" ) . ok ( ) ) ;
480+ return ( web_link, system_link) ;
435481 } ;
436482
437- let krate = def. krate ( db) ? ;
438- let display_name = krate. display_name ( db) ? ;
483+ let Some ( krate) = def. krate ( db) else { return Default :: default ( ) } ;
484+ let Some ( display_name) = krate. display_name ( db) else { return Default :: default ( ) } ;
439485 let crate_data = & db. crate_graph ( ) [ krate. into ( ) ] ;
440486 let channel = crate_data. channel . map_or ( "nightly" , ReleaseChannel :: as_str) ;
441- let base = match & crate_data. origin {
487+
488+ let ( web_base, local_base) = match & crate_data. origin {
442489 // std and co do not specify `html_root_url` any longer so we gotta handwrite this ourself.
443490 // FIXME: Use the toolchains channel instead of nightly
444491 CrateOrigin :: Lang (
@@ -448,15 +495,17 @@ fn get_doc_base_url(db: &RootDatabase, def: Definition) -> Option<Url> {
448495 | LangCrateOrigin :: Std
449496 | LangCrateOrigin :: Test ) ,
450497 ) => {
451- format ! ( "https://doc.rust-lang.org/{channel}/{origin}" )
498+ let system_url = system_doc. and_then ( |it| it. join ( & format ! ( "{origin}" ) ) . ok ( ) ) ;
499+ let web_url = format ! ( "https://doc.rust-lang.org/{channel}/{origin}" ) ;
500+ ( Some ( web_url) , system_url)
452501 }
453- CrateOrigin :: Lang ( _) => return None ,
502+ CrateOrigin :: Lang ( _) => return ( None , None ) ,
454503 CrateOrigin :: Rustc { name : _ } => {
455- format ! ( "https://doc.rust-lang.org/{channel}/nightly-rustc/" )
504+ ( Some ( format ! ( "https://doc.rust-lang.org/{channel}/nightly-rustc/" ) ) , None )
456505 }
457506 CrateOrigin :: Local { repo : _, name : _ } => {
458507 // FIXME: These should not attempt to link to docs.rs!
459- krate. get_html_root_url ( db) . or_else ( || {
508+ let weblink = krate. get_html_root_url ( db) . or_else ( || {
460509 let version = krate. version ( db) ;
461510 // Fallback to docs.rs. This uses `display_name` and can never be
462511 // correct, but that's what fallbacks are about.
@@ -468,10 +517,11 @@ fn get_doc_base_url(db: &RootDatabase, def: Definition) -> Option<Url> {
468517 krate = display_name,
469518 version = version. as_deref( ) . unwrap_or( "*" )
470519 ) )
471- } ) ?
520+ } ) ;
521+ ( weblink, local_doc)
472522 }
473523 CrateOrigin :: Library { repo : _, name } => {
474- krate. get_html_root_url ( db) . or_else ( || {
524+ let weblink = krate. get_html_root_url ( db) . or_else ( || {
475525 let version = krate. version ( db) ;
476526 // Fallback to docs.rs. This uses `display_name` and can never be
477527 // correct, but that's what fallbacks are about.
@@ -483,10 +533,16 @@ fn get_doc_base_url(db: &RootDatabase, def: Definition) -> Option<Url> {
483533 krate = name,
484534 version = version. as_deref( ) . unwrap_or( "*" )
485535 ) )
486- } ) ?
536+ } ) ;
537+ ( weblink, local_doc)
487538 }
488539 } ;
489- Url :: parse ( & base) . ok ( ) ?. join ( & format ! ( "{display_name}/" ) ) . ok ( )
540+ let web_base = web_base
541+ . and_then ( |it| Url :: parse ( & it) . ok ( ) )
542+ . and_then ( |it| it. join ( & format ! ( "{display_name}/" ) ) . ok ( ) ) ;
543+ let local_base = local_base. and_then ( |it| it. join ( & format ! ( "{display_name}/" ) ) . ok ( ) ) ;
544+
545+ ( web_base, local_base)
490546}
491547
492548/// Get the filename and extension generated for a symbol by rustdoc.
0 commit comments