Auto merge of #23289 - mihneadb:rustdoc-search-by-type, r=alexcrichton
This adds search by type (for functions/methods) support to Rustdoc. Target issue is at https://github.com/rust-lang/rfcs/issues/658.
I've described my approach here: https://github.com/rust-lang/rfcs/issues/658#issuecomment-76484200. I'll copy the text in here as well:
---
Hi, it took me longer than I wished, but I have implemented this in a not-too-complex way that I think can be extended to support more complex features (like the ones mentioned [here](https://github.com/rust-lang/rust/issues/12866#issuecomment-66945317)).
The idea is to generate a JSON representation of the types of methods/functions in the existing index, and then make the JS understand when it should look by type (and not by name).
I tried to come up with a JSON representation that can be extended to support generics, bounds, ref/mut annotations and so on. Here are a few samples:
Function:
```rust
fn to_uppercase(c: char) -> char
```
```json
{
"inputs": [
{"name": "char"}
],
"output": {
"name": "char",
}
}
```
Method (implemented or defined in trait):
```rust
// in struct Vec
// self is considered an argument as well
fn capacity(&self) -> usize
```
```json
{
"inputs": [
{"name": "vec"}
],
"output": {
"name": "usize"
}
}
```
This simple format can be extended by adding more fields, like `generic: bool`, a `bounds` mapping and so on.
I have a working implementation in https://github.com/rust-lang/rust/compare/master...mihneadb:rustdoc-search-by-type. You can check out a live demo [here](http://data.mihneadb.net/doc/std/index.html?search=charext%20-%3E%20char).

The feature list is not that long:
- search by types (you *can* use generics as well, as long as you use the exact name - e.g. [`vec,t -> `](http://data.mihneadb.net/doc/std/index.html?search=vec%2C%20t%20-%3E))
- order of arguments does not matter
- `self` is took into account as well (e.g. search for `vec -> usize`)
- does not use "complex" annotations (e.g. you don't search for `&char -> char` but for `char -> char`)
My goal is to get a working, minimal "base" merged so that others can build upon it. How should I proceed? Do I open a PR (badly in need of code review since this is my first non "hello world"-ish rust code)?
---
This commit is contained in:
commit
30e1f9a1c2
2 changed files with 142 additions and 3 deletions
|
@ -34,6 +34,7 @@
|
||||||
//! both occur before the crate is rendered.
|
//! both occur before the crate is rendered.
|
||||||
pub use self::ExternalLocation::*;
|
pub use self::ExternalLocation::*;
|
||||||
|
|
||||||
|
use std::ascii::OwnedAsciiExt;
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
|
@ -239,6 +240,51 @@ struct IndexItem {
|
||||||
path: String,
|
path: String,
|
||||||
desc: String,
|
desc: String,
|
||||||
parent: Option<ast::DefId>,
|
parent: Option<ast::DefId>,
|
||||||
|
search_type: Option<IndexItemFunctionType>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A type used for the search index.
|
||||||
|
struct Type {
|
||||||
|
name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Type {
|
||||||
|
/// Formats type as {name: $name}.
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
// Wrapping struct fmt should never call us when self.name is None,
|
||||||
|
// but just to be safe we write `null` in that case.
|
||||||
|
match self.name {
|
||||||
|
Some(ref n) => write!(f, "{{\"name\":\"{}\"}}", n),
|
||||||
|
None => write!(f, "null")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Full type of functions/methods in the search index.
|
||||||
|
struct IndexItemFunctionType {
|
||||||
|
inputs: Vec<Type>,
|
||||||
|
output: Option<Type>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for IndexItemFunctionType {
|
||||||
|
/// Formats a full fn type as a JSON {inputs: [Type], outputs: Type/null}.
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
// If we couldn't figure out a type, just write `null`.
|
||||||
|
if self.inputs.iter().any(|ref i| i.name.is_none()) ||
|
||||||
|
(self.output.is_some() && self.output.as_ref().unwrap().name.is_none()) {
|
||||||
|
return write!(f, "null")
|
||||||
|
}
|
||||||
|
|
||||||
|
let inputs: Vec<String> = self.inputs.iter().map(|ref t| format!("{}", t)).collect();
|
||||||
|
try!(write!(f, "{{\"inputs\":[{}],\"output\":", inputs.connect(",")));
|
||||||
|
|
||||||
|
match self.output {
|
||||||
|
Some(ref t) => try!(write!(f, "{}", t)),
|
||||||
|
None => try!(write!(f, "null"))
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(try!(write!(f, "}}")))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TLS keys used to carry information around during rendering.
|
// TLS keys used to carry information around during rendering.
|
||||||
|
@ -409,6 +455,7 @@ fn build_index(krate: &clean::Crate, cache: &mut Cache) -> io::Result<String> {
|
||||||
path: fqp[..fqp.len() - 1].connect("::"),
|
path: fqp[..fqp.len() - 1].connect("::"),
|
||||||
desc: shorter(item.doc_value()).to_string(),
|
desc: shorter(item.doc_value()).to_string(),
|
||||||
parent: Some(did),
|
parent: Some(did),
|
||||||
|
search_type: None,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
None => {}
|
None => {}
|
||||||
|
@ -458,7 +505,11 @@ fn build_index(krate: &clean::Crate, cache: &mut Cache) -> io::Result<String> {
|
||||||
let pathid = *nodeid_to_pathid.get(&nodeid).unwrap();
|
let pathid = *nodeid_to_pathid.get(&nodeid).unwrap();
|
||||||
try!(write!(&mut w, ",{}", pathid));
|
try!(write!(&mut w, ",{}", pathid));
|
||||||
}
|
}
|
||||||
None => {}
|
None => try!(write!(&mut w, ",null"))
|
||||||
|
}
|
||||||
|
match item.search_type {
|
||||||
|
Some(ref t) => try!(write!(&mut w, ",{}", t)),
|
||||||
|
None => try!(write!(&mut w, ",null"))
|
||||||
}
|
}
|
||||||
try!(write!(&mut w, "]"));
|
try!(write!(&mut w, "]"));
|
||||||
}
|
}
|
||||||
|
@ -872,12 +923,21 @@ impl DocFolder for Cache {
|
||||||
|
|
||||||
match parent {
|
match parent {
|
||||||
(parent, Some(path)) if is_method || (!self.privmod && !hidden_field) => {
|
(parent, Some(path)) if is_method || (!self.privmod && !hidden_field) => {
|
||||||
|
// Needed to determine `self` type.
|
||||||
|
let parent_basename = self.parent_stack.first().and_then(|parent| {
|
||||||
|
match self.paths.get(parent) {
|
||||||
|
Some(&(ref fqp, _)) => Some(fqp[fqp.len() - 1].clone()),
|
||||||
|
_ => None
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
self.search_index.push(IndexItem {
|
self.search_index.push(IndexItem {
|
||||||
ty: shortty(&item),
|
ty: shortty(&item),
|
||||||
name: s.to_string(),
|
name: s.to_string(),
|
||||||
path: path.connect("::").to_string(),
|
path: path.connect("::").to_string(),
|
||||||
desc: shorter(item.doc_value()).to_string(),
|
desc: shorter(item.doc_value()).to_string(),
|
||||||
parent: parent,
|
parent: parent,
|
||||||
|
search_type: get_index_search_type(&item, parent_basename),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
(Some(parent), None) if is_method || (!self.privmod && !hidden_field)=> {
|
(Some(parent), None) if is_method || (!self.privmod && !hidden_field)=> {
|
||||||
|
@ -2307,6 +2367,52 @@ fn make_item_keywords(it: &clean::Item) -> String {
|
||||||
format!("{}, {}", get_basic_keywords(), it.name.as_ref().unwrap())
|
format!("{}, {}", get_basic_keywords(), it.name.as_ref().unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_index_search_type(item: &clean::Item,
|
||||||
|
parent: Option<String>) -> Option<IndexItemFunctionType> {
|
||||||
|
let decl = match item.inner {
|
||||||
|
clean::FunctionItem(ref f) => &f.decl,
|
||||||
|
clean::MethodItem(ref m) => &m.decl,
|
||||||
|
clean::TyMethodItem(ref m) => &m.decl,
|
||||||
|
_ => return None
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut inputs = Vec::new();
|
||||||
|
|
||||||
|
// Consider `self` an argument as well.
|
||||||
|
if let Some(name) = parent {
|
||||||
|
inputs.push(Type { name: Some(name.into_ascii_lowercase()) });
|
||||||
|
}
|
||||||
|
|
||||||
|
inputs.extend(&mut decl.inputs.values.iter().map(|arg| {
|
||||||
|
get_index_type(&arg.type_)
|
||||||
|
}));
|
||||||
|
|
||||||
|
let output = match decl.output {
|
||||||
|
clean::FunctionRetTy::Return(ref return_type) => Some(get_index_type(return_type)),
|
||||||
|
_ => None
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(IndexItemFunctionType { inputs: inputs, output: output })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_index_type(clean_type: &clean::Type) -> Type {
|
||||||
|
Type { name: get_index_type_name(clean_type).map(|s| s.into_ascii_lowercase()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_index_type_name(clean_type: &clean::Type) -> Option<String> {
|
||||||
|
match *clean_type {
|
||||||
|
clean::ResolvedPath { ref path, .. } => {
|
||||||
|
let segments = &path.segments;
|
||||||
|
Some(segments[segments.len() - 1].name.clone())
|
||||||
|
},
|
||||||
|
clean::Generic(ref s) => Some(s.clone()),
|
||||||
|
clean::Primitive(ref p) => Some(format!("{:?}", p)),
|
||||||
|
clean::BorrowedRef { ref type_, .. } => get_index_type_name(type_),
|
||||||
|
// FIXME: add all from clean::Type.
|
||||||
|
_ => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn cache() -> Arc<Cache> {
|
pub fn cache() -> Arc<Cache> {
|
||||||
CACHE_KEY.with(|c| c.borrow().clone())
|
CACHE_KEY.with(|c| c.borrow().clone())
|
||||||
}
|
}
|
||||||
|
|
|
@ -209,6 +209,33 @@
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// searching by type
|
||||||
|
} else if (val.search("->") > -1) {
|
||||||
|
var trimmer = function (s) { return s.trim(); };
|
||||||
|
var parts = val.split("->").map(trimmer);
|
||||||
|
var input = parts[0];
|
||||||
|
// sort inputs so that order does not matter
|
||||||
|
var inputs = input.split(",").map(trimmer).sort();
|
||||||
|
var output = parts[1];
|
||||||
|
|
||||||
|
for (var i = 0; i < nSearchWords; ++i) {
|
||||||
|
var type = searchIndex[i].type;
|
||||||
|
if (!type) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort index inputs so that order does not matter
|
||||||
|
var typeInputs = type.inputs.map(function (input) {
|
||||||
|
return input.name;
|
||||||
|
}).sort();
|
||||||
|
|
||||||
|
// allow searching for void (no output) functions as well
|
||||||
|
var typeOutput = type.output ? type.output.name : "";
|
||||||
|
if (inputs.toString() === typeInputs.toString() &&
|
||||||
|
output == typeOutput) {
|
||||||
|
results.push({id: i, index: -1, dontValidate: true});
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// gather matching search results up to a certain maximum
|
// gather matching search results up to a certain maximum
|
||||||
val = val.replace(/\_/g, "");
|
val = val.replace(/\_/g, "");
|
||||||
|
@ -329,6 +356,11 @@
|
||||||
path = result.item.path.toLowerCase(),
|
path = result.item.path.toLowerCase(),
|
||||||
parent = result.item.parent;
|
parent = result.item.parent;
|
||||||
|
|
||||||
|
// this validation does not make sense when searching by types
|
||||||
|
if (result.dontValidate) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var valid = validateResult(name, path, split, parent);
|
var valid = validateResult(name, path, split, parent);
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
result.id = -1;
|
result.id = -1;
|
||||||
|
@ -573,7 +605,8 @@
|
||||||
// (String) name,
|
// (String) name,
|
||||||
// (String) full path or empty string for previous path,
|
// (String) full path or empty string for previous path,
|
||||||
// (String) description,
|
// (String) description,
|
||||||
// (optional Number) the parent path index to `paths`]
|
// (Number | null) the parent path index to `paths`]
|
||||||
|
// (Object | null) the type of the function (if any)
|
||||||
var items = rawSearchIndex[crate].items;
|
var items = rawSearchIndex[crate].items;
|
||||||
// an array of [(Number) item type,
|
// an array of [(Number) item type,
|
||||||
// (String) name]
|
// (String) name]
|
||||||
|
@ -598,7 +631,7 @@
|
||||||
var rawRow = items[i];
|
var rawRow = items[i];
|
||||||
var row = {crate: crate, ty: rawRow[0], name: rawRow[1],
|
var row = {crate: crate, ty: rawRow[0], name: rawRow[1],
|
||||||
path: rawRow[2] || lastPath, desc: rawRow[3],
|
path: rawRow[2] || lastPath, desc: rawRow[3],
|
||||||
parent: paths[rawRow[4]]};
|
parent: paths[rawRow[4]], type: rawRow[5]};
|
||||||
searchIndex.push(row);
|
searchIndex.push(row);
|
||||||
if (typeof row.name === "string") {
|
if (typeof row.name === "string") {
|
||||||
var word = row.name.toLowerCase();
|
var word = row.name.toLowerCase();
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue