Introduce Name and Fqn types for names

Previosuly, we used strings to store item names.  Sometimes, these
strings would refer to full paths (e. g. kuchiki::NodeDataRef::as_node),
sometimes to local paths (e. g. NodeDataRef::as_node) and sometimes only
to the last item (e. g. as_node).

With this patch, we add two new types:  doc::Name is a name without a
semantic meaning, so it could be any of the cases mentioned above.
doc::Fqn is a wrapper around doc::Name that stores fully-qualifier
names, i. e. names where the first element is the crate.
This commit is contained in:
Robin Krahl 2020-07-21 12:34:46 +02:00
parent 58929bc98b
commit 94dff39c8a
3 changed files with 278 additions and 70 deletions

View File

@ -1,11 +1,26 @@
// SPDX-FileCopyrightText: 2020 Robin Krahl <robin.krahl@ireas.org>
// SPDX-License-Identifier: MIT
use std::convert;
use std::fmt;
use std::ops;
use std::path;
use std::str;
use crate::parser;
#[derive(Clone, Debug, Default, Eq, Ord, PartialEq, PartialOrd)]
pub struct Name {
// s[..first_end] == first
// s[last_name_start..] == last_name
s: String,
first_end: usize,
last_start: usize,
}
#[derive(Clone, Debug, Default, Eq, Ord, PartialEq, PartialOrd)]
pub struct Fqn(Name);
#[derive(Clone, Debug, PartialEq)]
pub struct Crate {
pub name: String,
@ -15,8 +30,8 @@ pub struct Crate {
#[derive(Clone, Debug, PartialEq)]
pub struct Item {
pub path: path::PathBuf,
pub name: Fqn,
pub member: Option<String>,
pub name: String,
}
#[derive(Clone, Debug, Default)]
@ -27,61 +42,191 @@ pub struct Doc {
pub members: Vec<(String, Vec<Doc>)>,
}
impl Name {
pub fn is_singleton(&self) -> bool {
self.last_start == 0
}
pub fn first(&self) -> &str {
&self.s[..self.first_end]
}
pub fn last(&self) -> &str {
&self.s[self.last_start..]
}
pub fn full(&self) -> &str {
&self.s
}
pub fn rest(&self) -> Option<&str> {
if self.is_singleton() {
None
} else {
Some(&self.s[self.first_end + 2..])
}
}
pub fn rest_or_first(&self) -> &str {
self.rest().unwrap_or_else(|| self.first())
}
pub fn parent(&self) -> Option<Self> {
if self.is_singleton() {
None
} else {
Some((&self.s[..self.last_start - 2]).to_owned().into())
}
}
pub fn child(&self, s: &str) -> Self {
let mut name = self.s.clone();
name.push_str("::");
name.push_str(s);
name.into()
}
pub fn ends_with(&self, name: &Name) -> bool {
self.s == name.s || self.s.ends_with(&format!("::{}", name.s))
}
}
impl From<String> for Name {
fn from(s: String) -> Self {
let first_end = s.find("::").unwrap_or_else(|| s.len());
let last_start = s.rfind("::").map(|i| i + 2).unwrap_or(0);
Self {
s,
first_end,
last_start,
}
}
}
impl From<Name> for String {
fn from(n: Name) -> Self {
n.s
}
}
impl fmt::Display for Name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", &self.s)
}
}
impl str::FromStr for Name {
type Err = convert::Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(s.to_owned().into())
}
}
impl Fqn {
pub fn krate(&self) -> &str {
self.first()
}
pub fn parent(&self) -> Option<Self> {
self.0.parent().map(From::from)
}
pub fn child(&self, s: &str) -> Self {
self.0.child(s).into()
}
}
impl From<Name> for Fqn {
fn from(n: Name) -> Self {
Self(n)
}
}
impl From<String> for Fqn {
fn from(s: String) -> Self {
Self(s.into())
}
}
impl fmt::Display for Fqn {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", &self.0)
}
}
impl ops::Deref for Fqn {
type Target = Name;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Crate {
pub fn new(name: String, path: path::PathBuf) -> Self {
Crate { name, path }
}
pub fn find_item(&self, item: &[&str]) -> anyhow::Result<Option<Item>> {
let (name, full_name) = self.get_names(item);
parser::find_item(self.path.join("all.html"), &name)
.map(|o| o.map(|s| Item::new(full_name, self.path.join(path::PathBuf::from(s)), None)))
pub fn find_item(&self, name: &Fqn) -> anyhow::Result<Option<Item>> {
if self.name == name.krate() {
if let Some(local_name) = name.rest() {
if let Some(path) = parser::find_item(self.path.join("all.html"), local_name)? {
let path = path::PathBuf::from(path);
return Ok(Some(Item::new(name.clone(), self.path.join(path), None)));
}
}
}
Ok(None)
}
pub fn find_module(&self, item: &[&str]) -> Option<Item> {
let path = self
.path
.join(path::PathBuf::from(item.join("/")))
.join("index.html");
if path.is_file() {
let (_, full_name) = self.get_names(item);
Some(Item::new(full_name, path, None))
pub fn find_module(&self, name: &Fqn) -> Option<Item> {
if self.name == name.krate() {
let module_path = if let Some(rest) = name.rest() {
rest.split("::").fold(path::PathBuf::new(), |mut p, s| {
p.push(s);
p
})
} else {
path::PathBuf::new()
};
let path = self.path.join(module_path).join("index.html");
if path.is_file() {
Some(Item::new(name.clone(), path, None))
} else {
None
}
} else {
None
}
}
pub fn find_member(&self, item: &[&str]) -> Option<Item> {
if let Some((last, elements)) = item.split_last() {
// TODO: error
let parent = self.find_item(elements).unwrap();
parent.and_then(|i| i.find_member(last))
pub fn find_member(&self, name: &Fqn) -> Option<Item> {
if self.name == name.krate() {
if let Some(parent) = name.parent() {
// TODO: error
self.find_item(&parent)
.unwrap()
.and_then(|i| i.find_member(name.last()))
} else {
None
}
} else {
None
}
}
fn get_names(&self, item: &[&str]) -> (String, String) {
let name = item.join("::");
let full_name = if item.is_empty() {
self.name.clone()
} else {
format!("{}::{}", &self.name, &name)
};
(name, full_name)
}
}
impl Item {
pub fn new(name: String, path: path::PathBuf, member: Option<String>) -> Self {
pub fn new(name: Fqn, path: path::PathBuf, member: Option<String>) -> Self {
Item { path, member, name }
}
pub fn load_doc(&self) -> anyhow::Result<Doc> {
if let Some(member) = &self.member {
parser::parse_member_doc(&self.path, &self.name, member)
parser::parse_member_doc(&self.path, self.name.rest_or_first(), member)
} else {
parser::parse_item_doc(&self.path, &self.name)
parser::parse_item_doc(&self.path, self.name.rest_or_first())
}
}
@ -117,3 +262,58 @@ impl fmt::Display for Doc {
}
}
}
#[cfg(test)]
mod tests {
use super::Name;
fn assert_name(input: &str, first: &str, last: &str, rest: &str) {
let name: Name = input.to_owned().into();
assert_eq!(first, name.first(), "first for '{}'", input);
assert_eq!(last, name.last(), "last for '{}'", input);
assert_eq!(input, name.full(), "full for '{}'", input);
if rest == input {
assert_eq!(None, name.rest(), "rest for '{}'", input);
} else {
assert_eq!(Some(rest), name.rest(), "rest for '{}'", input);
}
}
#[test]
fn test_empty_name() {
assert_name("", "", "", "");
}
#[test]
fn test_crate_name() {
assert_name("rand", "rand", "rand", "rand");
}
#[test]
fn test_module_name() {
assert_name("rand::error", "rand", "error", "error");
assert_name("rand::error::nested", "rand", "nested", "error::nested");
}
#[test]
fn test_item_name() {
assert_name("rand::Error", "rand", "Error", "Error");
assert_name("rand::error::Error", "rand", "Error", "error::Error");
}
#[test]
fn test_member_name() {
assert_name("rand::Error::source", "rand", "source", "Error::source");
assert_name(
"rand::error::Error::source",
"rand",
"source",
"error::Error::source",
);
}
#[test]
fn test_colon() {
assert_name("er:ror::Error", "er:ror", "Error", "Error");
}
}

View File

@ -20,24 +20,25 @@ use std::fs;
use std::io;
use std::path;
use crate::doc;
#[derive(Debug)]
pub struct Index {
data: Data,
}
#[derive(Clone, Debug, Default, Eq, Ord, PartialEq, PartialOrd)]
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct IndexItem {
pub path: String,
pub name: String,
pub name: doc::Fqn,
pub description: String,
}
impl fmt::Display for IndexItem {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.description.is_empty() {
write!(f, "{}::{}", &self.path, &self.name)
write!(f, "{}", &self.name)
} else {
write!(f, "{}::{}: {}", &self.path, &self.name, &self.description)
write!(f, "{}: {}", &self.name, &self.description)
}
}
}
@ -110,8 +111,7 @@ impl Index {
}
}
pub fn find(&self, keyword: &str) -> Vec<IndexItem> {
let keyword = format!("::{}", keyword);
pub fn find(&self, name: &doc::Name) -> Vec<IndexItem> {
let mut matches: Vec<IndexItem> = Vec::new();
for (krate, data) in &self.data.crates {
let mut path = krate;
@ -134,11 +134,10 @@ impl Index {
}
None => path.to_owned(),
};
let full_name = format!("{}::{}", &full_path, &item.name);
if full_name.ends_with(&keyword) {
let full_name: doc::Fqn = format!("{}::{}", &full_path, &item.name).into();
if full_name.ends_with(&name) {
matches.push(IndexItem {
name: item.name.clone(),
path: full_path,
name: full_name,
description: item.desc.clone(),
});
}

View File

@ -48,7 +48,7 @@ use structopt::StructOpt;
#[derive(Debug, StructOpt)]
struct Opt {
/// The keyword to open the documentation for, e. g. `rand_core::RngCore`
keyword: String,
keyword: doc::Name,
/// The sources to check for documentation generated by rustdoc
///
@ -132,14 +132,14 @@ fn load_sources(
/// Find the documentation for an item with the given name (exact matches only).
fn find_doc(
sources: &[Box<dyn source::Source>],
keyword: &str,
name: &doc::Name,
) -> anyhow::Result<Option<doc::Doc>> {
let parts: Vec<&str> = keyword.split("::").collect();
if let Some(crate_) = find_crate(sources, parts[0]) {
crate_
.find_item(&parts[1..])?
.or_else(|| crate_.find_module(&parts[1..]))
.or_else(|| crate_.find_member(&parts[1..]))
let fqn: doc::Fqn = name.clone().into();
if let Some(krate) = find_crate(sources, fqn.krate()) {
krate
.find_item(&fqn)?
.or_else(|| krate.find_module(&fqn))
.or_else(|| krate.find_member(&fqn))
.map(|i| i.load_doc())
.transpose()
} else {
@ -156,14 +156,13 @@ fn find_crate(sources: &[Box<dyn source::Source>], name: &str) -> Option<doc::Cr
/// keyword.
fn search_doc(
sources: &[Box<dyn source::Source>],
keyword: &str,
name: &doc::Name,
) -> anyhow::Result<Option<doc::Doc>> {
if let Some(item) = search_item(sources, keyword)? {
if let Some(item) = search_item(sources, name)? {
use anyhow::Context;
let item = format!("{}::{}", item.path, item.name);
let doc = find_doc(sources, &item)?
.with_context(|| format!("Could not find documentation for {}", &item))?;
let doc = find_doc(sources, &item.name)?
.with_context(|| format!("Could not find documentation for {}", &item.name))?;
Ok(Some(doc))
} else {
Ok(None)
@ -173,7 +172,7 @@ fn search_doc(
/// Use the search index to find an item that partially matches the given keyword.
fn search_item(
sources: &[Box<dyn source::Source>],
keyword: &str,
name: &doc::Name,
) -> anyhow::Result<Option<index::IndexItem>> {
let indexes = sources
.iter()
@ -181,7 +180,7 @@ fn search_item(
.collect::<anyhow::Result<Vec<_>>>()?;
let mut items = indexes
.iter()
.map(|i| i.find(keyword))
.map(|i| i.find(name))
.collect::<Vec<_>>()
.concat();
items.sort_unstable();
@ -190,19 +189,19 @@ fn search_item(
if items.is_empty() {
Err(anyhow::anyhow!(
"Could not find documentation for {}",
&keyword
&name
))
} else if items.len() == 1 {
Ok(Some(items[0].clone()))
} else {
select_item(&items, keyword)
select_item(&items, name)
}
}
/// Let the user select an item from the given list of matches.
fn select_item(
items: &[index::IndexItem],
keyword: &str,
name: &doc::Name,
) -> anyhow::Result<Option<index::IndexItem>> {
use std::io::Write;
@ -210,10 +209,10 @@ fn select_item(
anyhow::ensure!(
termion::is_tty(&io::stdin()),
"Found multiple matches for {}",
keyword
name
);
println!("Found mulitple matches for {} select one of:", keyword);
println!("Found mulitple matches for {} select one of:", name);
println!();
let width = (items.len() + 1).to_string().len();
for (i, item) in items.iter().enumerate() {
@ -252,16 +251,26 @@ mod tests {
let path = ensure_docs();
let sources = vec![source::get_source(path).unwrap()];
assert!(super::find_doc(&sources, "kuchiki").unwrap().is_some());
assert!(super::find_doc(&sources, "kuchiki::NodeRef")
assert!(super::find_doc(&sources, &"kuchiki".to_owned().into())
.unwrap()
.is_some());
assert!(super::find_doc(&sources, "kuchiki::NodeDataRef::as_node")
assert!(
super::find_doc(&sources, &"kuchiki::NodeRef".to_owned().into())
.unwrap()
.is_some()
);
assert!(
super::find_doc(&sources, &"kuchiki::NodeDataRef::as_node".to_owned().into())
.unwrap()
.is_some()
);
assert!(
super::find_doc(&sources, &"kuchiki::traits".to_owned().into())
.unwrap()
.is_some()
);
assert!(super::find_doc(&sources, &"kachiki".to_owned().into())
.unwrap()
.is_some());
assert!(super::find_doc(&sources, "kuchiki::traits")
.unwrap()
.is_some());
assert!(super::find_doc(&sources, "kachiki").unwrap().is_none());
.is_none());
}
}