Add -e/--example option to only show examples
As requested here [0], this patch adds a new -e/--example option that extracts the examples from the documentation string instead of printing the full documentation for an item. Syntax highlighting will be added in the future. [0] https://old.reddit.com/r/rust/comments/hx16j0/rustyman_a_commandline_viewer_for_rustdoc/fz3utjf/
This commit is contained in:
parent
1a242e5474
commit
34857816e1
@ -10,6 +10,8 @@ SPDX-License-Identifier: MIT
|
||||
- Add `env_logger` dependency in version 0.7.1.
|
||||
- Add `log` dependency in version 0.4.11.
|
||||
- Show the definition for global functions.
|
||||
- Add the `-e`/`--examples` option to only show the examples instead of opening
|
||||
the full documentation for an item.
|
||||
|
||||
# v0.1.1 (2020-07-24)
|
||||
|
||||
|
32
src/doc.rs
32
src/doc.rs
@ -80,6 +80,12 @@ pub struct MemberGroup {
|
||||
pub members: Vec<Doc>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Example {
|
||||
pub description: Option<String>,
|
||||
pub code: String,
|
||||
}
|
||||
|
||||
impl Name {
|
||||
pub fn is_singleton(&self) -> bool {
|
||||
self.last_start == 0
|
||||
@ -465,13 +471,12 @@ impl Doc {
|
||||
groups: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MemberGroup {
|
||||
pub fn new(title: Option<String>) -> Self {
|
||||
MemberGroup {
|
||||
title,
|
||||
members: Vec::new(),
|
||||
pub fn find_examples(&self) -> anyhow::Result<Vec<Example>> {
|
||||
if let Some(description) = &self.description {
|
||||
parser::find_examples(&description)
|
||||
} else {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -486,6 +491,21 @@ impl fmt::Display for Doc {
|
||||
}
|
||||
}
|
||||
|
||||
impl MemberGroup {
|
||||
pub fn new(title: Option<String>) -> Self {
|
||||
MemberGroup {
|
||||
title,
|
||||
members: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Example {
|
||||
pub fn new(description: Option<String>, code: String) -> Self {
|
||||
Example { description, code }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Name;
|
||||
|
16
src/main.rs
16
src/main.rs
@ -86,6 +86,10 @@ struct Opt {
|
||||
/// indexes are not read.
|
||||
#[structopt(long)]
|
||||
no_search: bool,
|
||||
|
||||
/// Show all examples for the item instead of opening the full documentation.
|
||||
#[structopt(short, long)]
|
||||
examples: bool,
|
||||
}
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
@ -103,7 +107,17 @@ fn main() -> anyhow::Result<()> {
|
||||
|
||||
if let Some(doc) = doc {
|
||||
let viewer = opt.viewer.unwrap_or_else(viewer::get_default);
|
||||
viewer.open(&doc)
|
||||
if opt.examples {
|
||||
let examples = doc.find_examples()?;
|
||||
anyhow::ensure!(
|
||||
!examples.is_empty(),
|
||||
"Could not find examples for {}",
|
||||
&opt.keyword
|
||||
);
|
||||
viewer.open_examples(&doc, examples)
|
||||
} else {
|
||||
viewer.open(&doc)
|
||||
}
|
||||
} else {
|
||||
// item selection cancelled by user
|
||||
Ok(())
|
||||
|
@ -28,6 +28,15 @@ fn parse_file<P: AsRef<path::Path>>(path: P) -> anyhow::Result<kuchiki::NodeRef>
|
||||
.context("Could not read HTML file")
|
||||
}
|
||||
|
||||
fn parse_string(s: impl Into<String>) -> anyhow::Result<kuchiki::NodeRef> {
|
||||
use kuchiki::traits::TendrilSink;
|
||||
|
||||
kuchiki::parse_html()
|
||||
.from_utf8()
|
||||
.read_from(&mut s.into().as_bytes())
|
||||
.context("Could not read HTML string")
|
||||
}
|
||||
|
||||
pub fn find_item<P: AsRef<path::Path>>(path: P, item: &str) -> anyhow::Result<Option<String>> {
|
||||
use std::ops::Deref;
|
||||
|
||||
@ -77,6 +86,27 @@ fn select_first(
|
||||
select(element, selector).map(|mut i| i.next())
|
||||
}
|
||||
|
||||
pub fn find_examples(s: &str) -> anyhow::Result<Vec<doc::Example>> {
|
||||
let element = parse_string(s)?;
|
||||
let examples = select(&element, ".rust-example-rendered")?;
|
||||
Ok(examples.map(|n| get_example(n.as_node())).collect())
|
||||
}
|
||||
|
||||
fn get_example(node: &kuchiki::NodeRef) -> doc::Example {
|
||||
let code = node.text_contents();
|
||||
let description_element = node.parent().as_ref().and_then(previous_sibling_element);
|
||||
let description = description_element
|
||||
.and_then(|n| {
|
||||
if n.text_contents().ends_with(':') {
|
||||
Some(n)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.and_then(|n| get_html(&n).ok());
|
||||
doc::Example::new(description, code)
|
||||
}
|
||||
|
||||
pub fn parse_item_doc(item: &doc::Item) -> anyhow::Result<doc::Doc> {
|
||||
let document = parse_file(&item.path)?;
|
||||
let definition_selector = if item.ty == doc::ItemType::Function {
|
||||
@ -480,6 +510,17 @@ fn next_sibling_element(node: &kuchiki::NodeRef) -> Option<kuchiki::NodeRef> {
|
||||
next
|
||||
}
|
||||
|
||||
fn previous_sibling_element(node: &kuchiki::NodeRef) -> Option<kuchiki::NodeRef> {
|
||||
let mut previous = node.previous_sibling();
|
||||
while let Some(node) = &previous {
|
||||
if node.as_element().is_some() {
|
||||
break;
|
||||
}
|
||||
previous = node.previous_sibling();
|
||||
}
|
||||
previous
|
||||
}
|
||||
|
||||
fn is_element(node: &kuchiki::NodeRef, name: &markup5ever::LocalName) -> bool {
|
||||
node.as_element()
|
||||
.map(|e| &e.name.local == name)
|
||||
|
@ -11,6 +11,8 @@ use crate::doc;
|
||||
|
||||
pub trait Viewer: fmt::Debug {
|
||||
fn open(&self, doc: &doc::Doc) -> anyhow::Result<()>;
|
||||
|
||||
fn open_examples(&self, doc: &doc::Doc, examples: Vec<doc::Example>) -> anyhow::Result<()>;
|
||||
}
|
||||
|
||||
pub fn get_viewer(s: &str) -> anyhow::Result<Box<dyn Viewer>> {
|
||||
|
@ -17,6 +17,8 @@ pub trait Printer: fmt::Debug {
|
||||
|
||||
fn print_html(&self, indent: usize, s: &str, show_links: bool) -> io::Result<()>;
|
||||
|
||||
fn print_code(&self, indent: usize, code: &str) -> io::Result<()>;
|
||||
|
||||
fn println(&self) -> io::Result<()>;
|
||||
}
|
||||
|
||||
@ -31,9 +33,7 @@ impl<P: Printer> TextViewer<P> {
|
||||
}
|
||||
|
||||
fn print_doc(&self, doc: &doc::Doc) -> io::Result<()> {
|
||||
let title = format!("{} {}", doc.ty.name(), doc.name.as_ref());
|
||||
self.printer
|
||||
.print_title(doc.name.krate(), &title, "rusty-man")?;
|
||||
self.print_title(doc)?;
|
||||
self.print_opt("SYNOPSIS", doc.definition.as_deref(), false)?;
|
||||
self.print_opt("DESCRIPTION", doc.description.as_deref(), true)?;
|
||||
for (ty, groups) in &doc.groups {
|
||||
@ -65,6 +65,32 @@ impl<P: Printer> TextViewer<P> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_examples(&self, doc: &doc::Doc, examples: Vec<doc::Example>) -> io::Result<()> {
|
||||
self.print_title(doc)?;
|
||||
self.print_heading(1, "Examples")?;
|
||||
|
||||
let n = examples.len();
|
||||
for (i, example) in examples.iter().enumerate() {
|
||||
if n > 1 {
|
||||
self.print_heading(2, &format!("Example {} of {}", i + 1, n))?;
|
||||
}
|
||||
if let Some(description) = &example.description {
|
||||
self.printer.print_html(6, description, true)?;
|
||||
self.printer.println()?;
|
||||
}
|
||||
self.printer.print_code(6, &example.code)?;
|
||||
self.printer.println()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_title(&self, doc: &doc::Doc) -> io::Result<()> {
|
||||
let title = format!("{} {}", doc.ty.name(), doc.name.as_ref());
|
||||
self.printer
|
||||
.print_title(doc.name.krate(), &title, "rusty-man")
|
||||
}
|
||||
|
||||
fn print_opt(&self, title: &str, s: Option<&str>, show_links: bool) -> io::Result<()> {
|
||||
if let Some(s) = s {
|
||||
self.print_heading(1, title)?;
|
||||
@ -109,6 +135,14 @@ impl<P: Printer> viewer::Viewer for TextViewer<P> {
|
||||
.or_else(ignore_pipe_error)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
fn open_examples(&self, doc: &doc::Doc, examples: Vec<doc::Example>) -> anyhow::Result<()> {
|
||||
spawn_pager();
|
||||
|
||||
self.print_examples(doc, examples)
|
||||
.or_else(ignore_pipe_error)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spawn_pager() {
|
||||
|
@ -44,6 +44,13 @@ impl super::Printer for PlainTextRenderer {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_code(&self, indent: usize, code: &str) -> io::Result<()> {
|
||||
for line in code.split('\n') {
|
||||
writeln!(io::stdout(), "{}{}", " ".repeat(indent), line)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_heading(&self, indent: usize, _level: usize, s: &str) -> io::Result<()> {
|
||||
writeln!(io::stdout(), "{}{}", " ".repeat(indent), s)
|
||||
}
|
||||
|
@ -53,6 +53,13 @@ impl super::Printer for RichTextRenderer {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_code(&self, indent: usize, code: &str) -> io::Result<()> {
|
||||
for line in code.split('\n') {
|
||||
writeln!(io::stdout(), "{}{}", " ".repeat(indent), line)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_heading(&self, indent: usize, level: usize, s: &str) -> io::Result<()> {
|
||||
let mut text = crossterm::style::style(s);
|
||||
if level < 4 {
|
||||
|
Loading…
Reference in New Issue
Block a user