code_tour/
lib.rs

1#![cfg_attr(nightly, feature(proc_macro_span))]
2
3use proc_macro2::{Span, TokenStream, TokenTree};
4use quote::{quote, quote_spanned, ToTokens};
5#[cfg(feature = "interactive")]
6use syn::Block;
7use syn::{parse, AttrStyle, Ident, ItemFn, Pat, Path, Stmt};
8
9mod colours;
10mod format;
11
12#[proc_macro_attribute]
13pub fn code_tour(
14    _attributes: proc_macro::TokenStream,
15    input: proc_macro::TokenStream,
16) -> proc_macro::TokenStream {
17    if let Ok(mut function) = parse::<ItemFn>(input.clone()) {
18        let block_statements = &function.block.stmts;
19        let mut statements = Vec::with_capacity(block_statements.len());
20
21        for statement in block_statements.into_iter() {
22            // That's a `let` binding.
23            if let Stmt::Local(local) = statement {
24                // Extract the local pattern.
25                if let Pat::Ident(local_pat) = &local.pat {
26                    // There is attributes, which are normally `///`
27                    // (outer doc) or `/** */` (outer block) comments
28                    // before the `let` binding.
29                    if !local.attrs.is_empty() {
30                        // Write `println!("{}", ident_comment)`.
31                        {
32                            let formatted_comment = local
33                                .attrs
34                                .iter()
35                                .filter_map(|attribute| match (attribute.style, &attribute.path) {
36                                    (
37                                        AttrStyle::Outer,
38                                        Path {
39                                            leading_colon: None,
40                                            segments,
41                                        },
42                                    ) if !segments.is_empty()
43                                        && segments[0].ident
44                                            == Ident::new("doc", Span::call_site()) =>
45                                    {
46                                        if let Some(TokenTree::Literal(literal)) =
47                                            attribute.tokens.clone().into_iter().nth(1)
48                                        {
49                                            let literal_string =
50                                                literal.to_string().replace("\\\'", "'");
51
52                                            Some(format!(
53                                                "▍{:<80}",
54                                                &literal_string[1..literal_string.len() - 1]
55                                            ))
56                                        } else {
57                                            None
58                                        }
59                                    }
60
61                                    _ => None,
62                                })
63                                .map(colours::comment)
64                                .collect::<Vec<String>>()
65                                .join("\n ");
66
67                            let empty_line = colours::comment(format!("▍{:<80}", " "));
68                            let formatted_comment = format!(
69                                "\n {empty}\n {comment}\n {empty}\n",
70                                empty = empty_line,
71                                comment = formatted_comment
72                            );
73
74                            statements.push(println(quote!("{}", #formatted_comment)));
75                        }
76
77                        let mut local_without_attrs = local.clone();
78                        local_without_attrs.attrs = vec![];
79
80                        // Write `println!("{}", stringify!(<local>))`.
81                        {
82                            let statement =
83                                colours::statement(format::rust_code(&local_without_attrs));
84
85                            statements.push(println(quote!(" {}\n", #statement)));
86                        }
87
88                        // Write the original statement, without the documentation.
89                        {
90                            statements.push(Stmt::Local(local_without_attrs));
91                        }
92
93                        // Write `println!("{:?}", <ident>)`.
94                        {
95                            statements.push(println({
96                                let ident = &local_pat.ident;
97
98                                quote!(
99                                    " ◀︎    {}\n\n",
100                                    format!("{:#?}", #ident).replace("\n", "\n ▐    ")
101                                )
102                            }));
103                        }
104
105                        // Insert “Press Enter to continue…”
106                        #[cfg(feature = "interactive")]
107                        {
108                            let stream = quote!({
109                                {
110                                    use std::io::BufRead;
111
112                                    let mut line = String::new();
113                                    let stdin = ::std::io::stdin();
114
115                                    println!(
116                                        "\n(Press Enter to continue, otherwise Ctrl-C to exit).\n\n"
117                                    );
118
119                                    stdin
120                                        .lock()
121                                        .read_line(&mut line)
122                                        .expect("Failed to read a line from the user.");
123                                }
124                            });
125
126                            let block = parse::<Block>(stream.into()).unwrap();
127
128                            for statement in block.stmts {
129                                statements.push(statement);
130                            }
131                        }
132
133                        continue;
134                    }
135                }
136            }
137
138            statements.push(statement.clone());
139        }
140
141        function.block.stmts = statements;
142
143        quote!(#function).to_token_stream().into()
144    } else {
145        let span = TokenStream::from(input).into_iter().nth(0).unwrap().span();
146        quote_spanned!(span => compile_error!("`code_tour` works on functions only")).into()
147    }
148}
149
150fn println(tokens: TokenStream) -> Stmt {
151    let stream = quote!(if ::std::env::var("CODE_TOUR_QUIET").is_err() {
152        println!(#tokens);
153    });
154    let semi = parse::<Stmt>(stream.into()).unwrap();
155
156    semi
157}