@@ -29,8 +29,16 @@ use crate::{
2929 FilePosition , Semantics ,
3030} ;
3131
32- /// Weblink to an item's documentation.
33- pub ( crate ) type DocumentationLink = String ;
32+ /// Web and local links to an item's documentation.
33+ #[ derive( Default , Debug , Clone , PartialEq , Eq ) ]
34+ pub struct DocumentationLinks {
35+ /// The URL to the documentation on docs.rs.
36+ /// Could be invalid.
37+ pub web_url : Option < String > ,
38+ /// The URL to the documentation in the local file system.
39+ /// Could be invalid.
40+ pub local_url : Option < String > ,
41+ }
3442
3543const MARKDOWN_OPTIONS : Options =
3644 Options :: ENABLE_FOOTNOTES . union ( Options :: ENABLE_TABLES ) . union ( Options :: ENABLE_TASKLISTS ) ;
@@ -119,38 +127,38 @@ pub(crate) fn remove_links(markdown: &str) -> String {
119127//
120128// | VS Code | **rust-analyzer: Open Docs**
121129// |===
122- pub ( crate ) fn external_docs (
123- db : & RootDatabase ,
124- position : & FilePosition ,
125- ) -> Option < DocumentationLink > {
130+ pub ( crate ) fn external_docs ( db : & RootDatabase , position : & FilePosition ) -> DocumentationLinks {
126131 let sema = & Semantics :: new ( db) ;
127132 let file = sema. parse ( position. file_id ) . syntax ( ) . clone ( ) ;
128133 let token = pick_best_token ( file. token_at_offset ( position. offset ) , |kind| match kind {
129134 IDENT | INT_NUMBER | T ! [ self ] => 3 ,
130135 T ! [ '(' ] | T ! [ ')' ] => 2 ,
131136 kind if kind. is_trivia ( ) => 0 ,
132137 _ => 1 ,
133- } ) ?;
138+ } ) ;
139+ let Some ( token) = token else { return Default :: default ( ) } ;
134140 let token = sema. descend_into_macros_single ( token) ;
135141
136- let node = token. parent ( ) ? ;
142+ let Some ( node) = token. parent ( ) else { return Default :: default ( ) } ;
137143 let definition = match_ast ! {
138144 match node {
139- ast:: NameRef ( name_ref) => match NameRefClass :: classify( sema, & name_ref) ? {
140- NameRefClass :: Definition ( def) => def,
141- NameRefClass :: FieldShorthand { local_ref: _, field_ref } => {
145+ ast:: NameRef ( name_ref) => match NameRefClass :: classify( sema, & name_ref) {
146+ Some ( NameRefClass :: Definition ( def) ) => def,
147+ Some ( NameRefClass :: FieldShorthand { local_ref: _, field_ref } ) => {
142148 Definition :: Field ( field_ref)
143149 }
150+ None => return Default :: default ( ) ,
144151 } ,
145- ast:: Name ( name) => match NameClass :: classify( sema, & name) ? {
146- NameClass :: Definition ( it) | NameClass :: ConstReference ( it) => it,
147- NameClass :: PatFieldShorthand { local_def: _, field_ref } => Definition :: Field ( field_ref) ,
152+ ast:: Name ( name) => match NameClass :: classify( sema, & name) {
153+ Some ( NameClass :: Definition ( it) | NameClass :: ConstReference ( it) ) => it,
154+ Some ( NameClass :: PatFieldShorthand { local_def: _, field_ref } ) => Definition :: Field ( field_ref) ,
155+ None => return Default :: default ( ) ,
148156 } ,
149- _ => return None ,
157+ _ => return Default :: default ( ) ,
150158 }
151159 } ;
152160
153- get_doc_link ( db, definition)
161+ return get_doc_links ( db, definition) ;
154162}
155163
156164/// 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<'_>)
308316//
309317// This should cease to be a problem if RFC2988 (Stable Rustdoc URLs) is implemented
310318// 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) ? ;
319+ fn get_doc_links ( db : & RootDatabase , def : Definition ) -> DocumentationLinks {
320+ let Some ( ( target, file, frag) ) = filename_and_frag_for_def ( db, def) else { return Default :: default ( ) ; } ;
313321
314- let mut url = get_doc_base_url ( db, target) ? ;
322+ let ( mut web_url , mut local_url ) = get_doc_base_urls ( db, target) ;
315323
316324 if let Some ( path) = mod_path_of_def ( db, target) {
317- url = url. join ( & path) . ok ( ) ?;
325+ web_url = join_url ( web_url, & path) ;
326+ local_url = join_url ( local_url, & path) ;
318327 }
319328
320- url = url. join ( & file) . ok ( ) ?;
321- url. set_fragment ( frag. as_deref ( ) ) ;
329+ web_url = join_url ( web_url, & file) ;
330+ local_url = join_url ( local_url, & file) ;
331+
332+ set_fragment_for_url ( web_url. as_mut ( ) , frag. as_deref ( ) ) ;
333+ set_fragment_for_url ( local_url. as_mut ( ) , frag. as_deref ( ) ) ;
322334
323- Some ( url. into ( ) )
335+ return DocumentationLinks {
336+ web_url : web_url. map ( |it| it. into ( ) ) ,
337+ local_url : local_url. map ( |it| it. into ( ) ) ,
338+ } ;
339+
340+ fn join_url ( base_url : Option < Url > , path : & str ) -> Option < Url > {
341+ base_url. and_then ( |url| url. join ( path) . ok ( ) )
342+ }
343+
344+ fn set_fragment_for_url ( url : Option < & mut Url > , frag : Option < & str > ) {
345+ url. map ( |url| url. set_fragment ( frag) ) ;
346+ }
324347}
325348
326349fn rewrite_intra_doc_link (
@@ -332,7 +355,7 @@ fn rewrite_intra_doc_link(
332355 let ( link, ns) = parse_intra_doc_link ( target) ;
333356
334357 let resolved = resolve_doc_path_for_def ( db, def, link, ns) ?;
335- let mut url = get_doc_base_url ( db, resolved) ?;
358+ let mut url = get_doc_base_urls ( db, resolved) . 0 ?;
336359
337360 let ( _, file, frag) = filename_and_frag_for_def ( db, resolved) ?;
338361 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<
351374 return None ;
352375 }
353376
354- let mut url = get_doc_base_url ( db, def) ?;
377+ let mut url = get_doc_base_urls ( db, def) . 0 ?;
355378 let ( def, file, frag) = filename_and_frag_for_def ( db, def) ?;
356379
357380 if let Some ( path) = mod_path_of_def ( db, def) {
@@ -427,18 +450,26 @@ fn map_links<'e>(
427450/// https://doc.rust-lang.org/std/iter/trait.Iterator.html#tymethod.next
428451/// ^^^^^^^^^^^^^^^^^^^^^^^^^^
429452/// ```
430- fn get_doc_base_url ( db : & RootDatabase , def : Definition ) -> Option < Url > {
453+ fn get_doc_base_urls ( db : & RootDatabase , def : Definition ) -> ( Option < Url > , Option < Url > ) {
454+ // TODO: get this is from `CargoWorkspace`
455+ // TODO: get `CargoWorkspace` from `db`
456+ let target_path = "file:///project/root/target" ;
457+ let target_path = Url :: parse ( target_path) . ok ( ) ;
458+ let local_doc_path = target_path. and_then ( |url| url. join ( "doc" ) . ok ( ) ) ;
459+ debug_assert ! ( local_doc_path. is_some( ) , "failed to parse local doc path" ) ;
460+
431461 // special case base url of `BuiltinType` to core
432462 // https://github.com/rust-lang/rust-analyzer/issues/12250
433463 if let Definition :: BuiltinType ( ..) = def {
434- return Url :: parse ( "https://doc.rust-lang.org/nightly/core/" ) . ok ( ) ;
464+ let weblink = Url :: parse ( "https://doc.rust-lang.org/nightly/core/" ) . ok ( ) ;
465+ return ( weblink, local_doc_path) ;
435466 } ;
436467
437- let krate = def. krate ( db) ? ;
438- let display_name = krate. display_name ( db) ? ;
468+ let Some ( krate) = def. krate ( db) else { return Default :: default ( ) } ;
469+ let Some ( display_name) = krate. display_name ( db) else { return Default :: default ( ) } ;
439470 let crate_data = & db. crate_graph ( ) [ krate. into ( ) ] ;
440471 let channel = crate_data. channel . map_or ( "nightly" , ReleaseChannel :: as_str) ;
441- let base = match & crate_data. origin {
472+ let ( web_base , local_base ) = match & crate_data. origin {
442473 // std and co do not specify `html_root_url` any longer so we gotta handwrite this ourself.
443474 // FIXME: Use the toolchains channel instead of nightly
444475 CrateOrigin :: Lang (
@@ -447,16 +478,14 @@ fn get_doc_base_url(db: &RootDatabase, def: Definition) -> Option<Url> {
447478 | LangCrateOrigin :: ProcMacro
448479 | LangCrateOrigin :: Std
449480 | LangCrateOrigin :: Test ) ,
450- ) => {
451- format ! ( "https://doc.rust-lang.org/{channel}/{origin}" )
452- }
453- CrateOrigin :: Lang ( _) => return None ,
481+ ) => ( Some ( format ! ( "https://doc.rust-lang.org/{channel}/{origin}" ) ) , None ) ,
482+ CrateOrigin :: Lang ( _) => return ( None , None ) ,
454483 CrateOrigin :: Rustc { name : _ } => {
455- format ! ( "https://doc.rust-lang.org/{channel}/nightly-rustc/" )
484+ ( Some ( format ! ( "https://doc.rust-lang.org/{channel}/nightly-rustc/" ) ) , None )
456485 }
457486 CrateOrigin :: Local { repo : _, name : _ } => {
458487 // FIXME: These should not attempt to link to docs.rs!
459- krate. get_html_root_url ( db) . or_else ( || {
488+ let weblink = krate. get_html_root_url ( db) . or_else ( || {
460489 let version = krate. version ( db) ;
461490 // Fallback to docs.rs. This uses `display_name` and can never be
462491 // correct, but that's what fallbacks are about.
@@ -468,10 +497,11 @@ fn get_doc_base_url(db: &RootDatabase, def: Definition) -> Option<Url> {
468497 krate = display_name,
469498 version = version. as_deref( ) . unwrap_or( "*" )
470499 ) )
471- } ) ?
500+ } ) ;
501+ ( weblink, local_doc_path)
472502 }
473503 CrateOrigin :: Library { repo : _, name } => {
474- krate. get_html_root_url ( db) . or_else ( || {
504+ let weblink = krate. get_html_root_url ( db) . or_else ( || {
475505 let version = krate. version ( db) ;
476506 // Fallback to docs.rs. This uses `display_name` and can never be
477507 // correct, but that's what fallbacks are about.
@@ -483,10 +513,14 @@ fn get_doc_base_url(db: &RootDatabase, def: Definition) -> Option<Url> {
483513 krate = name,
484514 version = version. as_deref( ) . unwrap_or( "*" )
485515 ) )
486- } ) ?
516+ } ) ;
517+ ( weblink, local_doc_path)
487518 }
488519 } ;
489- Url :: parse ( & base) . ok ( ) ?. join ( & format ! ( "{display_name}/" ) ) . ok ( )
520+ let web_base = web_base
521+ . and_then ( |it| Url :: parse ( & it) . ok ( ) )
522+ . and_then ( |it| it. join ( & format ! ( "{display_name}/" ) ) . ok ( ) ) ;
523+ ( web_base, local_base)
490524}
491525
492526/// Get the filename and extension generated for a symbol by rustdoc.
0 commit comments