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:
parent
58929bc98b
commit
94dff39c8a
262
src/doc.rs
262
src/doc.rs
@ -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");
|
||||
}
|
||||
}
|
||||
|
21
src/index.rs
21
src/index.rs
@ -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(),
|
||||
});
|
||||
}
|
||||
|
65
src/main.rs
65
src/main.rs
@ -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());
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user