diff --git a/src/cli/main.rs b/src/cli/main.rs index 09abc2e1fb..6efa60dc74 100644 --- a/src/cli/main.rs +++ b/src/cli/main.rs @@ -27,6 +27,7 @@ mod rustup_mode; mod self_update; mod setup_mode; mod term2; +mod topical_doc; use crate::errors::*; use rustup::env_var::RUST_RECURSION_COUNT_MAX; diff --git a/src/cli/rustup_mode.rs b/src/cli/rustup_mode.rs index 512fbae9c2..d82fb633b4 100644 --- a/src/cli/rustup_mode.rs +++ b/src/cli/rustup_mode.rs @@ -4,6 +4,7 @@ use crate::help::*; use crate::self_update; use crate::term2; use crate::term2::Terminal; +use crate::topical_doc; use clap::{App, AppSettings, Arg, ArgGroup, ArgMatches, Shell, SubCommand}; use rustup::dist::dist::{PartialTargetTriple, PartialToolchainDesc, Profile, TargetTriple}; use rustup::dist::manifest::Component; @@ -510,7 +511,11 @@ pub fn cli() -> App<'static, 'static> { .map(|(name, _, _)| *name) .collect::>(), ), - ), + ) + .arg( + Arg::with_name("topic") + .help("Topic such as 'core', 'fn', 'usize', 'eprintln!', 'core::arch', 'alloc::format!', 'std::fs', 'std::fs::read_dir', 'std::io::Bytes', 'std::iter::Sum', 'std::io::error::Result' etc..."), + ), ); if cfg!(not(target_os = "windows")) { @@ -1240,13 +1245,16 @@ const DOCS_DATA: &[(&str, &str, &str,)] = &[ fn doc(cfg: &Cfg, m: &ArgMatches<'_>) -> Result<()> { let toolchain = explicit_or_dir_toolchain(cfg, m)?; + let topical_path: PathBuf; - let doc_url = - if let Some((_, _, path)) = DOCS_DATA.iter().find(|(name, _, _)| m.is_present(name)) { - path - } else { - "index.html" - }; + let doc_url = if let Some(topic) = m.value_of("topic") { + topical_path = topical_doc::local_path(&toolchain.doc_path("").unwrap(), topic)?; + topical_path.to_str().unwrap() + } else if let Some((_, _, path)) = DOCS_DATA.iter().find(|(name, _, _)| m.is_present(name)) { + path + } else { + "index.html" + }; if m.is_present("path") { let doc_path = toolchain.doc_path(doc_url)?; diff --git a/src/cli/topical_doc.rs b/src/cli/topical_doc.rs new file mode 100644 index 0000000000..f1d4517545 --- /dev/null +++ b/src/cli/topical_doc.rs @@ -0,0 +1,141 @@ +use crate::errors::*; +use std::ffi::OsString; +use std::fs; +use std::path::{Path, PathBuf}; + +struct DocData<'a> { + topic: &'a str, + subtopic: &'a str, + root: &'a Path, +} + +fn no_document(topic: &str) -> Result { + Err(format!("No document for '{}'", topic).into()) +} + +fn index_html(doc: &DocData, wpath: &Path) -> Option { + let indexhtml = wpath.join("index.html"); + match &doc.root.join(&indexhtml).exists() { + true => Some(indexhtml), + false => None, + } +} + +fn dir_into_vec(dir: &PathBuf) -> Result> { + let entries = fs::read_dir(dir).chain_err(|| format!("Opening directory {:?}", dir))?; + let mut v = Vec::new(); + for entry in entries { + let entry = entry?; + v.push(entry.file_name()); + } + Ok(v) +} + +fn search_path(doc: &DocData, wpath: &Path, keywords: &[&str]) -> Result { + let dir = &doc.root.join(&wpath); + if dir.is_dir() { + let entries = dir_into_vec(dir)?; + for k in keywords { + let filename = &format!("{}.{}.html", k, doc.subtopic); + if entries.contains(&OsString::from(filename)) { + return Ok(dir.join(filename)); + } + } + no_document(doc.topic) + } else { + no_document(doc.topic) + } +} + +pub fn local_path(root: &Path, topic: &str) -> Result { + let keywords_top = ["macro", "keyword", "primitive"]; + let keywords_mod = ["fn", "struct", "trait", "enum", "type", "constant"]; + + let topic_vec: Vec<&str> = topic.split("::").collect(); + let work_path = topic_vec.iter().fold(PathBuf::new(), |acc, e| acc.join(e)); + + let doc = DocData { + topic: &topic, + subtopic: topic_vec[topic_vec.len() - 1], + root, + }; + + /************************** + * Please ensure tests/mock/topical_doc_data.rs is UPDATED to reflect + * any change in functionality. + + Argument File directory + + # len() == 1 Return index.html + std std/index.html root/std + core core/index.html root/core + alloc alloc/index.html root/core + KKK std/keyword.KKK.html root/std + PPP std/primitive.PPP.html root/std + MMM std/macro.MMM.html root/std + + + # len() == 2 not ending in :: + MMM std/macro.MMM.html root/std + KKK std/keyword.KKK.html root/std + PPP std/primitive.PPP.html root/std + MMM core/macro.MMM.html root/core + MMM alloc/macro.MMM.html root/alloc + # If above fail, try module + std::module std/module/index.html root/std/module + core::module core/module/index.html root/core/module + alloc::module alloc/module/index.html alloc/core/module + + # len() == 2, ending with :: + std::module std/module/index.html root/std/module + core::module core/module/index.html root/core/module + alloc::module alloc/module/index.html alloc/core/module + + # len() > 2 + # search for index.html in rel_path + std::AAA::MMM std/AAA/MMM/index.html root/std/AAA/MMM + + # OR check if parent() dir exists and search for fn/sturct/etc + std::AAA::FFF std/AAA/fn.FFF9.html root/std/AAA + std::AAA::SSS std/AAA/struct.SSS.html root/std/AAA + core:AAA::SSS std/AAA/struct.SSS.html root/coreAAA + alloc:AAA::SSS std/AAA/struct.SSS.html root/coreAAA + std::AAA::TTT std/2222/trait.TTT.html root/std/AAA + std::AAA::EEE std/2222/enum.EEE.html root/std/AAA + std::AAA::TTT std/2222/type.TTT.html root/std/AAA + std::AAA::CCC std/2222/constant.CCC.html root/std/AAA + + **************************/ + + // topic.split.count cannot be 0 + let subpath_os_path = match topic_vec.len() { + 1 => match topic { + "std" | "core" | "alloc" => match index_html(&doc, &work_path) { + Some(f) => f, + None => no_document(doc.topic)?, + }, + _ => { + let std = PathBuf::from("std"); + search_path(&doc, &std, &keywords_top)? + } + }, + 2 => match index_html(&doc, &work_path) { + Some(f) => f, + None => { + let parent = work_path.parent().unwrap(); + search_path(&doc, &parent, &keywords_top)? + } + }, + _ => match index_html(&doc, &work_path) { + Some(f) => f, + None => { + // len > 2, guaranteed to have a parent, safe to unwrap + let parent = work_path.parent().unwrap(); + search_path(&doc, &parent, &keywords_mod)? + } + }, + }; + // The path and filename were validated to be existing on the filesystem. + // It should be safe to unwrap, or worth panicking. + Ok(subpath_os_path) +} diff --git a/tests/cli-rustup.rs b/tests/cli-rustup.rs index 3247e7a0a4..be3eb7183d 100644 --- a/tests/cli-rustup.rs +++ b/tests/cli-rustup.rs @@ -1577,6 +1577,39 @@ fn docs_with_path() { }); } +#[test] +fn docs_topical_with_path() { + setup(&|config| { + expect_ok(config, &["rustup", "default", "stable"]); + expect_ok( + config, + &[ + "rustup", + "toolchain", + "install", + "nightly", + "--no-self-update", + ], + ); + + for (topic, path) in mock::topical_doc_data::test_cases() { + let mut cmd = clitools::cmd(config, "rustup", &["doc", "--path", topic]); + clitools::env(config, &mut cmd); + + let out = cmd.output().unwrap(); + eprintln!("{:?}", String::from_utf8(out.stderr).unwrap()); + let out_str = String::from_utf8(out.stdout).unwrap(); + assert!( + out_str.contains(&path), + "comparing path\ntopic: '{}'\npath: '{}'\noutput: {}\n\n\n", + topic, + path, + out_str, + ); + } + }); +} + #[cfg(unix)] #[test] fn non_utf8_arg() { diff --git a/tests/mock/clitools.rs b/tests/mock/clitools.rs index cab443d7ae..77eb1e602b 100644 --- a/tests/mock/clitools.rs +++ b/tests/mock/clitools.rs @@ -5,6 +5,7 @@ use crate::mock::dist::{ change_channel_date, ManifestVersion, MockChannel, MockComponent, MockDistServer, MockPackage, MockTargetedPackage, }; +use crate::mock::topical_doc_data; use crate::mock::{MockComponentBuilder, MockFile, MockInstallerBuilder}; use lazy_static::lazy_static; use std::cell::RefCell; @@ -960,10 +961,14 @@ fn build_mock_rls_installer( } fn build_mock_rust_doc_installer() -> MockInstallerBuilder { + let mut files: Vec = topical_doc_data::paths() + .map(|x| MockFile::new(x, b"")) + .collect(); + files.insert(0, MockFile::new("share/doc/rust/html/index.html", b"")); MockInstallerBuilder { components: vec![MockComponentBuilder { name: "rust-docs".to_string(), - files: vec![MockFile::new("share/doc/rust/html/index.html", b"")], + files: files, }], } } diff --git a/tests/mock/mod.rs b/tests/mock/mod.rs index b3d5a3817e..db2e503ef0 100644 --- a/tests/mock/mod.rs +++ b/tests/mock/mod.rs @@ -2,6 +2,7 @@ pub mod clitools; pub mod dist; +pub mod topical_doc_data; use std::fs::{self, File, OpenOptions}; use std::io::Write; diff --git a/tests/mock/topical_doc_data.rs b/tests/mock/topical_doc_data.rs new file mode 100644 index 0000000000..9b3b3b2a8d --- /dev/null +++ b/tests/mock/topical_doc_data.rs @@ -0,0 +1,33 @@ +use std::path::PathBuf; + +// Paths are written as a string in the UNIX format to make it easy +// to maintain. +static TEST_CASES: &[&[&str]] = &[ + &["core", "core/index.html"], + &["core::arch", "core/arch/index.html"], + &["fn", "std/keyword.fn.html"], + &["std::fs", "std/fs/index.html"], + &["std::fs::read_dir", "std/fs/fn.read_dir.html"], + &["std::io::Bytes", "std/io/struct.Bytes.html"], + &["std::iter::Sum", "std/iter/trait.Sum.html"], + &["std::io::error::Result", "std/io/error/type.Result.html"], + &["usize", "std/primitive.usize.html"], + &["eprintln", "std/macro.eprintln.html"], + &["alloc::format", "alloc/macro.format.html"], +]; + +fn repath(origin: &str) -> String { + // Add doc prefix and rewrite string paths for the current platform + let with_prefix = "share/doc/rust/html/".to_owned() + origin; + let splitted = with_prefix.split("/"); + let repathed = splitted.fold(PathBuf::new(), |acc, e| acc.join(e)); + repathed.into_os_string().into_string().unwrap() +} + +pub fn test_cases<'a>() -> impl Iterator { + TEST_CASES.iter().map(|x| (x[0], repath(x[1]))) +} + +pub fn paths() -> impl Iterator { + TEST_CASES.iter().map(|x| repath(x[1])) +}