mirror of
https://github.com/rust-lang/rust-analyzer
synced 2025-01-13 21:54:42 +00:00
Working intra-doc-links
This commit is contained in:
parent
8f56e7c3b1
commit
5f52a516de
5 changed files with 119 additions and 8 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -1270,6 +1270,7 @@ dependencies = [
|
||||||
"insta",
|
"insta",
|
||||||
"itertools",
|
"itertools",
|
||||||
"log",
|
"log",
|
||||||
|
"maplit",
|
||||||
"ra_assists",
|
"ra_assists",
|
||||||
"ra_cfg",
|
"ra_cfg",
|
||||||
"ra_db",
|
"ra_db",
|
||||||
|
@ -1278,6 +1279,7 @@ dependencies = [
|
||||||
"ra_hir_def",
|
"ra_hir_def",
|
||||||
"ra_hir_expand",
|
"ra_hir_expand",
|
||||||
"ra_ide_db",
|
"ra_ide_db",
|
||||||
|
"ra_parser",
|
||||||
"ra_prof",
|
"ra_prof",
|
||||||
"ra_project_model",
|
"ra_project_model",
|
||||||
"ra_ssr",
|
"ra_ssr",
|
||||||
|
|
|
@ -19,6 +19,7 @@ rustc-hash = "1.1.0"
|
||||||
rand = { version = "0.7.3", features = ["small_rng"] }
|
rand = { version = "0.7.3", features = ["small_rng"] }
|
||||||
comrak = "0.7.0"
|
comrak = "0.7.0"
|
||||||
url = "*"
|
url = "*"
|
||||||
|
maplit = "*"
|
||||||
|
|
||||||
stdx = { path = "../stdx" }
|
stdx = { path = "../stdx" }
|
||||||
|
|
||||||
|
@ -36,6 +37,7 @@ ra_project_model = { path = "../ra_project_model" }
|
||||||
ra_hir_def = { path = "../ra_hir_def" }
|
ra_hir_def = { path = "../ra_hir_def" }
|
||||||
ra_tt = { path = "../ra_tt" }
|
ra_tt = { path = "../ra_tt" }
|
||||||
ra_hir_expand = { path = "../ra_hir_expand" }
|
ra_hir_expand = { path = "../ra_hir_expand" }
|
||||||
|
ra_parser = { path = "../ra_parser" }
|
||||||
|
|
||||||
# ra_ide should depend only on the top-level `hir` package. if you need
|
# ra_ide should depend only on the top-level `hir` package. if you need
|
||||||
# something from some `hir_xxx` subpackage, reexport the API via `hir`.
|
# something from some `hir_xxx` subpackage, reexport the API via `hir`.
|
||||||
|
|
|
@ -4,7 +4,7 @@ use std::sync::Arc;
|
||||||
|
|
||||||
use hir::{
|
use hir::{
|
||||||
Adt, AsAssocItem, AssocItemContainer, FieldSource, HasSource, HirDisplay, ModuleDef,
|
Adt, AsAssocItem, AssocItemContainer, FieldSource, HasSource, HirDisplay, ModuleDef,
|
||||||
ModuleSource, Semantics, Documentation, AttrDef, Crate
|
ModuleSource, Semantics, Documentation, AttrDef, Crate, GenericDef, ModPath, Hygiene
|
||||||
};
|
};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use ra_db::SourceDatabase;
|
use ra_db::SourceDatabase;
|
||||||
|
@ -12,11 +12,13 @@ use ra_ide_db::{
|
||||||
defs::{classify_name, classify_name_ref, Definition},
|
defs::{classify_name, classify_name_ref, Definition},
|
||||||
RootDatabase,
|
RootDatabase,
|
||||||
};
|
};
|
||||||
use ra_syntax::{ast, match_ast, AstNode, SyntaxKind::*, SyntaxToken, TokenAtOffset};
|
use ra_syntax::{ast, match_ast, AstNode, SyntaxKind::*, SyntaxToken, SyntaxNode, TokenAtOffset, ast::Path};
|
||||||
use ra_project_model::ProjectWorkspace;
|
use ra_project_model::ProjectWorkspace;
|
||||||
use ra_hir_def::{item_scope::ItemInNs, db::DefDatabase};
|
use ra_hir_def::{item_scope::ItemInNs, db::DefDatabase, GenericDefId, ModuleId, resolver::HasResolver};
|
||||||
use ra_tt::{Literal, Ident, Punct, TokenTree, Leaf};
|
use ra_tt::{Literal, Ident, Punct, TokenTree, Leaf};
|
||||||
use ra_hir_expand::name::AsName;
|
use ra_hir_expand::name::AsName;
|
||||||
|
use ra_parser::FragmentKind;
|
||||||
|
use maplit::{hashset, hashmap};
|
||||||
|
|
||||||
use comrak::{parse_document,format_commonmark, ComrakOptions, Arena};
|
use comrak::{parse_document,format_commonmark, ComrakOptions, Arena};
|
||||||
use comrak::nodes::NodeValue;
|
use comrak::nodes::NodeValue;
|
||||||
|
@ -412,8 +414,9 @@ fn rewrite_links(db: &RootDatabase, markdown: &str, definition: &Definition, wor
|
||||||
// module-based links (AKA intra-doc links): `super::super::module::MyStruct`
|
// module-based links (AKA intra-doc links): `super::super::module::MyStruct`
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
let link_str = String::from_utf8(link.url.clone()).unwrap();
|
let link_str = String::from_utf8(link.url.clone()).unwrap();
|
||||||
|
let link_text = String::from_utf8(link.title.clone()).unwrap();
|
||||||
let resolved = try_resolve_path(db, &mut doc_target_dirs.clone(), definition, &link_str, UrlMode::Url)
|
let resolved = try_resolve_path(db, &mut doc_target_dirs.clone(), definition, &link_str, UrlMode::Url)
|
||||||
.or_else(|| try_resolve_intra(db, &mut doc_target_dirs.clone(), definition, &link_str));
|
.or_else(|| try_resolve_intra(db, &mut doc_target_dirs.clone(), definition, &link_text, &link_str));
|
||||||
|
|
||||||
if let Some(resolved) = resolved {
|
if let Some(resolved) = resolved {
|
||||||
link.url = resolved.as_bytes().to_vec();
|
link.url = resolved.as_bytes().to_vec();
|
||||||
|
@ -430,11 +433,113 @@ fn rewrite_links(db: &RootDatabase, markdown: &str, definition: &Definition, wor
|
||||||
Some(String::from_utf8(out).unwrap())
|
Some(String::from_utf8(out).unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq, Hash, Copy, Clone, Debug)]
|
||||||
|
enum Namespace {
|
||||||
|
Types,
|
||||||
|
Values,
|
||||||
|
Macros
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Namespace {
|
||||||
|
/// Extract the specified namespace from an intra-doc-link if one exists.
|
||||||
|
fn from_intra_spec(s: &str) -> Option<Self> {
|
||||||
|
let ns_map = hashmap!{
|
||||||
|
Self::Types => (hashset!{"type", "struct", "enum", "mod", "trait", "union", "module"}, hashset!{}),
|
||||||
|
Self::Values => (hashset!{"value", "function", "fn", "method", "const", "static", "mod", "module"}, hashset!{"()"}),
|
||||||
|
Self::Macros => (hashset!{"macro"}, hashset!{"!"})
|
||||||
|
};
|
||||||
|
|
||||||
|
ns_map
|
||||||
|
.iter()
|
||||||
|
.filter(|(ns, (prefixes, suffixes))| {
|
||||||
|
prefixes.iter().map(|prefix| s.starts_with(prefix) && s.chars().nth(prefix.len()+1).map(|c| c == '@' || c == ' ').unwrap_or(false)).any(|cond| cond) ||
|
||||||
|
suffixes.iter().map(|suffix| s.starts_with(suffix) && s.chars().nth(suffix.len()+1).map(|c| c == '@' || c == ' ').unwrap_or(false)).any(|cond| cond)
|
||||||
|
})
|
||||||
|
.map(|(ns, (_, _))| *ns)
|
||||||
|
.next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Try to resolve path to local documentation via intra-doc-links (i.e. `super::gateway::Shard`).
|
/// Try to resolve path to local documentation via intra-doc-links (i.e. `super::gateway::Shard`).
|
||||||
///
|
///
|
||||||
/// See [RFC1946](https://github.com/rust-lang/rfcs/blob/master/text/1946-intra-rustdoc-links.md).
|
/// See [RFC1946](https://github.com/rust-lang/rfcs/blob/master/text/1946-intra-rustdoc-links.md).
|
||||||
fn try_resolve_intra(db: &RootDatabase, doc_target_dirs: impl Iterator<Item = PathBuf>, definition: &Definition, link: &str) -> Option<String> {
|
fn try_resolve_intra(db: &RootDatabase, doc_target_dirs: impl Iterator<Item = PathBuf>, definition: &Definition, link_text: &str, link_target: &str) -> Option<String> {
|
||||||
None
|
eprintln!("try_resolve_intra");
|
||||||
|
|
||||||
|
// Set link_target for implied shortlinks
|
||||||
|
let link_target = if link_target.is_empty() {
|
||||||
|
link_text.trim_matches('`')
|
||||||
|
} else {
|
||||||
|
link_target
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse link as a module path
|
||||||
|
// This expects a full document, which a single path isn't, but we can just ignore the errors.
|
||||||
|
let parsed = SyntaxNode::new_root(ra_syntax::parse_text(link_target).0);
|
||||||
|
let path = parsed.descendants().filter_map(Path::cast).next()?;
|
||||||
|
let modpath = ModPath::from_src(path, &Hygiene::new_unhygienic()).unwrap();
|
||||||
|
|
||||||
|
// Resolve it relative to symbol's location (according to the RFC this should consider small scopes
|
||||||
|
let resolver = {
|
||||||
|
use ra_hir_def::*;
|
||||||
|
use hir::*;
|
||||||
|
|
||||||
|
// TODO: This should be replaced by implementing HasResolver/TryHasResolver on ModuleDef and Definition.
|
||||||
|
match definition {
|
||||||
|
Definition::ModuleDef(def) => match def {
|
||||||
|
ModuleDef::Module(m) => Into::<ModuleId>::into(m.clone()).resolver(db),
|
||||||
|
ModuleDef::Function(f) => Into::<FunctionId>::into(f.clone()).resolver(db),
|
||||||
|
ModuleDef::Adt(adt) => Into::<AdtId>::into(adt.clone()).resolver(db),
|
||||||
|
ModuleDef::EnumVariant(ev) => Into::<GenericDefId>::into(Into::<GenericDef>::into(ev.clone())).resolver(db),
|
||||||
|
ModuleDef::Const(c) => Into::<GenericDefId>::into(Into::<GenericDef>::into(c.clone())).resolver(db),
|
||||||
|
ModuleDef::Static(s) => Into::<StaticId>::into(s.clone()).resolver(db),
|
||||||
|
ModuleDef::Trait(t) => Into::<TraitId>::into(t.clone()).resolver(db),
|
||||||
|
ModuleDef::TypeAlias(t) => Into::<ModuleId>::into(t.module(db)).resolver(db),
|
||||||
|
// TODO: This should be a resolver relative to `std`
|
||||||
|
ModuleDef::BuiltinType(t) => Into::<ModuleId>::into(definition.module(db)?).resolver(db)
|
||||||
|
},
|
||||||
|
Definition::Field(field) => Into::<VariantId>::into(Into::<VariantDef>::into(field.parent_def(db))).resolver(db),
|
||||||
|
Definition::Macro(m) => Into::<ModuleId>::into(m.module(db)?).resolver(db),
|
||||||
|
Definition::SelfType(imp) => Into::<ImplId>::into(imp.clone()).resolver(db),
|
||||||
|
// it's possible, read probable, that other arms of this are also unreachable
|
||||||
|
Definition::Local(local) => unreachable!(),
|
||||||
|
Definition::TypeParam(tp) => Into::<ModuleId>::into(tp.module(db)).resolver(db)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Namespace disambiguation
|
||||||
|
let namespace = Namespace::from_intra_spec(link_target);
|
||||||
|
|
||||||
|
let resolved = resolver.resolve_module_path_in_items(db, &modpath);
|
||||||
|
let (defid, namespace) = match namespace {
|
||||||
|
// TODO: .or(resolved.macros)
|
||||||
|
None => resolved.types.map(|t| (t.0, Namespace::Types)).or(resolved.values.map(|t| (t.0, Namespace::Values)))?,
|
||||||
|
Some(ns @ Namespace::Types) => (resolved.types?.0, ns),
|
||||||
|
Some(ns @ Namespace::Values) => (resolved.values?.0, ns),
|
||||||
|
// TODO:
|
||||||
|
Some(Namespace::Macros) => None?
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get the filepath of the final symbol
|
||||||
|
let def: ModuleDef = defid.into();
|
||||||
|
let module = def.module(db)?;
|
||||||
|
let krate = module.krate();
|
||||||
|
let ns = match namespace {
|
||||||
|
Namespace::Types => ItemInNs::Types(defid),
|
||||||
|
Namespace::Values => ItemInNs::Values(defid),
|
||||||
|
// TODO:
|
||||||
|
Namespace::Macros => None?
|
||||||
|
};
|
||||||
|
let import_map = db.import_map(krate.into());
|
||||||
|
let path = import_map.path_of(ns)?;
|
||||||
|
|
||||||
|
Some(
|
||||||
|
get_doc_url(db, &krate)?
|
||||||
|
.join(&format!("{}/", krate.display_name(db)?)).ok()?
|
||||||
|
.join(&path.segments.iter().map(|name| format!("{}", name)).join("/")).ok()?
|
||||||
|
.join(&get_symbol_filename(db, definition)?).ok()?
|
||||||
|
.into_string()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum UrlMode {
|
enum UrlMode {
|
||||||
|
@ -444,6 +549,7 @@ enum UrlMode {
|
||||||
|
|
||||||
/// Try to resolve path to local documentation via path-based links (i.e. `../gateway/struct.Shard.html`).
|
/// Try to resolve path to local documentation via path-based links (i.e. `../gateway/struct.Shard.html`).
|
||||||
fn try_resolve_path(db: &RootDatabase, doc_target_dirs: impl Iterator<Item = PathBuf>, definition: &Definition, link: &str, mode: UrlMode) -> Option<String> {
|
fn try_resolve_path(db: &RootDatabase, doc_target_dirs: impl Iterator<Item = PathBuf>, definition: &Definition, link: &str, mode: UrlMode) -> Option<String> {
|
||||||
|
eprintln!("try_resolve_path");
|
||||||
let ns = if let Definition::ModuleDef(moddef) = definition {
|
let ns = if let Definition::ModuleDef(moddef) = definition {
|
||||||
ItemInNs::Types(moddef.clone().into())
|
ItemInNs::Types(moddef.clone().into())
|
||||||
} else {
|
} else {
|
||||||
|
@ -481,6 +587,7 @@ fn try_resolve_path(db: &RootDatabase, doc_target_dirs: impl Iterator<Item = Pat
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Try to get the root URL of the documentation of a crate.
|
/// Try to get the root URL of the documentation of a crate.
|
||||||
|
// TODO: Special case standard, core, alloc libraries
|
||||||
fn get_doc_url(db: &RootDatabase, krate: &Crate) -> Option<Url> {
|
fn get_doc_url(db: &RootDatabase, krate: &Crate) -> Option<Url> {
|
||||||
// Look for #![doc(html_root_url = "...")]
|
// Look for #![doc(html_root_url = "...")]
|
||||||
let attrs = db.attrs(AttrDef::from(krate.root_module(db)?).into());
|
let attrs = db.attrs(AttrDef::from(krate.root_module(db)?).into());
|
||||||
|
|
|
@ -47,7 +47,7 @@ use crate::syntax_node::GreenNode;
|
||||||
pub use crate::{
|
pub use crate::{
|
||||||
algo::InsertPosition,
|
algo::InsertPosition,
|
||||||
ast::{AstNode, AstToken},
|
ast::{AstNode, AstToken},
|
||||||
parsing::{lex_single_syntax_kind, lex_single_valid_syntax_kind, tokenize, Token},
|
parsing::{lex_single_syntax_kind, lex_single_valid_syntax_kind, tokenize, Token, parse_text},
|
||||||
ptr::{AstPtr, SyntaxNodePtr},
|
ptr::{AstPtr, SyntaxNodePtr},
|
||||||
syntax_error::SyntaxError,
|
syntax_error::SyntaxError,
|
||||||
syntax_node::{
|
syntax_node::{
|
||||||
|
|
|
@ -15,7 +15,7 @@ pub use lexer::*;
|
||||||
pub(crate) use self::reparsing::incremental_reparse;
|
pub(crate) use self::reparsing::incremental_reparse;
|
||||||
use ra_parser::SyntaxKind;
|
use ra_parser::SyntaxKind;
|
||||||
|
|
||||||
pub(crate) fn parse_text(text: &str) -> (GreenNode, Vec<SyntaxError>) {
|
pub fn parse_text(text: &str) -> (GreenNode, Vec<SyntaxError>) {
|
||||||
let (tokens, lexer_errors) = tokenize(&text);
|
let (tokens, lexer_errors) = tokenize(&text);
|
||||||
|
|
||||||
let mut token_source = TextTokenSource::new(text, &tokens);
|
let mut token_source = TextTokenSource::new(text, &tokens);
|
||||||
|
|
Loading…
Reference in a new issue